diff --git a/scripts/lib/lastcode-action-kit.test.ts b/scripts/lib/lastcode-action-kit.test.ts new file mode 100644 index 000000000000..e1198b319297 --- /dev/null +++ b/scripts/lib/lastcode-action-kit.test.ts @@ -0,0 +1,59 @@ +// @effect-diagnostics nodeBuiltinImport:off -- Repository declaration coverage reads local source files. +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; + +import { describe, expect, it } from "vite-plus/test"; + +import { createLastCodeActionReporter } from "./lastcode-action-kit.ts"; + +type ProjectScript = { + readonly id?: string; + readonly name: string; + readonly command: string; +}; + +describe("LastCode Action kit", () => { + it("allows one terminal result and rejects a second", () => { + const output: string[] = []; + const action = createLastCodeActionReporter({ + env: {}, + write: () => undefined, + log: (message) => output.push(message), + }); + + action.progress({ state: "working", summary: "Running checks" }); + action.result({ outcome: "success", summary: "Checks passed" }); + + expect(output).toHaveLength(2); + expect(output[1]).toContain('"summary":"Checks passed"'); + expect(() => + action.result({ outcome: "attention", summary: "Unexpected second result" }), + ).toThrow("only one terminal result"); + expect(output).toHaveLength(2); + }); + + it("keeps every repository-owned resumable Action on the shared reporting path", () => { + const repoRoot = NodePath.resolve(import.meta.dirname, "../.."); + const project = JSON.parse(NodeFS.readFileSync(NodePath.join(repoRoot, "t3.json"), "utf8")) as { + readonly scripts: ReadonlyArray; + }; + const actions = project.scripts.filter(({ id }) => id?.startsWith("lc-")); + + expect(actions.length).toBeGreaterThan(0); + for (const action of actions) { + const scriptPath = /\bnode (scripts\/[^\s]+\.(?:ts|mjs))\b/u.exec(action.command)?.[1]; + expect(scriptPath, `${action.name} must run a repository-owned script`).toBeDefined(); + + const source = NodeFS.readFileSync(NodePath.join(repoRoot, scriptPath!), "utf8"); + expect(source, `${action.name} must import the LastCode Action kit`).toContain( + 'from "./lib/lastcode-action-kit.ts"', + ); + expect(source, `${action.name} must report coarse progress`).toContain( + "lastCodeAction.progress", + ); + expect(source, `${action.name} must report a compact terminal result`).toContain( + "lastCodeAction.result", + ); + } + }); +}); diff --git a/scripts/lib/lastcode-action-kit.ts b/scripts/lib/lastcode-action-kit.ts index 6604ec28269d..a7b391fd879c 100644 --- a/scripts/lib/lastcode-action-kit.ts +++ b/scripts/lib/lastcode-action-kit.ts @@ -1,10 +1,28 @@ import { createActionReporter } from "@t3tools/shared/actionResumeProtocol"; +type ReporterOptions = Parameters[0]; + /** * Structured reporting for LastCode's repository-owned resumable Project Actions. * Outside a resumable run, reports remain readable ordinary terminal output. */ -export const lastCodeAction = createActionReporter({ +export function createLastCodeActionReporter(options: ReporterOptions) { + const reporter = createActionReporter(options); + let terminalResultEmitted = false; + + return { + progress: reporter.progress, + result(report: Parameters[0]): void { + if (terminalResultEmitted) { + throw new Error("LastCode Actions may emit only one terminal result."); + } + terminalResultEmitted = true; + reporter.result(report); + }, + }; +} + +export const lastCodeAction = createLastCodeActionReporter({ env: process.env, write: (data) => process.stdout.write(data), log: (message) => process.stdout.write(`${message}\n`),