fc3a095858
Keep diary summary as an independent Decision Center field when body content is supplied from --body-file, and cover the CLI/backend contract.
137 lines
5.3 KiB
TypeScript
137 lines
5.3 KiB
TypeScript
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<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 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<unknown> => {
|
|
calls.push({ path, init });
|
|
return { ok: true, status: 200, body: { ok: true, action: "updated", entry: { id: "diary_issue_191" } } };
|
|
};
|
|
}
|
|
|
|
async function assertCliPayloadContract(): Promise<string[]> {
|
|
const checks: string[] = [];
|
|
const config = {} as Parameters<typeof runDecisionCenterCommandAsync>[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<JsonRecord> {
|
|
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`);
|
|
}
|