test: close local release gate regressions
This commit is contained in:
@@ -3227,3 +3227,18 @@
|
||||
- 防复发:golden fixture 只能保存 synthetic intent code/context tags,不得保存真实用户正文或出生资料;事实、权限、证据、状态与精确时间边界只能由确定性门禁判定,模型评审必须保持 pending 直到真实执行;catalog membership 不能替代本轮 evidence availability,`requiresEvidence` 不能关闭事实/时间证据规则,accepted 不得升级为 confirmed;Agent observability 不得加入自由格式 metadata、正文、prompt、messages、出生资料、身份信息、secret/API key、provider payload、stack 或内部路径,结算失败不得降级为 cancelled,公开 NDJSON 不得扩展为内部 telemetry。
|
||||
- 相关记录:BUG-181、BUG-186、BUG-187、BUG-188、BUG-189
|
||||
- 修复版本:本次功能分支提交(精确 SHA 以提交与远程分支核对结果为准;未合并 main/staging,未部署)
|
||||
|
||||
## BUG-191 | PR-8 全量发布门禁被过期测试合同与本地解释器假设阻断
|
||||
|
||||
- 状态:resolved(本地候选,待 staging 精确 SHA 发布验收)
|
||||
- 首次发现:2026-08-15
|
||||
- 最近更新:2026-08-15
|
||||
- 影响面:PR-8 全量前端测试、TypeScript 编译、本地 PostgreSQL 业务迁移测试、staging workflow YAML 合同与质量门禁。
|
||||
- 用户现象:PR-1 至 PR-8 功能提交本身已完成,但全量前端门禁仍有 5 个失败:starter questions 继续锁死旧四主题并扫描派生 wrapper 的重复文案;数据库业务测试继续调用已撤权的 legacy 校时候选接受 RPC;生产迁移 fixture 缺少新增列元数据;staging workflow 测试使用 ES2018 `/s` flag 且假设 PATH 中存在可导入 PyYAML 的 `python`。因此不能安全提交、推送或发布 staging。
|
||||
- 触发条件:在 PR-7 十域 registry、PR-4 V2 candidate decision contract、生产迁移列模型与独立 staging workflow 合同合并后运行全量 `tsx --test tests/*.test.ts`、`tsc --noEmit` 或 Python quality gate。
|
||||
- 根因:测试仍复制旧业务常量和旧 RPC 调用方式,没有跟随新的单一真源与服务端 UUID 合同;测试 fixture 未补齐列模型新增字段;Node 启动的 YAML 检查未继承质量门禁实际使用的 Python 解释器,并包含依赖特定 worktree 深度的临时 fallback。
|
||||
- 修复:starter tests 改为验证全部 10 个 canonical domain、`label`/`prompt`/evidence/claim 投影及按 domain 归属的 D10、D9、Ashtakavarga、negative holdout gate;billing 测试分别锁定 reserve/complete/release 的免费 turn 短路与付费调用;数据库测试保留 legacy RPC 撤权断言,并恢复 `persist_agentic_rectification_candidate_v2` 到 `accept_agentic_rectification_candidate_for_case_v2` 的真实纵向链路,覆盖服务端 candidate UUID、首次接受、幂等重放、切换候选、profile 落库、reported time 保留及基线变化后的 expired 拒绝;生产迁移 fixture 补齐 identity/data type 元数据;YAML 检查改用 `PYTHON`、`VIRTUAL_ENV`、仓库 `.venv` 与 PATH fallback,quality gate 通过 `os.environ.setdefault("PYTHON", sys.executable)` 向前端测试传递解释器,并移除 worktree 层级假设。
|
||||
- 验证:聚焦非数据库测试 70 passed、0 failed;本地 PostgreSQL 全迁移与业务链路 1 passed、0 failed;全量前端 1532 passed、0 failed;`tsc --noEmit` 通过;ESLint 0 errors、4 个既有 warnings;Next 16 webpack production build 与静态生成通过;`git diff --check` 通过。四档质量门禁中的 quick、browser、accuracy 已完整通过;release 档的公开发布隐私扫描通过(3212 files、0 findings),三引擎 parity validator 命令成功执行且 golden cases 3/3 passed。三引擎业务比对仍如实记录为 92 行中 32 match、60 mismatch、无缺失引擎或高严谨 section,未启用 `--require-external-parity`,不得解释为外部公式完全一致。release 档唯一未完成项是 Playwright/Chromium 报告渲染隔离 POC;因本机浏览器进程持续闪退并按用户明确要求停止,未将该项宣称为 passed,后续不再通过 Playwright 调用 Chrome。staging 仍需以正式 Gitea 独立质量门禁、精确 SHA 部署与非 Playwright HTTP/API smoke 完成验收。
|
||||
- 防复发:测试必须读取 canonical registry,不得重新硬编码派生业务真源;正式 V2 RPC 替代 legacy RPC 时必须保留完整成功、幂等、切换、失效和持久化业务覆盖,不能以“旧入口被拒绝”替代纵向链路;跨语言测试必须显式传递当前解释器,不得依赖 PATH 别名或 worktree 深度;类型 fixture 必须跟随共享列模型演进。
|
||||
- 相关记录:BUG-178、BUG-181、BUG-189、BUG-190
|
||||
- 修复版本:本次功能分支提交(精确 SHA 以提交、远程分支和 staging 发布核对结果为准)
|
||||
|
||||
@@ -8,6 +8,14 @@ const rectificationRoute = readFileSync(new URL("src/app/api/rectification/agent
|
||||
const consultRoute = readFileSync(new URL("src/app/api/consult/route.ts", root), "utf8");
|
||||
const packagesRoute = readFileSync(new URL("src/app/api/admin/packages/route.ts", root), "utf8");
|
||||
|
||||
function sourceBetween(source: string, start: string, end: string): string {
|
||||
const startIndex = source.indexOf(start);
|
||||
const endIndex = source.indexOf(end, startIndex + start.length);
|
||||
assert.ok(startIndex >= 0, `missing start marker: ${start}`);
|
||||
assert.ok(endIndex > startIndex, `missing end marker: ${end}`);
|
||||
return source.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
test("Agentic rectification reuses one case-level usage authorization and the session-pinned model version", () => {
|
||||
assert.match(rectificationRoute, /select\("id,messages,session_type,model_id,model_config_version,agentic_rectification_case_id"\)/);
|
||||
assert.match(rectificationRoute, /resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version,?\s*\)/);
|
||||
@@ -21,9 +29,19 @@ test("Agentic rectification reuses one case-level usage authorization and the se
|
||||
assert.match(rectificationRoute, /releaseUsage\(accounting, userId, billingRequestId,/);
|
||||
});
|
||||
|
||||
test("free Agentic rectification turns bypass both usage reservation and settlement", () => {
|
||||
assert.equal(rectificationRoute.match(/if \(action !== "message"\)/g)?.length, 2);
|
||||
assert.match(rectificationRoute, /async complete\(usage\) \{\s*if \(action !== "message"\) return true;/);
|
||||
test("free Agentic rectification turns bypass reservation, completion, and cancellation settlement", () => {
|
||||
const reserveBilling = sourceBetween(rectificationRoute, "async reserve() {", "async complete(usage) {");
|
||||
const completeBilling = sourceBetween(rectificationRoute, "async complete(usage) {", "async release() {");
|
||||
const releaseBilling = sourceBetween(rectificationRoute, "async release() {", " };\n\n try {");
|
||||
|
||||
assert.match(rectificationRoute, /action: z\.enum\(\["opening", "message", "read_only"\]\)/);
|
||||
assert.equal(rectificationRoute.match(/if \(action !== "message"\)/g)?.length, 3);
|
||||
assert.match(reserveBilling, /if \(action !== "message"\) \{[\s\S]*return \{ success: true, status: 200 \};/);
|
||||
assert.match(reserveBilling, /authorizeUsage\(/);
|
||||
assert.match(completeBilling, /if \(action !== "message"\) return true;/);
|
||||
assert.match(completeBilling, /completeUsage\(/);
|
||||
assert.match(releaseBilling, /if \(action !== "message"\) return true;/);
|
||||
assert.match(releaseBilling, /releaseUsage\(/);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,23 @@ const runnerPath = fileURLToPath(
|
||||
new URL("../scripts/db-migrate.mjs", import.meta.url),
|
||||
);
|
||||
|
||||
function rpcError(error: unknown): string {
|
||||
if (!error || typeof error !== "object") return "";
|
||||
const value = error as { message?: unknown };
|
||||
return typeof value.message === "string" ? value.message : "";
|
||||
}
|
||||
|
||||
function candidateIds(value: unknown): string[] {
|
||||
if (!value || typeof value !== "object") return [];
|
||||
const candidates = (value as { candidates?: unknown }).candidates;
|
||||
if (!Array.isArray(candidates)) return [];
|
||||
return candidates.flatMap((candidate) => {
|
||||
if (!candidate || typeof candidate !== "object") return [];
|
||||
const candidateId = (candidate as { candidate_id?: unknown }).candidate_id;
|
||||
return typeof candidateId === "string" ? [candidateId] : [];
|
||||
});
|
||||
}
|
||||
|
||||
test("local PostgreSQL applies the reviewed business schema and serves authenticated business calls", async () => {
|
||||
const fixture = startPostgresFixture();
|
||||
const schemaUrl = fixture.connectionUrl(
|
||||
@@ -600,62 +617,11 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
|
||||
{ time: "05:07", relative_support: 40 },
|
||||
]);
|
||||
assert.equal(
|
||||
fixture.psqlAs(
|
||||
"service_runtime",
|
||||
"service-runtime-test-password",
|
||||
`set role service_role;
|
||||
select (result ->> 'saved_time') || ':' || (result ->> 'status') || ':' || (result ->> 'idempotent')
|
||||
from (
|
||||
select public.accept_agentic_rectification_candidate(
|
||||
'${userId}', '${rectificationSessionId}', '33333333-3333-4333-8333-333333333333', '04:55'
|
||||
) as result
|
||||
) accepted`,
|
||||
),
|
||||
"SET\n04:55:accepted:false",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select to_char(active_birth_time, 'HH24:MI') || ':' || birth_time_status || ':' || to_char(reported_birth_time, 'HH24:MI') || ':' || coalesce(to_char(birth_time, 'HH24:MI'), 'null') from public.profiles where id = '${userId}'`),
|
||||
"04:55:accepted:05:00:null",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psqlAs(
|
||||
"service_runtime",
|
||||
"service-runtime-test-password",
|
||||
`set role service_role;
|
||||
select result ->> 'idempotent'
|
||||
from (
|
||||
select public.accept_agentic_rectification_candidate(
|
||||
'${userId}', '${rectificationSessionId}', '33333333-3333-4333-8333-333333333333', '04:55'
|
||||
) as result
|
||||
) accepted`,
|
||||
),
|
||||
"SET\ntrue",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psqlAs(
|
||||
"service_runtime",
|
||||
"service-runtime-test-password",
|
||||
`set role service_role;
|
||||
select (result ->> 'saved_time') || ':' || (result ->> 'status') || ':' || (result ->> 'idempotent')
|
||||
from (
|
||||
select public.accept_agentic_rectification_candidate(
|
||||
'${userId}', '${rectificationSessionId}', '33333333-3333-4333-8333-333333333333', '05:07'
|
||||
) as result
|
||||
) accepted`,
|
||||
),
|
||||
"SET\n05:07:accepted:false",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select to_char(active_birth_time, 'HH24:MI') || ':' || birth_time_status || ':' || to_char(reported_birth_time, 'HH24:MI') || ':' || coalesce(to_char(birth_time, 'HH24:MI'), 'null') from public.profiles where id = '${userId}'`),
|
||||
"05:07:accepted:05:00:null",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select invalidated_at is null from public.agentic_rectification_results where id = '33333333-3333-4333-8333-333333333333'`),
|
||||
"t",
|
||||
);
|
||||
fixture.psql(`update public.profiles set reported_birth_time = '05:01' where id = '${userId}'`);
|
||||
assert.equal(
|
||||
fixture.psql(`select invalidated_at is not null from public.agentic_rectification_results where id = '33333333-3333-4333-8333-333333333333'`),
|
||||
fixture.psql(`
|
||||
select
|
||||
not has_function_privilege('service_role', 'public.accept_agentic_rectification_candidate(uuid,uuid,uuid,time without time zone)', 'EXECUTE')
|
||||
and has_function_privilege('service_role', 'public.accept_agentic_rectification_candidate_for_case_v2(uuid,uuid,uuid,uuid,uuid)', 'EXECUTE')
|
||||
`),
|
||||
"t",
|
||||
);
|
||||
assert.throws(
|
||||
@@ -667,7 +633,158 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
|
||||
'${userId}', '${rectificationSessionId}', '33333333-3333-4333-8333-333333333333', '04:55'
|
||||
)`,
|
||||
),
|
||||
/agentic_rectification_candidate_expired/,
|
||||
/permission denied for function accept_agentic_rectification_candidate/,
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select coalesce(to_char(active_birth_time, 'HH24:MI'), 'null') || ':' || birth_time_status || ':' || to_char(reported_birth_time, 'HH24:MI') from public.profiles where id = '${userId}'`),
|
||||
"null:reported:05:00",
|
||||
);
|
||||
|
||||
const rectificationCaseId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||||
const baselineBirthSnapshot = {
|
||||
birth_date: "1997-08-08",
|
||||
reported_birth_time: "05:00",
|
||||
active_birth_time: null,
|
||||
birth_time_source: "family_exact",
|
||||
birth_time_period: null,
|
||||
uncertainty_before_minutes: 10,
|
||||
uncertainty_after_minutes: 10,
|
||||
latitude: 36.420487,
|
||||
longitude: 114.209936,
|
||||
timezone_offset: 8,
|
||||
};
|
||||
const candidateRange = { start_time: "04:50", end_time: "05:10" };
|
||||
fixture.psql(`
|
||||
insert into public.agentic_rectification_cases (
|
||||
id, user_id, session_id, status, skill_name, skill_version,
|
||||
baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range
|
||||
) values (
|
||||
'${rectificationCaseId}', '${userId}', '${rectificationSessionId}', 'candidate_ready',
|
||||
'jyotish-birth-time-rectification', '10.0.0', '${"e".repeat(64)}',
|
||||
'${JSON.stringify(baselineBirthSnapshot)}'::jsonb,
|
||||
'${JSON.stringify(candidateRange)}'::jsonb
|
||||
)
|
||||
`);
|
||||
|
||||
const persisted = await admin.rpc("persist_agentic_rectification_candidate_v2", {
|
||||
p_user_id: userId,
|
||||
p_case_id: rectificationCaseId,
|
||||
p_engine_result_id: "database-local-business-v2",
|
||||
p_evidence_ledger_fingerprint: "f".repeat(64),
|
||||
p_candidate_range_fingerprint: "1".repeat(64),
|
||||
p_skill_version: "10.0.0",
|
||||
p_algorithm_version: "database-local-business-v2",
|
||||
p_event_contract_version: "rectification-event-contract-v2",
|
||||
p_decision_policy_version: "decision-policy-v2",
|
||||
p_candidate_range: candidateRange,
|
||||
p_candidates: [
|
||||
{ rank: 1, time: "04:55", relative_support: 60, tied_minute_count: 1 },
|
||||
{ rank: 2, time: "05:07", relative_support: 40, tied_minute_count: 1 },
|
||||
],
|
||||
p_decision_receipt: {
|
||||
display_allowed: true,
|
||||
accept_allowed: true,
|
||||
confirm_allowed: false,
|
||||
representative_time: "04:55",
|
||||
overall_confidence: "medium",
|
||||
margin_percent: 20,
|
||||
},
|
||||
p_execution_ledger: [
|
||||
{ phase: "candidate.score", status: "completed", engine: "fixture-engine" },
|
||||
{ phase: "decision.evaluate", status: "completed", policy: "decision-policy-v2" },
|
||||
],
|
||||
});
|
||||
assert.equal(persisted.error, null, rpcError(persisted.error));
|
||||
const persistedRow = persisted.data as Record<string, unknown>;
|
||||
const v2ResultId = String(persistedRow.result_id);
|
||||
const [firstCandidateId, secondCandidateId] = candidateIds(persistedRow);
|
||||
assert.ok(firstCandidateId);
|
||||
assert.ok(secondCandidateId);
|
||||
|
||||
const firstAcceptRequestId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
||||
const firstAccepted = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||||
p_user_id: userId,
|
||||
p_case_id: rectificationCaseId,
|
||||
p_result_id: v2ResultId,
|
||||
p_candidate_id: firstCandidateId,
|
||||
p_request_id: firstAcceptRequestId,
|
||||
});
|
||||
assert.equal(firstAccepted.error, null, rpcError(firstAccepted.error));
|
||||
assert.deepEqual(
|
||||
{
|
||||
saved_time: (firstAccepted.data as Record<string, unknown>).saved_time,
|
||||
status: (firstAccepted.data as Record<string, unknown>).status,
|
||||
case_status: (firstAccepted.data as Record<string, unknown>).case_status,
|
||||
idempotent: (firstAccepted.data as Record<string, unknown>).idempotent,
|
||||
},
|
||||
{ saved_time: "04:55", status: "accepted", case_status: "candidate_accepted", idempotent: false },
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`
|
||||
select to_char(active_birth_time, 'HH24:MI') || ':' ||
|
||||
to_char(birth_time, 'HH24:MI') || ':' || birth_time_status || ':' ||
|
||||
to_char(reported_birth_time, 'HH24:MI')
|
||||
from public.profiles where id = '${userId}'
|
||||
`),
|
||||
"04:55:04:55:accepted:05:00",
|
||||
);
|
||||
|
||||
const firstReplay = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||||
p_user_id: userId,
|
||||
p_case_id: rectificationCaseId,
|
||||
p_result_id: v2ResultId,
|
||||
p_candidate_id: firstCandidateId,
|
||||
p_request_id: firstAcceptRequestId,
|
||||
});
|
||||
assert.equal(firstReplay.error, null, rpcError(firstReplay.error));
|
||||
assert.equal((firstReplay.data as Record<string, unknown>).idempotent, true);
|
||||
|
||||
const secondAccepted = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||||
p_user_id: userId,
|
||||
p_case_id: rectificationCaseId,
|
||||
p_result_id: v2ResultId,
|
||||
p_candidate_id: secondCandidateId,
|
||||
p_request_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
||||
});
|
||||
assert.equal(secondAccepted.error, null, rpcError(secondAccepted.error));
|
||||
assert.deepEqual(
|
||||
{
|
||||
saved_time: (secondAccepted.data as Record<string, unknown>).saved_time,
|
||||
status: (secondAccepted.data as Record<string, unknown>).status,
|
||||
case_status: (secondAccepted.data as Record<string, unknown>).case_status,
|
||||
idempotent: (secondAccepted.data as Record<string, unknown>).idempotent,
|
||||
},
|
||||
{ saved_time: "05:07", status: "accepted", case_status: "candidate_accepted", idempotent: false },
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select to_char(active_birth_time, 'HH24:MI') || ':' || to_char(birth_time, 'HH24:MI') || ':' || to_char(reported_birth_time, 'HH24:MI') from public.profiles where id = '${userId}'`),
|
||||
"05:07:05:07:05:00",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
fixture.psql(`select invalidated_at is null from public.agentic_rectification_results where id = '33333333-3333-4333-8333-333333333333'`),
|
||||
"t",
|
||||
);
|
||||
fixture.psql(`update public.profiles set reported_birth_time = '05:01' where id = '${userId}'`);
|
||||
assert.equal(
|
||||
fixture.psql(`select invalidated_at is not null from public.agentic_rectification_results where id = '33333333-3333-4333-8333-333333333333'`),
|
||||
"t",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select invalidated_at is not null from public.agentic_rectification_results where id = '${v2ResultId}'`),
|
||||
"t",
|
||||
);
|
||||
const expiredAccept = await admin.rpc("accept_agentic_rectification_candidate_for_case_v2", {
|
||||
p_user_id: userId,
|
||||
p_case_id: rectificationCaseId,
|
||||
p_result_id: v2ResultId,
|
||||
p_candidate_id: firstCandidateId,
|
||||
p_request_id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
||||
});
|
||||
assert.match(rpcError(expiredAccept.error), /agentic_rectification_candidate_expired/);
|
||||
assert.equal(
|
||||
await loadLatestAgenticRectificationResult(admin as never, userId, rectificationSessionId),
|
||||
null,
|
||||
);
|
||||
|
||||
fixture.psql(`
|
||||
|
||||
@@ -336,9 +336,11 @@ test("transfer plan uses non-nullable dependencies and rejects unsafe cycles", (
|
||||
|
||||
target.get("parent")!.columns.push({
|
||||
name: "child_id", nullable: false, defaultValue: null, generated: false,
|
||||
identity: false, identityGeneration: null, dataType: "text", udtName: "text",
|
||||
});
|
||||
source.get("parent")!.columns.push({
|
||||
name: "child_id", nullable: false, defaultValue: null, generated: false,
|
||||
identity: false, identityGeneration: null, dataType: "text", udtName: "text",
|
||||
});
|
||||
target.get("parent")!.foreignKeys.push({
|
||||
columns: ["child_id"], refSchema: "public", refTable: "child", refColumns: ["id"],
|
||||
|
||||
@@ -107,6 +107,32 @@ function assertOrder(text: string, labels: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
function yamlPythonCandidates(): string[] {
|
||||
return [
|
||||
process.env.PYTHON,
|
||||
process.env.VIRTUAL_ENV ? join(process.env.VIRTUAL_ENV, "bin", "python") : undefined,
|
||||
fileURLToPath(new URL("../../.venv/bin/python", import.meta.url)),
|
||||
"python3",
|
||||
"python",
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
}
|
||||
|
||||
function parseYaml(workflow: URL) {
|
||||
for (const python of yamlPythonCandidates()) {
|
||||
const result = spawnSync(
|
||||
python,
|
||||
[
|
||||
"-c",
|
||||
"import sys,yaml; yaml.safe_load(open(sys.argv[1], encoding='utf-8'))",
|
||||
fileURLToPath(workflow),
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
if (!result.error || (result.error as NodeJS.ErrnoException).code !== "ENOENT") return result;
|
||||
}
|
||||
throw new Error("No Python interpreter is available for YAML validation");
|
||||
}
|
||||
|
||||
test("changed staging workflows are syntactically valid YAML", () => {
|
||||
for (const workflow of [
|
||||
qualityWorkflow,
|
||||
@@ -118,15 +144,7 @@ test("changed staging workflows are syntactically valid YAML", () => {
|
||||
giteaProductionWorkflow,
|
||||
giteaProductionMigrationWorkflow,
|
||||
]) {
|
||||
const result = spawnSync(
|
||||
"python",
|
||||
[
|
||||
"-c",
|
||||
"import sys,yaml; yaml.safe_load(open(sys.argv[1], encoding='utf-8'))",
|
||||
fileURLToPath(workflow),
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const result = parseYaml(workflow);
|
||||
assert.ifError(result.error);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
}
|
||||
@@ -932,7 +950,7 @@ test("production deploy is manual-only and consumes the accepted staging artifac
|
||||
assert.match(production, /endswith\("backend-quality-gate\.yml"\)/);
|
||||
assert.match(production, /endswith\("release-quality-gate\.yml"\)/);
|
||||
assert.match(production, /public staging has not accepted the requested SHA/);
|
||||
assert.match(production, /current_main.*DEPLOY_SHA.*current_staging.*DEPLOY_SHA/s);
|
||||
assert.match(production, /current_main[\s\S]*DEPLOY_SHA[\s\S]*current_staging[\s\S]*DEPLOY_SHA/);
|
||||
assert.match(production, /verification_mode:/);
|
||||
assert.match(production, /PRODUCTION_HOST/);
|
||||
assert.match(production, /118\.194\.235\.34/);
|
||||
@@ -1035,12 +1053,12 @@ test("Gitea production schema migration is exact-SHA gated and isolated from ETL
|
||||
assert.match(workflow, /^on:\n\s+workflow_dispatch:/m);
|
||||
assert.doesNotMatch(workflow, /workflow_run:|\n\s+push:/);
|
||||
assert.match(workflow, /\^\[0-9a-f\]\{40\}\$/);
|
||||
assert.match(workflow, /main_head.*DEPLOY_SHA.*staging_head.*DEPLOY_SHA/s);
|
||||
assert.match(workflow, /main_head[\s\S]*DEPLOY_SHA[\s\S]*staging_head[\s\S]*DEPLOY_SHA/);
|
||||
assert.match(workflow, /GITEA_SHA.*DEPLOY_SHA/);
|
||||
assert.match(workflow, /STAGING_URL: \$\{\{ vars\.STAGING_URL \}\}/);
|
||||
assert.match(workflow, /\[\[ "\$STAGING_URL" == "https:\/\/staging\.jyotisha\.chat" \]\]/);
|
||||
assert.match(workflow, /"\$STAGING_URL\/api\/health"/);
|
||||
assert.match(workflow, /observed_staging_sha.*DEPLOY_SHA/s);
|
||||
assert.match(workflow, /observed_staging_sha[\s\S]*DEPLOY_SHA/);
|
||||
assert.match(workflow, /branch=staging&event=push&status=success/);
|
||||
assert.match(workflow, /endswith\("backend-quality-gate\.yml"\)/);
|
||||
assert.match(workflow, /endswith\("release-quality-gate\.yml"\)/);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import {
|
||||
consultationDomainIds,
|
||||
consultationDomainRegistry,
|
||||
} from "../src/lib/consultation-domain-registry.ts";
|
||||
import {
|
||||
defaultGuidedJyotishTopics,
|
||||
generalGuidedJyotishTopics,
|
||||
@@ -38,29 +42,60 @@ test("completed account initialization switches directly to the home cards", ()
|
||||
assert.doesNotMatch(pageSource, /!profileComplete \|\| onboardingJustCompleted/);
|
||||
});
|
||||
|
||||
test("default starter questions are guided Jyotish topics with evidence and claim boundaries", () => {
|
||||
test("default starter questions derive every canonical domain with evidence and claim boundaries", () => {
|
||||
assert.match(pageSource, /defaultGuidedJyotishTopics/);
|
||||
assert.match(pageSource, /starterSuggestions\.map/);
|
||||
assert.match(pageSource, /starterThemes\.find\(\(candidate\) => candidate\.id === item\.theme\)/);
|
||||
assert.match(pageSource, /chooseSuggestedQuestion\(item\.text, item\.theme\)/);
|
||||
assert.match(guidedTopicsSource, /strictWorkflowRoute/);
|
||||
assert.match(guidedTopicsSource, /evidencePreview/);
|
||||
assert.match(guidedTopicsSource, /confidenceCap/);
|
||||
assert.match(guidedTopicsSource, /claimBoundary/);
|
||||
assert.match(guidedTopicsSource, /D10/);
|
||||
assert.match(guidedTopicsSource, /D9/);
|
||||
assert.match(guidedTopicsSource, /Ashtakavarga/);
|
||||
assert.match(guidedTopicsSource, /独立 holdout/);
|
||||
assert.equal(consultationDomainIds.length, 10);
|
||||
assert.deepEqual(consultationDomainRegistry.map((domain) => domain.id), [...consultationDomainIds]);
|
||||
assert.match(guidedTopicsSource, /consultationDomainRegistry\.map/);
|
||||
assert.deepEqual(defaultGuidedJyotishTopics.map((topic) => topic.id), [...consultationDomainIds]);
|
||||
assert.equal(defaultGuidedJyotishTopics.length, consultationDomainRegistry.length);
|
||||
|
||||
for (const [index, topic] of defaultGuidedJyotishTopics.entries()) {
|
||||
const domain = consultationDomainRegistry[index];
|
||||
assert.equal(topic.id, domain.id);
|
||||
assert.equal(topic.label, domain.label);
|
||||
assert.equal(topic.prompt, domain.prompt);
|
||||
assert.equal(topic.strictWorkflowRoute, domain.strictWorkflowRoute);
|
||||
assert.deepEqual(topic.evidencePreview, [...domain.evidencePreview]);
|
||||
assert.equal(topic.confidenceCap, domain.confidenceCap);
|
||||
assert.equal(topic.claimBoundary, domain.claimBoundary);
|
||||
}
|
||||
|
||||
const domainById = new Map(consultationDomainRegistry.map((domain) => [domain.id, domain]));
|
||||
assert.match(domainById.get("career")?.requiredLayers.join(" ") ?? "", /D10/);
|
||||
assert.match(domainById.get("marriage")?.requiredLayers.join(" ") ?? "", /D9/);
|
||||
assert.match(domainById.get("wealth")?.requiredLayers.join(" ") ?? "", /Ashtakavarga/);
|
||||
assert.match(domainById.get("timing")?.requiredLayers.join(" ") ?? "", /negative holdout gate/);
|
||||
});
|
||||
|
||||
test("profiles without a usable birth minute only receive general-knowledge homepage prompts", () => {
|
||||
assert.deepEqual(generalGuidedJyotishTopics.map((topic) => topic.id), defaultGuidedJyotishTopics.map((topic) => topic.id));
|
||||
test("profiles without a usable birth minute receive all canonical general-knowledge prompts", () => {
|
||||
assert.deepEqual(generalGuidedJyotishTopics.map((topic) => topic.id), [...consultationDomainIds]);
|
||||
assert.deepEqual(generalGuidedJyotishTopics.map((topic) => topic.prompt), [
|
||||
"印度占星一般会从哪些因素理解事业方向?",
|
||||
"印度占星一般如何分析关系模式?",
|
||||
"印度占星一般如何分析财富结构与风险?",
|
||||
"印度占星如何在非医疗诊断边界内理解身心压力?",
|
||||
"印度占星一般如何理解学习方式与进阶节奏?",
|
||||
"印度占星一般如何分析迁居、置业与海外发展?",
|
||||
"印度占星一般如何理解家庭关系与责任模式?",
|
||||
"印度占星中的年运分析通常包含哪些证据层?",
|
||||
"印度占星中的时间推运通常会看哪些因素?",
|
||||
"印度占星综合咨询会如何划分不同主题与证据边界?",
|
||||
]);
|
||||
for (const topic of generalGuidedJyotishTopics) {
|
||||
const personalTopic = defaultGuidedJyotishTopics.find((candidate) => candidate.id === topic.id);
|
||||
assert.ok(personalTopic);
|
||||
assert.equal(topic.label, personalTopic.label);
|
||||
assert.equal(topic.strictWorkflowRoute, personalTopic.strictWorkflowRoute);
|
||||
assert.deepEqual(topic.evidencePreview, personalTopic.evidencePreview);
|
||||
assert.equal(topic.confidenceCap, personalTopic.confidenceCap);
|
||||
assert.equal(topic.claimBoundary, personalTopic.claimBoundary);
|
||||
assert.notEqual(topic.prompt, personalTopic.prompt);
|
||||
}
|
||||
assert.ok(generalGuidedJyotishTopics.every((topic) => !/我的|我近期|未来一年,事业和收入/.test(topic.prompt)));
|
||||
assert.match(pageSource, /const starterThemes = personalChartAvailable \? themes : generalGuidedJyotishTopics/);
|
||||
assert.doesNotMatch(pageSource, /回答一般占星知识/);
|
||||
assert.match(pageSource, /完成生时校正后,再讨论个人星盘结论/);
|
||||
|
||||
@@ -20,6 +20,7 @@ from local_env import load_local_env # noqa: E402
|
||||
load_local_env(ROOT)
|
||||
APP = ROOT / "frontend"
|
||||
PYTHON = sys.executable
|
||||
os.environ.setdefault("PYTHON", PYTHON)
|
||||
|
||||
COMPILE_DIRS = [
|
||||
ROOT / "scripts",
|
||||
|
||||
Reference in New Issue
Block a user