import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { runDecisionCenterCommandAsync } from "./src/decision-center"; type JsonRecord = Record; 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 includesAll(text: string, snippets: string[]): boolean { return snippets.every((snippet) => text.includes(snippet)); } function makeFetcher(calls: FetchCall[]) { return async (path: string, init?: { method?: string; body?: unknown }): Promise => { calls.push({ path, init }); return { ok: true, status: 200, body: { ok: true, action: "updated", entry: { id: "diary_issue_191" } } }; }; } async function assertCliPayloadContract(): Promise { const checks: string[] = []; const config = {} as Parameters[0]; const tempDir = mkdtempSync(join(tmpdir(), "unidesk-diary-summary-")); try { const bodyFile = join(tempDir, "body.md"); const bodyText = "# 2099-12-31\n\n## 进展\n- body from file\n"; writeFileSync(bodyFile, bodyText, "utf8"); { const calls: FetchCall[] = []; const result = asRecord(await runDecisionCenterCommandAsync(config, [ "diary", "upsert", "2099-12-31", "--title", "Issue 191 Repro", "--summary", "explicit summary stays separate", "--body-file", bodyFile, "--source-file", "issue-191-contract", ], makeFetcher(calls))); const call = calls[0]; const body = asRecord(call?.init?.body); const bodySource = asRecord(result.bodySource); assertCondition(call?.path === "/api/microservices/decision-center/proxy/api/diary/entries/2099-12-31", "diary upsert must address date route", { call }); assertCondition(call?.init?.method === "PUT", "diary upsert must use PUT", { call }); assertCondition(body.summary === "explicit summary stays separate", "CLI must send explicit summary as its own payload field", { body }); assertCondition(body.body === bodyText, "CLI must keep body populated from --body-file", { body }); assertCondition(body.sourceFile === "issue-191-contract", "CLI must keep sourceFile disambiguation", { body }); assertCondition(bodySource.kind === "file" && bodySource.path === bodyFile, "CLI must disclose file body source", { bodySource }); checks.push("cli-diary-upsert-summary-plus-body-file-payload"); } { const calls: FetchCall[] = []; const result = asRecord(await runDecisionCenterCommandAsync(config, [ "diary", "upsert", "2099-12-31", "--summary", "summary only update", ], makeFetcher(calls))); const body = asRecord(calls[0]?.init?.body); const bodySource = asRecord(result.bodySource); assertCondition(body.summary === "summary only update", "CLI must support summary-only diary updates", { body }); assertCondition(!("body" in body), "summary-only updates must not synthesize body from summary", { body }); assertCondition(bodySource.kind === "none", "summary-only updates must disclose no body source", { bodySource }); checks.push("cli-diary-upsert-summary-only-payload"); } } finally { rmSync(tempDir, { recursive: true, force: true }); } return checks; } export async function runDecisionCenterDiarySummaryContract(): Promise { const service = source("src/components/microservices/decision-center/src/index.ts"); const cli = source("scripts/src/decision-center.ts"); assertCondition( includesAll(cli, [ "function optionalBodyFromArgs(args: string[], command: string)", "const summary = optionValue(args, [\"--summary\"])", "if (summary !== undefined) payload.summary = summary", "bodySource: { kind: \"none\" }", ]), "CLI must route --summary independently from body input", ); assertCondition( includesAll(service, [ "summary TEXT NOT NULL DEFAULT ''", "ALTER TABLE decision_center_diary_entries ADD COLUMN IF NOT EXISTS summary TEXT NOT NULL DEFAULT ''", "summary: row.summary || summaryFromBody(body)", "function normalizeDiarySummary(value: unknown, body: string): string", "const summaryProvided = \"summary\" in input", "normalizeDiarySummary(input.summary, body)", "id, entry_date, month, title, summary, body, source_file", "summary = ${summary}", "OR summary IS DISTINCT FROM ${summary}", ]), "Decision Center backend must persist explicit diary summary separately from body", ); const cliChecks = await assertCliPayloadContract(); return { ok: true, checks: [ "cli-summary-independent-field-contract", "backend-diary-summary-column-contract", ...cliChecks, ], }; } if (import.meta.main) { process.stdout.write(`${JSON.stringify(await runDecisionCenterDiarySummaryContract(), null, 2)}\n`); }