309 lines
9.0 KiB
JavaScript
309 lines
9.0 KiB
JavaScript
#!/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);
|
|
}
|
|
`;
|
|
}
|