Files
easyai-codex-plugins/plugins/easyai-product-intake/skills/easyai-submit-task/scripts/test-intake.mjs
T

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);
}
`;
}