/** * Live post-install smoke test for the AcFun read tools. * * Exercises the same tool definitions the Harness registers, against the live * acfun.cn API, validates every canonical value against the tool's declared * output schema, and renders it. Write tools are expected to fail closed. * * Run with: node scripts/live-check.mjs */ import { AcfunProvider } from '../lib/provider.js' import { resolveConfig } from '../lib/config.js' import { createAcfunTools } from '../lib/tools.js' import { AcfunQrFlow } from '../lib/qr-flow.js' import { validateValue } from './schema-check.mjs' const provider = new AcfunProvider({ requestTimeoutMs: 30_000 }) const config = resolveConfig({}) const tools = new Map(createAcfunTools(provider, config).map((tool) => [tool.name, tool])) const exec = { signal: new AbortController().signal } const failures = [] /** Execute one tool, validate its canonical output and render it. */ async function run(toolName, args, { expectError } = {}) { const tool = tools.get(toolName) if (tool === undefined) { failures.push(`${toolName}: not registered`) return undefined } console.log(`\n=== ${toolName} ${JSON.stringify(args)}`) const started = Date.now() let value try { value = await tool.execute(args, exec) } catch (error) { if (expectError !== undefined) { if (error?.code === expectError) { console.log(` ok failed closed with ${error.code}: ${error.message}`) return undefined } failures.push(`${toolName}: expected ${expectError}, got ${error?.code ?? error?.name}: ${error?.message}`) return undefined } failures.push(`${toolName}: threw ${error?.name}: ${error?.message}`) return undefined } if (expectError !== undefined) { failures.push(`${toolName}: expected ${expectError} but the call succeeded`) return value } const schemaErrors = validateValue(value, tool.output.schema) if (schemaErrors.length > 0) failures.push(`${toolName}: output schema violations: ${schemaErrors.join('; ')}`) else console.log(' ok canonical value matches output schema') let rendered try { rendered = tool.output.render(args, value) } catch (error) { failures.push(`${toolName}: render threw ${error?.message}`) return value } const text = rendered.map((block) => block.text).join('\n') console.log(` ok rendered ${text.length} chars in ${Date.now() - started} ms`) console.log( text .split('\n') .slice(0, 12) .map((line) => ` | ${line}`) .join('\n'), ) return value } const search = await run('acfun_search', { keyword: '原神' }) await run('acfun_search', { keyword: '原神', sort: 'views' }) const acId = search?.items?.[0]?.acId ?? '42588725' console.log(`\n(using acId ${acId} for the follow-up calls)`) const video = await run('acfun_video', { acId }) await run('acfun_comments', { acId, page: 1 }) await run('acfun_danmaku', { acId, limit: 50 }) await run('acfun_rank', { period: 'day', limit: 5 }) await run('acfun_playurl', { acId }) await run('acfun_account', {}) await run('acfun_moment', { momentId: process.env.ACFUN_MOMENT_ID ?? '5093831' }) await run('acfun_moment_comments', { momentId: process.env.ACFUN_MOMENT_ID ?? '5093831', page: 1 }) await run('acfun_topic', { tagId: process.env.ACFUN_TOPIC_ID ?? '488' }) if (video !== undefined && video.title.length === 0) failures.push('acfun_video: empty title') // Interactions must fail closed without allowWriteActions + cookie. await run('acfun_interact', { action: 'like', acId }, { expectError: 'WRITE_DISABLED' }) await run('acfun_interact', { action: 'moment_like', momentId: process.env.ACFUN_MOMENT_ID ?? '5093831' }, { expectError: 'WRITE_DISABLED' }) await run('acfun_post_comment', { acId, content: 'live-check should not post this' }, { expectError: 'WRITE_DISABLED' }) // QR login: ask AcFun for a real ticket and check the waiting state. No phone // scans it, so this proves issuance + the long-poll waiting path only. console.log('\n=== QR login (live, never scanned)') try { const flow = new AcfunQrFlow({ longPollTimeoutMs: 8_000 }) const started = await flow.start() console.log(` ok issued ticket ${String(started.ticket).slice(0, 12)}… with QR ${started.qr.kind} (${started.qr.value.length} chars), ttl ${Math.round((started.expiresAt - Date.now()) / 1000)}s`) const aborter = new AbortController() const scan = await flow.waitForScan({ signal: aborter.signal }) console.log(` ok unscanned ticket reports state=${scan.state}`) if (scan.state === 'waiting') { const pending = flow.waitForScan({ signal: aborter.signal }) setTimeout(() => aborter.abort(), 400) try { await pending failures.push('QR wait did not abort promptly') } catch (error) { console.log(` ok abort cancels the long poll (${error?.name})`) } } } catch (error) { failures.push(`QR login live check failed: ${error?.name}: ${error?.message}`) } if (failures.length > 0) { console.error(`\n${failures.length} live check(s) failed`) for (const failure of failures) console.error(`- ${failure}`) process.exitCode = 1 } else { console.log('\nall live read checks passed; write tools failed closed as designed') }