Skip to content

Commit f11ea19

Browse files
dmealingclaude
andcommitted
feat(cli): meta prompt-snapshot --check drift gate [FR-004]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a187cbe commit f11ea19

2 files changed

Lines changed: 80 additions & 5 deletions

File tree

server/typescript/packages/cli/src/commands/prompt-snapshot.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
1212
import { parsePromptSnapshotArgs } from "../lib/args.js";
1313
import { log } from "../lib/log.js";
1414
import { FileProvider } from "../lib/file-provider.js";
15-
import { snapshotPaths } from "../lib/snapshot.js";
15+
import { snapshotPaths, unifiedDiff } from "../lib/snapshot.js";
1616
import { loadMemory } from "@metaobjectsdev/sdk";
1717
import { TYPE_TEMPLATE, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_FORMAT } from "@metaobjectsdev/metadata";
1818
import { render, type RenderFormat } from "@metaobjectsdev/render";
@@ -51,7 +51,9 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
5151
}
5252

5353
let errorCount = 0;
54+
let driftCount = 0;
5455
let wrote = 0;
56+
let checked = 0;
5557
let skipped = 0;
5658

5759
for (const tmpl of templates) {
@@ -87,10 +89,40 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
8789
continue;
8890
}
8991

90-
mkdirSync(dir, { recursive: true });
91-
writeFileSync(snapPath, rendered, "utf8");
92-
log.info(`[${tmpl.name}] wrote ${snapPath}`);
93-
wrote++;
92+
if (flags.check) {
93+
checked++;
94+
if (!existsSync(snapPath)) {
95+
log.error(
96+
`[${tmpl.name}] no committed snapshot at ${snapPath}; run 'meta prompt-snapshot' to create it`,
97+
);
98+
driftCount++;
99+
continue;
100+
}
101+
const golden = readFileSync(snapPath, "utf8");
102+
if (golden !== rendered) {
103+
log.error(`[${tmpl.name}] snapshot drift:\n${unifiedDiff(golden, rendered)}`);
104+
log.error(`[${tmpl.name}] run 'meta prompt-snapshot' to accept the change`);
105+
driftCount++;
106+
}
107+
} else {
108+
mkdirSync(dir, { recursive: true });
109+
writeFileSync(snapPath, rendered, "utf8");
110+
log.info(`[${tmpl.name}] wrote ${snapPath}`);
111+
wrote++;
112+
}
113+
}
114+
115+
if (flags.check) {
116+
if (errorCount > 0 || driftCount > 0) {
117+
log.error(
118+
`meta prompt-snapshot --check — ${driftCount} drifted, ${errorCount} error(s) across ${checked} checked.`,
119+
);
120+
return 1;
121+
}
122+
log.info(
123+
`meta prompt-snapshot --check — ${checked} snapshot(s) clean${skipped > 0 ? `, ${skipped} skipped` : ""}.`,
124+
);
125+
return 0;
94126
}
95127

96128
if (errorCount > 0) {

server/typescript/packages/cli/test/integration/prompt-snapshot.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,46 @@ describe("meta prompt-snapshot (write mode)", () => {
117117
}
118118
});
119119
});
120+
121+
describe("meta prompt-snapshot --check", () => {
122+
test("passes clean immediately after a write", async () => {
123+
const tmp = scaffold();
124+
writePayload(tmp, "greeting", { name: "Ada" });
125+
try {
126+
expect(await run(["prompt-snapshot", "--cwd", tmp])).toBe(0); // write golden
127+
expect(await run(["prompt-snapshot", "--check", "--cwd", tmp])).toBe(0);
128+
} finally {
129+
rmSync(tmp, { recursive: true, force: true });
130+
}
131+
});
132+
133+
test("exit 1 + reports drift when a shared partial changed", async () => {
134+
const tmp = scaffold();
135+
writePayload(tmp, "greeting", { name: "Ada" });
136+
try {
137+
await run(["prompt-snapshot", "--cwd", tmp]); // write golden
138+
// Edit the shared partial — greeting.mustache itself is untouched, yet the
139+
// rendered prompt changes. This is the case the template's git history misses.
140+
writeFileSync(join(tmp, "prompts", "shared", "preamble.mustache"), "You are a WISE guide.\n", "utf8");
141+
expect(await run(["prompt-snapshot", "--check", "--cwd", tmp])).toBe(1);
142+
const all = [...out, ...err].join("\n");
143+
expect(all).toContain("greeting");
144+
expect(all).toMatch(/drift/i);
145+
// --check must NOT rewrite the golden.
146+
expect(readFileSync(snapPath(tmp, "greeting"), "utf8")).toBe("You are a helpful guide.\nHi Ada.");
147+
} finally {
148+
rmSync(tmp, { recursive: true, force: true });
149+
}
150+
});
151+
152+
test("exit 1 when a payload exists but no golden is committed", async () => {
153+
const tmp = scaffold();
154+
writePayload(tmp, "greeting", { name: "Ada" }); // payload but no output.snap written
155+
try {
156+
expect(await run(["prompt-snapshot", "--check", "--cwd", tmp])).toBe(1);
157+
expect([...out, ...err].join("\n")).toContain("greeting");
158+
} finally {
159+
rmSync(tmp, { recursive: true, force: true });
160+
}
161+
});
162+
});

0 commit comments

Comments
 (0)