210 lines
10 KiB
TypeScript
210 lines
10 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import {
|
|
buildDocumentNumber,
|
|
extractDocumentNumberFromLegacy,
|
|
parseDocumentNumber,
|
|
} from "../src/components/microservices/decision-center/src/document-contract";
|
|
import { runDecisionCenterCommandAsync } from "./src/decision-center";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
interface FetchCall {
|
|
path: string;
|
|
init?: { method?: string; body?: unknown };
|
|
}
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function source(path: string): string {
|
|
return readFileSync(path, "utf8");
|
|
}
|
|
|
|
function asRecord(value: unknown): JsonRecord {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {};
|
|
}
|
|
|
|
function makeFetcher(calls: FetchCall[]) {
|
|
return async (path: string, init?: { method?: string; body?: unknown }): Promise<unknown> => {
|
|
calls.push({ path, init });
|
|
return { ok: true, status: init?.method === "POST" ? 201 : 200, body: { ok: true, record: { id: "dc_test", docNo: "DC-GOAL-P0-2026-001" } } };
|
|
};
|
|
}
|
|
|
|
async function assertCliContract(): Promise<string[]> {
|
|
const checks: string[] = [];
|
|
const config = {} as Parameters<typeof runDecisionCenterCommandAsync>[0];
|
|
|
|
{
|
|
const calls: FetchCall[] = [];
|
|
await runDecisionCenterCommandAsync(config, [
|
|
"requirement",
|
|
"create",
|
|
"--title",
|
|
"Doc Create",
|
|
"--body",
|
|
"body",
|
|
"--type",
|
|
"goal",
|
|
"--priority",
|
|
"P0",
|
|
"--doc-type",
|
|
"GOAL",
|
|
"--doc-priority",
|
|
"P0",
|
|
"--doc-year",
|
|
"2026",
|
|
"--signer",
|
|
"Decision Center",
|
|
"--issued-at",
|
|
"2026-05-21",
|
|
"--effective-scope",
|
|
"unidesk",
|
|
], makeFetcher(calls));
|
|
const call = calls[0];
|
|
const body = asRecord(call?.init?.body);
|
|
assertCondition(call?.path === "/api/microservices/decision-center/proxy/api/requirements", "create must call requirements collection route", { call });
|
|
assertCondition(call?.init?.method === "POST", "create must use POST", { call });
|
|
assertCondition(body.docType === "GOAL" && body.docPriority === "P0" && body.docYear === 2026, "create must include document allocation fields", { body });
|
|
assertCondition(body.signer === "Decision Center" && body.issuedAt === "2026-05-21" && body.effectiveScope === "unidesk", "create must include document metadata fields", { body });
|
|
checks.push("cli-create-document-fields");
|
|
}
|
|
|
|
{
|
|
const calls: FetchCall[] = [];
|
|
await runDecisionCenterCommandAsync(config, [
|
|
"requirement",
|
|
"upsert",
|
|
"--id",
|
|
"dc_goal_big_paper_submission",
|
|
"--title",
|
|
"Doc Upsert",
|
|
"--body",
|
|
"body",
|
|
"--doc-no",
|
|
"dc-goal-p0-2026-001",
|
|
"--supersedes",
|
|
"DC-DCSN-P0-2026-001",
|
|
], makeFetcher(calls));
|
|
const call = calls[0];
|
|
const body = asRecord(call?.init?.body);
|
|
assertCondition(call?.init?.method === "PUT", "upsert must use PUT", { call });
|
|
assertCondition(body.docNo === "DC-GOAL-P0-2026-001", "upsert must normalize explicit docNo", { body });
|
|
assertCondition(Array.isArray(body.supersedes) && body.supersedes[0] === "DC-DCSN-P0-2026-001", "upsert must include supersedes list", { body });
|
|
checks.push("cli-upsert-document-fields");
|
|
}
|
|
|
|
{
|
|
const calls: FetchCall[] = [];
|
|
await runDecisionCenterCommandAsync(config, [
|
|
"requirement",
|
|
"update",
|
|
"DC-GOAL-P0-2026-001",
|
|
"--signer",
|
|
"Signer B",
|
|
"--superseded-by",
|
|
"DC-GOAL-P0-2026-002",
|
|
], makeFetcher(calls));
|
|
const call = calls[0];
|
|
const body = asRecord(call?.init?.body);
|
|
assertCondition(call?.path === "/api/microservices/decision-center/proxy/api/requirements/DC-GOAL-P0-2026-001", "update must address records by docNo", { call });
|
|
assertCondition(call?.init?.method === "PUT", "update must use PUT", { call });
|
|
assertCondition(body.signer === "Signer B", "update must include signer", { body });
|
|
assertCondition(Array.isArray(body.supersededBy) && body.supersededBy[0] === "DC-GOAL-P0-2026-002", "update must include supersededBy list", { body });
|
|
checks.push("cli-update-document-fields");
|
|
}
|
|
|
|
{
|
|
const calls: FetchCall[] = [];
|
|
await runDecisionCenterCommandAsync(config, [
|
|
"requirement",
|
|
"list",
|
|
"--doc-no",
|
|
"DC-GOAL-P0-2026-001",
|
|
"--doc-type",
|
|
"GOAL",
|
|
"--doc-priority",
|
|
"P0",
|
|
"--year",
|
|
"2026",
|
|
], makeFetcher(calls));
|
|
const path = calls[0]?.path ?? "";
|
|
assertCondition(path.includes("/api/requirements?"), "list must call requirements query route", { path });
|
|
assertCondition(path.includes("docNo=DC-GOAL-P0-2026-001"), "list must filter by docNo", { path });
|
|
assertCondition(path.includes("docType=GOAL") && path.includes("docPriority=P0") && path.includes("docYear=2026"), "list must filter by document components", { path });
|
|
checks.push("cli-list-document-query");
|
|
}
|
|
|
|
{
|
|
const calls: FetchCall[] = [];
|
|
await runDecisionCenterCommandAsync(config, ["requirement", "show", "DC-GOAL-P0-2026-001"], makeFetcher(calls));
|
|
assertCondition(calls[0]?.path === "/api/microservices/decision-center/proxy/api/requirements/DC-GOAL-P0-2026-001", "show must support docNo path keys", { call: calls[0] });
|
|
checks.push("cli-show-document-key");
|
|
}
|
|
|
|
return checks;
|
|
}
|
|
|
|
export async function runDecisionCenterDocumentContract(): Promise<JsonRecord> {
|
|
const service = source("src/components/microservices/decision-center/src/index.ts");
|
|
const cli = source("scripts/src/decision-center.ts");
|
|
const cliEntry = source("scripts/cli.ts");
|
|
const remote = source("scripts/src/remote.ts");
|
|
const doc = parseDocumentNumber("dc-goal-p0-2026-1");
|
|
assertCondition(doc.docNo === "DC-GOAL-P0-2026-001", "docNo parser must normalize sequence width", { doc });
|
|
assertCondition(buildDocumentNumber("DCSN", "P0", 2026, 1) === "DC-DCSN-P0-2026-001", "docNo builder must produce canonical sequence");
|
|
|
|
const legacyCases = [
|
|
{ id: "dc_decision_thesis_unidesk_integration_rule", expected: "DC-DCSN-P0-2026-001" },
|
|
{ id: "x", title: "DC-GOAL-P0-2026-001 big paper", expected: "DC-GOAL-P0-2026-001" },
|
|
{ id: "x", body: "doc-no: DC-GOAL-P0-2026-002\n\nBody stays unchanged.", expected: "DC-GOAL-P0-2026-002" },
|
|
{ id: "x", tags: ["doc-no:DC-DCSN-P0-2026-001"], expected: "DC-DCSN-P0-2026-001" },
|
|
{ id: "dc_goal_big_paper_submission", expected: "DC-GOAL-P0-2026-001" },
|
|
{ id: "dc_goal_small_paper_submission_gate", expected: "DC-GOAL-P0-2026-002" },
|
|
];
|
|
for (const item of legacyCases) {
|
|
const extracted = extractDocumentNumberFromLegacy(item);
|
|
assertCondition(extracted?.docNo === item.expected, "legacy document number extraction failed", { item, extracted });
|
|
}
|
|
|
|
assertCondition(service.includes("CREATE UNIQUE INDEX IF NOT EXISTS idx_decision_center_records_doc_no_unique"), "service must enforce docNo uniqueness");
|
|
assertCondition(service.includes("doc_no = 'DC-' || doc_type || '-' || doc_priority || '-' || doc_year::text || '-' || lpad(doc_seq::text, 3, '0')"), "schema must keep docNo and parsed components consistent");
|
|
assertCondition(service.includes("clearInvalidDocumentNumbers") && service.indexOf("await clearInvalidDocumentNumbers();") < service.indexOf("ADD CONSTRAINT decision_center_records_doc_shape_check"), "schema migration must clear invalid doc fields before adding the document shape check");
|
|
assertCondition(service.includes("document number already exists") && service.includes("code: \"doc_no_conflict\""), "service must expose structured duplicate docNo errors");
|
|
assertCondition(service.includes("nextDocumentSequence") && service.includes("LOCK TABLE decision_center_records IN SHARE ROW EXCLUSIVE MODE"), "service must allocate next doc sequence under table lock");
|
|
assertCondition(service.includes("getRecordByIdOrDocNo(id, docNo)") && service.includes("doc_no = ${docNo || null}"), "requirement upsert must resolve existing records by id or docNo");
|
|
assertCondition(service.includes("clearDuplicateDocumentNumbers") && service.indexOf("await backfillLegacyDocumentNumbers();") < service.indexOf("CREATE UNIQUE INDEX IF NOT EXISTS idx_decision_center_records_doc_no_unique"), "schema migration must de-duplicate/backfill before creating docNo unique index");
|
|
assertCondition(service.includes("backfillLegacyDocumentNumbers") && service.includes("updated_at = updated_at"), "service must run idempotent legacy backfill without touching body/title/tags");
|
|
assertCondition(service.includes("const documentFields = [\"docNo\"") && service.includes("delete inheritedBase[field]"), "meeting import must persist its own document fields without cloning them to child decisions");
|
|
assertCondition(service.includes("WHERE id = ${id}") && service.includes("doc_no = ${docNo || null}"), "service get route must support id or docNo lookup");
|
|
assertCondition(service.includes("ORDER BY") && service.includes("doc_seq ASC NULLS LAST"), "service list route must sort by document number components");
|
|
assertCondition(cli.includes("--doc-no") && cli.includes("--doc-type") && cli.includes("--doc-priority") && cli.includes("--issued-at"), "CLI must expose document options");
|
|
assertCondition(cliEntry.includes("const result = await runDecisionCenterCommand(config, args.slice(1))") && cliEntry.includes("if (!ok) process.exitCode = 1"), "local CLI must propagate decision ok:false as process failure");
|
|
assertCondition(remote.includes("const result = await runDecisionCenterCommandAsync(config, args.slice(1), fetcher)") && remote.includes("return ok ? 0 : 1"), "remote CLI must propagate decision ok:false as process failure");
|
|
|
|
const cliChecks = await assertCliContract();
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
"doc-number-parse-and-build",
|
|
"doc-no-unique-structured-error-contract",
|
|
"docNo-component-consistency-check",
|
|
"invalid-docNo-migration-guard",
|
|
"doc-sequence-auto-allocation-contract",
|
|
"upsert-by-docNo-contract",
|
|
"duplicate-docNo-migration-guard",
|
|
"legacy-doc-no-title-tag-body-idempotent-backfill",
|
|
"meeting-import-document-field-contract",
|
|
"service-docNo-and-component-query-contract",
|
|
"service-docNo-sort-contract",
|
|
"cli-structured-error-exit-contract",
|
|
...cliChecks,
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(await runDecisionCenterDocumentContract(), null, 2)}\n`);
|
|
}
|