#!/usr/bin/env node import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { chmod, mkdir, mkdtemp, readFile, rm, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { attachmentManifest, contentDigest, createDraftIdentity, normalize, parseConfirmationToken, preview, verifySecret, } from "./intake-lib.mjs"; const scriptRoot = dirname(fileURLToPath(import.meta.url)); const temporary = await mkdtemp(resolve(tmpdir(), "easyai-intake-v2-")); try { const minimal = normalize({ type: "需求", title: "在设置页展示版本号", problem_or_goal: "便于支持人员确认当前版本", expected_outcome: "设置页可看到当前前端版本号", }); assert.equal(minimal.priority, "P2"); assert.deepEqual(minimal.acceptance_criteria, []); assert.deepEqual(minimal.impact_scope, []); assert.deepEqual(preview(minimal), { 类型: "需求", 标题: "在设置页展示版本号", 背景与目标: "便于支持人员确认当前版本", 期望结果: "设置页可看到当前前端版本号", 优先级: "P2", }); const incompleteBug = normalize({ type: "Bug", title: "保存后页面未更新", background: "负责人观察到保存结果没有出现在列表", expected_result: "保存成功后列表显示新数据", }); assert.equal(incompleteBug.environment, undefined); assert.deepEqual(incompleteBug.reproduction_steps, undefined); assert.throws( () => normalize({ type: "需求", title: "x", background: "y" }), /期望结果缺失/, ); assert.throws( () => normalize({ ...minimalInput(), impact_scope: ["全栈", "前端"], }), /不能再同时选择/, ); assert.doesNotThrow(() => normalize({ ...minimalInput(), impact_scope: ["部署"] }), ); assert.throws( () => normalize({ ...minimalInput(), desired_at: "2026-02-30" }), /不是有效日期时间/, ); assert.throws( () => normalize({ ...minimalInput(), attachments: ["../secret.txt"] }), /不安全的附件路径/, ); assert.throws( () => normalize({ ...minimalInput(), task_id: "EAI-9999" }), /禁止或未知字段/, ); const firstIdentity = createDraftIdentity("ou_owner"); const secondIdentity = createDraftIdentity("ou_owner"); assert.notEqual(firstIdentity.draft_id, secondIdentity.draft_id); assert.notEqual(firstIdentity.submission_key, secondIdentity.submission_key); const parsed = parseConfirmationToken(firstIdentity.confirmation_token); assert.equal(parsed.draftId, firstIdentity.draft_id); assert.equal( verifySecret(parsed.secret, firstIdentity.confirmation_token_hash), true, ); await writeFile(resolve(temporary, "evidence.txt"), "first"); const attachmentValue = normalize({ ...minimalInput(), attachments: ["evidence.txt"], }); const firstManifest = await attachmentManifest( attachmentValue.attachments, temporary, ); const firstDigest = contentDigest(attachmentValue, "ou_owner", firstManifest); await writeFile(resolve(temporary, "evidence.txt"), "second"); const secondManifest = await attachmentManifest( attachmentValue.attachments, temporary, ); assert.notEqual( firstDigest, contentDigest(attachmentValue, "ou_owner", secondManifest), ); const fakeCli = resolve(temporary, "fake-lark-cli.mjs"); await writeFile(fakeCli, fakeLarkCli(), { mode: 0o700 }); await chmod(fakeCli, 0o700); const intakeHome = resolve(temporary, "home"); const env = { ...process.env, EASYAI_INTAKE_HOME: intakeHome, EASYAI_LARK_CLI: fakeCli, }; const prepared = runJson( "prepare-intake.mjs", [], { ...minimalInput(), constraints: ["必须复用既有组件"] }, env, ); assert.equal(prepared.status_code, "prepared"); assert.match( prepared.confirmation_token, new RegExp(`^${prepared.draft_id}\\.`), ); const draft = JSON.parse( await readFile( resolve( intakeHome, ".easyai-intake", "drafts", `${prepared.draft_id}.json`, ), "utf8", ), ); assert.equal(draft.owner_open_id, "ou_owner"); assert.equal(draft.status_code, "prepared"); const dryRun = runJson( "submit-intake.mjs", ["--confirm", prepared.confirmation_token, "--dry-run"], undefined, env, ); assert.equal(dryRun.dry_run, true); const submitted = runJson( "submit-intake.mjs", ["--confirm", prepared.confirmation_token], undefined, env, ); assert.equal(submitted.status_code, "submitted"); const duplicate = runJson( "submit-intake.mjs", ["--confirm", prepared.confirmation_token], undefined, env, ); assert.equal(duplicate.duplicate, true); const unknownDraft = runJson( "prepare-intake.mjs", [], { ...minimalInput(), title: "未知结果恢复" }, env, ); const failed = runJson( "submit-intake.mjs", ["--confirm", unknownDraft.confirmation_token], undefined, { ...env, EASYAI_FAKE_FAIL_FORM: "1" }, 1, ); assert.equal(failed.status_code, "unknown"); const blockedRetry = runJson( "submit-intake.mjs", ["--confirm", unknownDraft.confirmation_token], undefined, env, 1, ); assert.match(blockedRetry.error, /--retry-unknown/); const recovered = runJson( "submit-intake.mjs", ["--confirm", unknownDraft.confirmation_token, "--retry-unknown"], undefined, env, ); assert.equal(recovered.status_code, "submitted"); const lockedDraft = runJson( "prepare-intake.mjs", [], { ...minimalInput(), title: "并发锁测试" }, env, ); await mkdir( resolve(intakeHome, ".easyai-intake", "locks", lockedDraft.draft_id), { recursive: true }, ); const locked = runJson( "submit-intake.mjs", ["--confirm", lockedDraft.confirmation_token], undefined, env, 1, ); assert.match(locked.error, /正在提交/); console.log("intake v2 tests passed"); } finally { await rm(temporary, { recursive: true, force: true }); } function minimalInput() { return { type: "需求", title: "在设置页展示版本号", background: "便于支持人员确认当前版本", expected_result: "设置页可看到当前前端版本号", }; } function runJson(script, args, input, env, expectedStatus = 0) { const result = spawnSync( process.execPath, [resolve(scriptRoot, script), ...args], { cwd: temporary, env, encoding: "utf8", input: input === undefined ? undefined : JSON.stringify(input), }, ); assert.equal( result.status, expectedStatus, `${script} status=${result.status}\nstdout=${result.stdout}\nstderr=${result.stderr}`, ); return JSON.parse( (expectedStatus === 0 ? result.stdout : result.stderr).trim(), ); } function fakeLarkCli() { return `#!/usr/bin/env node const args = process.argv.slice(2); const ok = (data) => console.log(JSON.stringify({ ok: true, data })); if (args[0] === "auth") { ok({ identities: { user: { verified: true, openId: "ou_owner" } } }); } else if (args.includes("+form-detail")) { const question = (title, type, required = false, options = []) => ({ title, type, required, options: options.map((name) => ({ name })), }); ok({ base_token: "Ed21b9VwNaZfiesxWgDc67wXnoc", questions: [ question("类型", "select", true, ["需求", "Bug"]), question("标题", "text", true), question("需求背景与目标", "text", true), question("期望结果", "text", true), question("优先级", "select", true, ["P0", "P1", "P2", "P3"]), question("产品负责人", "user", true), question("提交幂等键", "text", true), question("提交来源", "select", true, ["Codex Skill"]), question("验收标准", "text"), question("发生环境", "text"), question("复现步骤", "text"), question("实际结果", "text"), question("影响范围", "select", false, ["前端", "后端", "全栈", "部署"]), question("约束与非目标", "text"), question("期望完成时间", "datetime"), question("参考资料", "text"), question("附件", "attachment"), ], }); } else if (args.includes("+record-search")) { ok({ data: [], record_id_list: [] }); } else if (args.includes("+form-submit")) { if (process.env.EASYAI_FAKE_FAIL_FORM === "1") { console.error(JSON.stringify({ ok: false, error: { message: "network interrupted" } })); process.exit(1); } ok({ record: { record_id: "rec_test" } }); } else if (args.includes("+record-upsert")) { const payload = JSON.parse(args[args.indexOf("--json") + 1] ?? "{}"); if (!/^[a-f0-9]{64}$/.test(payload["原始意图指纹"] ?? "")) { console.error(JSON.stringify({ ok: false, error: { message: "missing owner intent fingerprint" } })); process.exit(1); } ok({ record_id: "rec_test" }); } else { console.error(JSON.stringify({ ok: false, error: { message: "unexpected command: " + args.join(" ") } })); process.exit(1); } `; }