Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 25s
u1 argparse --reuse-from PREV_RUN_ID + post-merge fail-closed guard (rejects
layout/zone_geometry/zone_section/image override axes by name; only
--override-frame is preserved).
u2 src/phase_z2_reuse_snapshot.py — JSON-only Step 6 snapshot with mdx_sha256
integrity key and {value, source_path, upstream_step} provenance per axis
(pickle forbidden per Stage 2 guardrail).
u3 _write_reuse_snapshot at the Step 6 boundary; soft-fails to stderr without
aborting the seed run.
u4 prev_run_dir RO copy of step00/01/02/05/06 + _reuse_snapshot.json into
new run_dir, state rehydration, reuse marker, frame-override application on
restored units, Step 7+ resume.
u4b fail-closed for missing prev_run_dir / missing/corrupt/invalid snapshot /
mdx_sha256 mismatch / accidental new==prev write, with value+path+upstream
diagnostics per axis.
u5 reuse_from Optional[str] threaded through run_phase_z2_mvp1 signature and
CLI dispatch; default None preserves byte-identical pre-IMP-43 behavior.
u6 Front /api/run optional reuseFromRunId forwarding (vite.config.ts +
designAgentApi.ts + run_pipeline_reuse_from.test.ts).
u7a fast CI equivalence (1 mdx × 1 layout × 2 frames); step13 whitelist =
run_id/timestamps/prev_run_id only. u7b 3 layouts × 3 mdx × 32 frames
sweep gated by pytest.mark.sweep (registered in pyproject.toml; default CI
must use -m 'not sweep').
u8 scripts/measure_reuse_savings.py argv-driven A/B/C harness with frame
pin self-discovery + seed-time exclusion; status board §8 TBD anchor
(issue-body 50-70% / 10-20s→3-8s claim explicitly unverified, not mirrored).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
251 lines
11 KiB
TypeScript
251 lines
11 KiB
TypeScript
// IMP-43 (#72) u6 — /api/run reuseFromRunId forwarding coverage.
|
|
//
|
|
// Stage 2 unit scope:
|
|
// 1) Front/client/src/services/designAgentApi.ts `runPipeline`:
|
|
// • accepts an optional 3rd arg `reuseFromRunId: string`.
|
|
// • includes `reuseFromRunId` in the POST body when truthy.
|
|
// • OMITS `reuseFromRunId` from the body when absent / empty / undefined
|
|
// → byte-identical to the pre-u6 POST contract (absent flag = full
|
|
// pipeline; backend u1 guard never sees an empty PREV_RUN_ID).
|
|
// • leaves `filename`, `content`, and `overrides` untouched alongside
|
|
// the new field (no payload-shape regression).
|
|
// 2) Front/vite.config.ts `/api/run` handler:
|
|
// • declares `reuseFromRunId?: string` in the payload type so a typed
|
|
// client cannot send a payload the server silently drops.
|
|
// • destructures `reuseFromRunId` from `payload` (sibling of
|
|
// `overrides`, NOT nested under it — the backend u1 post-merge
|
|
// guard treats reuse as a pipeline mode, not an override).
|
|
// • forwards `--reuse-from <PREV_RUN_ID>` to spawn cliArgs guarded by
|
|
// a truthy check (empty string / undefined ⇒ no flag, per Stage 2
|
|
// contract: invalid CLI args must never reach argparse).
|
|
// • places the forward block AFTER the `--override-section-assignment`
|
|
// loop so the spawn argv preserves backend argparse's no-positional-
|
|
// before-flag expectation and so `--override-frame` (still allowed
|
|
// by the u1 guard) is positioned ahead of `--reuse-from`.
|
|
//
|
|
// runPipeline is exercised with a duck-typed `File` plus a `vi.stubGlobal`
|
|
// fetch mock — mirrors the user_overrides_service.test.ts pattern. The
|
|
// vite handler is source-sliced (mirrors handle_generate_diag.test.ts)
|
|
// because the handler spawns python and a real /api/run round-trip is
|
|
// out of unit-test scope.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { runPipeline } from "../src/services/designAgentApi";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// vite.config.ts source — read once for the handler source-slice assertions.
|
|
// Path: Front/client/tests/ → Front/vite.config.ts (two levels up).
|
|
// ---------------------------------------------------------------------------
|
|
const VITE_CONFIG_PATH = resolve(__dirname, "..", "..", "vite.config.ts");
|
|
const VITE_CONFIG_SOURCE = readFileSync(VITE_CONFIG_PATH, "utf-8");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// fetch mock — minimal Response stub mirroring runPipeline's `.ok` + `.json()`
|
|
// + `.status` surface. Same shape as the user_overrides_service.test.ts
|
|
// helper so the two test files stay drift-free.
|
|
// ---------------------------------------------------------------------------
|
|
type MockResponse = {
|
|
ok: boolean;
|
|
status: number;
|
|
json: () => Promise<unknown>;
|
|
};
|
|
|
|
function mockResponse(body: unknown, ok = true, status = 200): MockResponse {
|
|
return { ok, status, json: async () => body };
|
|
}
|
|
|
|
const SUCCESS_BODY = {
|
|
success: true,
|
|
run_id: "test_run_id_20260524",
|
|
exit_code: 0,
|
|
final_html_exists: true,
|
|
preview_exists: true,
|
|
stdout: "",
|
|
stderr: "",
|
|
};
|
|
|
|
// Duck-typed File — runPipeline reads only `.name` and `.text()`. Avoids a
|
|
// hard dependency on the global File constructor (varies across node /
|
|
// jsdom / happy-dom test environments).
|
|
function makeFakeFile(name: string, content: string): File {
|
|
return {
|
|
name,
|
|
text: async () => content,
|
|
} as unknown as File;
|
|
}
|
|
|
|
let fetchMock: Mock;
|
|
|
|
beforeEach(() => {
|
|
fetchMock = vi.fn();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
function lastPostBody(): Record<string, unknown> {
|
|
const lastCall = fetchMock.mock.calls.at(-1);
|
|
if (!lastCall) throw new Error("fetch was not called");
|
|
const init = lastCall[1] as RequestInit | undefined;
|
|
if (!init?.body) throw new Error("fetch was called without a body");
|
|
return JSON.parse(String(init.body));
|
|
}
|
|
|
|
// ============================================================================
|
|
// runPipeline (designAgentApi.ts) — forwarding/omission coverage
|
|
// ============================================================================
|
|
|
|
describe("runPipeline reuseFromRunId forwarding (IMP-43 #72 u6)", () => {
|
|
it("posts to /api/run via POST with JSON content-type", async () => {
|
|
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
|
await runPipeline(makeFakeFile("03.mdx", "# title"));
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchMock.mock.calls[0];
|
|
expect(url).toBe("/api/run");
|
|
expect((init as RequestInit).method).toBe("POST");
|
|
expect((init as RequestInit).headers).toMatchObject({
|
|
"Content-Type": "application/json",
|
|
});
|
|
});
|
|
|
|
it("includes reuseFromRunId in the POST body when provided", async () => {
|
|
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
|
await runPipeline(
|
|
makeFakeFile("03.mdx", "# title"),
|
|
undefined,
|
|
"mdx03_20260524080000",
|
|
);
|
|
const body = lastPostBody();
|
|
expect(body.reuseFromRunId).toBe("mdx03_20260524080000");
|
|
expect(body.filename).toBe("03.mdx");
|
|
expect(body.content).toBe("# title");
|
|
});
|
|
|
|
it("omits reuseFromRunId when 3rd arg is undefined (pre-u6 byte-identical)", async () => {
|
|
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
|
await runPipeline(makeFakeFile("03.mdx", "# title"));
|
|
const body = lastPostBody();
|
|
expect("reuseFromRunId" in body).toBe(false);
|
|
// Pre-u6 contract: filename/content are the only keys when overrides
|
|
// is undefined (JSON.stringify drops undefined values; pre-u6 emitted
|
|
// `JSON.stringify({filename, content, overrides})` with the same
|
|
// drop-undefined behaviour, so the wire body is byte-identical).
|
|
expect(Object.keys(body).sort()).toEqual(["content", "filename"]);
|
|
});
|
|
|
|
it("omits reuseFromRunId but keeps overrides when only overrides provided", async () => {
|
|
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
|
await runPipeline(makeFakeFile("03.mdx", "# title"), {
|
|
frames: { "03-1": "frame_07" },
|
|
});
|
|
const body = lastPostBody();
|
|
expect("reuseFromRunId" in body).toBe(false);
|
|
expect(Object.keys(body).sort()).toEqual([
|
|
"content",
|
|
"filename",
|
|
"overrides",
|
|
]);
|
|
expect(body.overrides).toEqual({ frames: { "03-1": "frame_07" } });
|
|
});
|
|
|
|
it("omits reuseFromRunId when passed an empty string (truthy guard)", async () => {
|
|
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
|
await runPipeline(makeFakeFile("03.mdx", "# title"), undefined, "");
|
|
const body = lastPostBody();
|
|
expect("reuseFromRunId" in body).toBe(false);
|
|
});
|
|
|
|
it("forwards reuseFromRunId alongside frame overrides (the only u1-permitted combo)", async () => {
|
|
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
|
await runPipeline(
|
|
makeFakeFile("03.mdx", "# title"),
|
|
{ frames: { "03-1+03-2": "frame_07" } },
|
|
"mdx03_20260524080000",
|
|
);
|
|
const body = lastPostBody();
|
|
expect(body.overrides).toEqual({ frames: { "03-1+03-2": "frame_07" } });
|
|
expect(body.reuseFromRunId).toBe("mdx03_20260524080000");
|
|
});
|
|
|
|
it("returns the parsed RunPipelineResult on success", async () => {
|
|
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
|
const res = await runPipeline(
|
|
makeFakeFile("03.mdx", "# title"),
|
|
undefined,
|
|
"mdx03_20260524080000",
|
|
);
|
|
expect(res.success).toBe(true);
|
|
expect(res.run_id).toBe("test_run_id_20260524");
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// /api/run handler (vite.config.ts) — source-slice forwarding contract
|
|
// ============================================================================
|
|
|
|
describe("/api/run handler reuseFromRunId source-slice (IMP-43 #72 u6)", () => {
|
|
it("declares reuseFromRunId?: string on the /api/run payload type", () => {
|
|
// Payload type at the top of the /api/run handler body. The
|
|
// optional-string declaration is the single source-of-truth for what
|
|
// shape the handler accepts; a typed frontend client (u5 saveUserOverrides
|
|
// sibling pattern) cannot silently send a payload the server drops.
|
|
expect(VITE_CONFIG_SOURCE).toMatch(/reuseFromRunId\?:\s*string\s*;/);
|
|
});
|
|
|
|
it("destructures reuseFromRunId from payload alongside filename/content/overrides", () => {
|
|
expect(VITE_CONFIG_SOURCE).toMatch(
|
|
/const\s*\{\s*filename\s*,\s*content\s*,\s*overrides\s*,\s*reuseFromRunId\s*\}\s*=\s*payload\s*;/,
|
|
);
|
|
});
|
|
|
|
it("forwards --reuse-from <PREV_RUN_ID> after the override-section-assignment loop", () => {
|
|
// Stage 2 contract: reuse_from is a pipeline mode, not an override.
|
|
// The forward block must sit AFTER the last override loop so the spawn
|
|
// argv preserves the order documented in the u1 backend post-merge
|
|
// guard (overrides parsed first; reuse_from precondition runs against
|
|
// the merged overrides view).
|
|
const reuseFromIdx = VITE_CONFIG_SOURCE.indexOf('"--reuse-from"');
|
|
const zoneSectionsIdx = VITE_CONFIG_SOURCE.indexOf(
|
|
'"--override-section-assignment"',
|
|
);
|
|
expect(reuseFromIdx).toBeGreaterThan(-1);
|
|
expect(zoneSectionsIdx).toBeGreaterThan(-1);
|
|
expect(reuseFromIdx).toBeGreaterThan(zoneSectionsIdx);
|
|
});
|
|
|
|
it("guards the forward with a truthy check on reuseFromRunId", () => {
|
|
// Empty string / undefined ⇒ no flag pushed (Stage 2 contract: invalid
|
|
// CLI args must never reach argparse — the backend u1 guard would
|
|
// fail-closed with `reuse_artifact_missing` on the empty PREV_RUN_ID).
|
|
const reuseFromIdx = VITE_CONFIG_SOURCE.indexOf('"--reuse-from"');
|
|
expect(reuseFromIdx).toBeGreaterThan(-1);
|
|
const preface = VITE_CONFIG_SOURCE.slice(
|
|
Math.max(0, reuseFromIdx - 200),
|
|
reuseFromIdx,
|
|
);
|
|
expect(preface).toMatch(/if\s*\(\s*reuseFromRunId/);
|
|
expect(preface).toMatch(/typeof\s+reuseFromRunId\s*===\s*"string"/);
|
|
});
|
|
|
|
it("pushes reuseFromRunId as the --reuse-from argument value (no string interpolation)", () => {
|
|
// The CLI value must be the raw PREV_RUN_ID — no `=` join, no quoting
|
|
// (spawn is shell:false). Mirrors the `--override-layout` shape.
|
|
const reuseFromIdx = VITE_CONFIG_SOURCE.indexOf('"--reuse-from"');
|
|
expect(reuseFromIdx).toBeGreaterThan(-1);
|
|
// Window spans both before (`cliArgs.push(`) and after
|
|
// (`reuseFromRunId)`) the literal so the full push expression is
|
|
// captured.
|
|
const window = VITE_CONFIG_SOURCE.slice(
|
|
Math.max(0, reuseFromIdx - 100),
|
|
reuseFromIdx + 200,
|
|
);
|
|
expect(window).toMatch(
|
|
/cliArgs\.push\(\s*"--reuse-from"\s*,\s*reuseFromRunId\s*\)/,
|
|
);
|
|
});
|
|
});
|