feat(#93): IMP-55 u1~u12 frontend manual section swap detection (manual_section_assignment bool axis + drag-only marker gate + dual-axis persistence + backend manual-true gate)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 9s
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 9s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -310,16 +310,44 @@ describe("KNOWN_USER_OVERRIDES_AXES (IMP-52 u4)", () => {
|
||||
// The on-disk schema is shared with backend pipeline fallback (u2).
|
||||
// Any drift here means a PUT could write an axis that the Python
|
||||
// load() ignores, or vice-versa, silently losing user overrides.
|
||||
// Mirror is Python-minus-`slide_css` (known IMP-45 #74 gap — the
|
||||
// frontend never writes slide_css). IMP-55 #93 u1 adds the bool
|
||||
// `manual_section_assignment` axis as a first-class allowlist entry.
|
||||
expect(KNOWN_USER_OVERRIDES_AXES).toEqual([
|
||||
"layout",
|
||||
"zone_geometries",
|
||||
"zone_sections",
|
||||
"frames",
|
||||
"image_overrides",
|
||||
"manual_section_assignment",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeUserOverrides (IMP-55 #93 u1) — manual_section_assignment bool axis", () => {
|
||||
it("merges bool true / false literally and clears on null", () => {
|
||||
// The PUT handler must treat the bool axis like any other allowlisted
|
||||
// axis: replace on write, preserve when absent, delete on null. Tests
|
||||
// both true→false flip and explicit null-clear so the backend (u9)
|
||||
// sees the exact frontend intent.
|
||||
let merged = mergeUserOverrides({}, { manual_section_assignment: true });
|
||||
expect(merged.manual_section_assignment).toBe(true);
|
||||
|
||||
merged = mergeUserOverrides(merged, { manual_section_assignment: false });
|
||||
expect(merged.manual_section_assignment).toBe(false);
|
||||
|
||||
merged = mergeUserOverrides(merged, { manual_section_assignment: null });
|
||||
expect("manual_section_assignment" in merged).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves bool axis when partial touches only a sibling axis", () => {
|
||||
const existing = { manual_section_assignment: true, layout: "old" };
|
||||
const merged = mergeUserOverrides(existing, { layout: "new" });
|
||||
expect(merged.manual_section_assignment).toBe(true);
|
||||
expect(merged.layout).toBe("new");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeUserOverrides (IMP-52 u4)", () => {
|
||||
it("only mutates KNOWN_AXES present in partial", () => {
|
||||
const existing = {
|
||||
|
||||
@@ -54,6 +54,11 @@ function makeSelection(overrides?: Partial<UserSelection["overrides"]>): UserSel
|
||||
// axis declared on `UserSelection.overrides`. Empty by default so the
|
||||
// existing IMP-52 cases remain unchanged in shape.
|
||||
image_overrides: {},
|
||||
// IMP-55 (#93) u3 — bool intent marker is REQUIRED on
|
||||
// `UserSelection.overrides` (not optional). Default to `false` so every
|
||||
// pre-existing fixture matches the `createInitialUserSelection` seed
|
||||
// and stays compile-clean after u3 widened the type.
|
||||
manual_section_assignment: false,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
@@ -460,3 +465,88 @@ describe("image_overrides axis — saveImageOverride (IMP-51 u11)", () => {
|
||||
expect(sel.overrides.image_overrides).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── IMP-55 (#93) u3 — manual_section_assignment bool axis ──────────────────
|
||||
// Restore-on-reopen / seed coverage for the bool intent marker. Production
|
||||
// branch lives at `slidePlanUtils.ts` — `applyPersistedNonFrameOverrides`
|
||||
// guards with `typeof persisted.manual_section_assignment === "boolean"`,
|
||||
// and `createInitialUserSelection` seeds the axis to `false`. The marker
|
||||
// gates whether `handleGenerate` (u7) forwards `overrides.zoneSections`
|
||||
// to the backend; the pipeline (u9) consumes persisted `zone_sections`
|
||||
// only when the marker is exactly `true`, so any non-boolean payload MUST
|
||||
// end up `false` in memory (fail-closed).
|
||||
|
||||
describe("manual_section_assignment axis — applyPersistedNonFrameOverrides (IMP-55 #93 u3)", () => {
|
||||
it("restores literal true verbatim", () => {
|
||||
const sel = makeSelection();
|
||||
const next = applyPersistedNonFrameOverrides(sel, {
|
||||
manual_section_assignment: true,
|
||||
});
|
||||
expect(next.overrides.manual_section_assignment).toBe(true);
|
||||
});
|
||||
|
||||
it("restores literal false verbatim (u12 apply/cancel write must survive reopen)", () => {
|
||||
// Seed `true` so the assertion proves `false` overwrites; a truthiness
|
||||
// check instead of `typeof === \"boolean\"` would silently keep `true`
|
||||
// and resurrect stale auto-carry assignments as user intent.
|
||||
const sel = makeSelection({ manual_section_assignment: true });
|
||||
const next = applyPersistedNonFrameOverrides(sel, {
|
||||
manual_section_assignment: false,
|
||||
});
|
||||
expect(next.overrides.manual_section_assignment).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves the in-memory marker unchanged when the persisted axis is absent", () => {
|
||||
const sel = makeSelection({ manual_section_assignment: true });
|
||||
const next = applyPersistedNonFrameOverrides(sel, { layout: "horizontal-2" });
|
||||
expect(next.overrides.manual_section_assignment).toBe(true);
|
||||
expect(next.overrides.layout_preset).toBe("horizontal-2");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["null clear sentinel", null],
|
||||
['string "true"', "true"],
|
||||
['string "false"', "false"],
|
||||
["number 1", 1],
|
||||
["number 0", 0],
|
||||
["object {}", {}],
|
||||
["array []", []],
|
||||
])("ignores non-boolean payload (%s) — keeps prior in-memory value", (_label, payload) => {
|
||||
const sel = makeSelection({ manual_section_assignment: true });
|
||||
const next = applyPersistedNonFrameOverrides(sel, {
|
||||
manual_section_assignment: payload as unknown as boolean,
|
||||
});
|
||||
expect(next.overrides.manual_section_assignment).toBe(true);
|
||||
});
|
||||
|
||||
it("seeds an empty selection with manual_section_assignment=false (createInitialUserSelection)", () => {
|
||||
const sel = createInitialUserSelection();
|
||||
expect(sel.overrides.manual_section_assignment).toBe(false);
|
||||
});
|
||||
|
||||
it("returns a NEW selection object (no input mutation) when restoring the marker", () => {
|
||||
const sel = makeSelection({ manual_section_assignment: false });
|
||||
const next = applyPersistedNonFrameOverrides(sel, {
|
||||
manual_section_assignment: true,
|
||||
});
|
||||
expect(next).not.toBe(sel);
|
||||
expect(next.overrides).not.toBe(sel.overrides);
|
||||
// Input still pristine — proves the helper does not flip the fixture.
|
||||
expect(sel.overrides.manual_section_assignment).toBe(false);
|
||||
});
|
||||
|
||||
it("layers the bool axis alongside other persisted axes in a single call", () => {
|
||||
const sel = makeSelection();
|
||||
const next = applyPersistedNonFrameOverrides(sel, {
|
||||
layout: "vertical-2",
|
||||
zone_sections: { top: ["03-1"], bottom: ["03-2"] },
|
||||
manual_section_assignment: true,
|
||||
});
|
||||
expect(next.overrides.layout_preset).toBe("vertical-2");
|
||||
expect(next.overrides.zone_sections).toEqual({
|
||||
top: ["03-1"],
|
||||
bottom: ["03-2"],
|
||||
});
|
||||
expect(next.overrides.manual_section_assignment).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -559,3 +559,67 @@ describe("saveUserOverrides (IMP-51 #79 u3) — image_overrides axis", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// IMP-55 #93 u1 — manual_section_assignment axis (7th axis) parity coverage
|
||||
//
|
||||
// The bool intent marker rides on the same per-axis coalescing rails as the
|
||||
// 6 sibling axes. These tests lock the typed client behavior so a regression
|
||||
// in the boolean serialization (e.g., coercion to "true" string, dropped
|
||||
// `false` due to truthy filtering) fails here instead of in Home.tsx (u6/u7)
|
||||
// or the backend gate (u9~u11).
|
||||
// ============================================================================
|
||||
|
||||
describe("saveUserOverrides (IMP-55 #93 u1) — manual_section_assignment axis", () => {
|
||||
it("PUT body carries only manual_section_assignment when it is the sole mutated axis", async () => {
|
||||
fetchMock.mockResolvedValue(mockResponse({}));
|
||||
void saveUserOverrides("03", { manual_section_assignment: true });
|
||||
vi.advanceTimersByTime(300);
|
||||
await drainMicrotasks();
|
||||
|
||||
const body = lastPutBody() as Record<string, unknown>;
|
||||
expect(Object.keys(body)).toEqual(["manual_section_assignment"]);
|
||||
expect(body.manual_section_assignment).toBe(true);
|
||||
});
|
||||
|
||||
it("later-wins coalesces true → false within a single debounce window", async () => {
|
||||
// Drag-then-cancel inside 300 ms — server must see only the final
|
||||
// `false`, not a transient `true` that would re-enable backend
|
||||
// consumption of stale zone_sections.
|
||||
fetchMock.mockResolvedValue(mockResponse({}));
|
||||
void saveUserOverrides("03", { manual_section_assignment: true });
|
||||
void saveUserOverrides("03", { manual_section_assignment: false });
|
||||
|
||||
vi.advanceTimersByTime(300);
|
||||
await drainMicrotasks();
|
||||
expect(putCallsCount()).toBe(1);
|
||||
expect(lastPutBody()).toEqual({ manual_section_assignment: false });
|
||||
});
|
||||
|
||||
it("forwards null sentinel verbatim (explicit clear)", async () => {
|
||||
fetchMock.mockResolvedValue(mockResponse({}));
|
||||
void saveUserOverrides("03", { manual_section_assignment: null });
|
||||
|
||||
vi.advanceTimersByTime(300);
|
||||
await drainMicrotasks();
|
||||
expect(lastPutBody()).toEqual({ manual_section_assignment: null });
|
||||
});
|
||||
|
||||
it("coalesces with zone_sections sibling into a single PUT (drag-drop pair)", async () => {
|
||||
// Real-world drag flow (u6): one save() sets the bool + zone_sections
|
||||
// together. Asserts both axes survive coalescing as a single PUT body.
|
||||
fetchMock.mockResolvedValue(mockResponse({}));
|
||||
void saveUserOverrides("03", {
|
||||
zone_sections: { left: ["03-2"], right: ["03-1"] },
|
||||
manual_section_assignment: true,
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(300);
|
||||
await drainMicrotasks();
|
||||
expect(putCallsCount()).toBe(1);
|
||||
expect(lastPutBody()).toEqual({
|
||||
zone_sections: { left: ["03-2"], right: ["03-1"] },
|
||||
manual_section_assignment: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,6 +85,22 @@ function sliceHandler(source: string, name: string): string {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* IMP-55 #93 u8 — strip JS/TS line + block comments so source-pattern
|
||||
* regex checks assert against LIVE code only. The u5 / u7 docblocks in
|
||||
* Home.tsx intentionally reference removed identifiers (e.g. `defaultByZone`,
|
||||
* `sameAsDefault`, `zoneSectionsDiff`) and the marker axis name in prose to
|
||||
* document the Stage 1 root cause for future readers — those references are
|
||||
* documentation, not behavior, and must not trigger negative-match guards.
|
||||
* Strips `// ...` to EOL and `/* ... */` (incl. multi-line) — keeps string
|
||||
* literals intact because we only consume the result for regex-match tests.
|
||||
*/
|
||||
function stripComments(source: string): string {
|
||||
return source
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/\/\/.*$/gm, "");
|
||||
}
|
||||
|
||||
describe("Home.tsx write-side wiring (IMP-52 u10) — source pattern", () => {
|
||||
it("handleSectionDrop persists zone_sections behind uploadedFile gate", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleSectionDrop");
|
||||
@@ -567,3 +583,220 @@ describe("restore-on-reopen end-to-end (IMP-52 u10)", () => {
|
||||
expect(remapPersistedFramesToZoneFrames(plan, persisted.frames)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── IMP-55 #93 u8 — manual_section_assignment intent marker contract ─────
|
||||
// Verifies four axes of the marker contract introduced in u3 (type) / u5
|
||||
// (apply reset) / u6 (drag flip + co-PUT) / u7 (generate gate):
|
||||
// 1) Drag dual-axis persistence — handleSectionDrop persists BOTH
|
||||
// `zone_sections` AND `manual_section_assignment: true` in the SAME
|
||||
// PUT body (co-PUT atomicity — disk never sees post-drop zone_sections
|
||||
// without the marker).
|
||||
// 2) Apply / cancel reset — handleApplyPendingLayout writes explicit
|
||||
// `manual_section_assignment: false` after the `...overrides` spread,
|
||||
// and handleCancelPendingLayout relies on createInitialUserSelection
|
||||
// (which u3 seeds to `false`) to drop a prior `true`.
|
||||
// 3) Marker-gated forwarding — handleGenerate gates `overrides.zoneSections`
|
||||
// forwarding strictly on `manualMarker === true` (NOT truthiness, NOT
|
||||
// `!= null`, NOT presence). u3-seeded `false` and absent values both
|
||||
// skip forwarding.
|
||||
// 4) sameAsDefault NOT required — the Stage 1 anti-pattern (defaultByZone
|
||||
// / sameAsDefault / zoneSectionsDiff self-compare loop) is gone from
|
||||
// `handleGenerate` entirely; the marker is the source of intent.
|
||||
|
||||
describe("IMP-55 #93 u8 — manual_section_assignment marker contract", () => {
|
||||
it("handleSectionDrop sets marker true in-memory before persistence", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleSectionDrop");
|
||||
// finalSelection literal (built from zoneSelected, then marker = true)
|
||||
// must occur BEFORE the saveUserOverrides call so the in-memory state
|
||||
// and the PUT body source from the same overrides shape.
|
||||
const markerIdx = block.search(/manual_section_assignment:\s*true/);
|
||||
const saveIdx = block.search(/saveUserOverrides\(/);
|
||||
expect(markerIdx).toBeGreaterThan(-1);
|
||||
expect(saveIdx).toBeGreaterThan(-1);
|
||||
expect(markerIdx).toBeLessThan(saveIdx);
|
||||
});
|
||||
|
||||
it("handleSectionDrop co-PUTs zone_sections + manual_section_assignment:true (single body)", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleSectionDrop");
|
||||
// Single saveUserOverrides call carrying BOTH axes. The regex spans the
|
||||
// call body to prove the two keys live in the same object literal — a
|
||||
// future split into two PUTs would race the 300ms debounce and re-open
|
||||
// the IMP-55 stale-disk window.
|
||||
expect(block).toMatch(
|
||||
/saveUserOverrides\([\s\S]*?zone_sections:[\s\S]*?manual_section_assignment:\s*true[\s\S]*?\)/,
|
||||
);
|
||||
// Exactly ONE saveUserOverrides call in the handler.
|
||||
const calls = block.match(/saveUserOverrides\(/g) ?? [];
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("handleApplyPendingLayout resets the marker to false in overrides literal", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleApplyPendingLayout");
|
||||
// After spreading `...p.userSelection.overrides`, the explicit
|
||||
// `manual_section_assignment: false` overrides any prior-drag `true`.
|
||||
// Without this the layout flip would carry the marker through, and u7
|
||||
// would forward auto-carried assignments as user overrides → the
|
||||
// PARTIAL_COVERAGE regression that motivated IMP-55.
|
||||
expect(block).toMatch(/\.\.\.p\.userSelection\.overrides[\s\S]*?manual_section_assignment:\s*false/);
|
||||
});
|
||||
|
||||
it("handleCancelPendingLayout uses createInitialUserSelection (u3 seeds false)", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleCancelPendingLayout");
|
||||
// Cancel discards all pending in-memory edits via the fresh-selection
|
||||
// helper — the seed (u3) is the single source of truth for the
|
||||
// in-memory marker on this path. u12 adds a separate disk-side
|
||||
// saveUserOverrides PUT (covered by the u12 describe block below);
|
||||
// the in-memory userSelection literal still has no explicit marker
|
||||
// field — the seed handles it.
|
||||
expect(block).toMatch(/createInitialUserSelection\(p\.slidePlan\)/);
|
||||
// In-memory contract: no `manual_section_assignment` property appears
|
||||
// inside the userSelection assignment. The only marker reference in
|
||||
// live code lives inside the u12 saveUserOverrides(...) call body.
|
||||
const codeOnly = stripComments(block);
|
||||
expect(codeOnly).not.toMatch(
|
||||
/userSelection:[\s\S]*?manual_section_assignment/,
|
||||
);
|
||||
});
|
||||
|
||||
it("handleGenerate gates overrides.zoneSections on manualMarker === true (strict bool)", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleGenerate");
|
||||
// Marker read AND strict-equality gate. `===` not `==`, not truthiness,
|
||||
// not presence — so `false` / absent both skip forwarding (fail-closed).
|
||||
expect(block).toMatch(/state\.userSelection\.overrides\.manual_section_assignment/);
|
||||
expect(block).toMatch(/manualMarker\s*===\s*true/);
|
||||
// The assignment to `overrides.zoneSections` must live INSIDE the
|
||||
// marker-true branch.
|
||||
const gateIdx = block.search(/if\s*\(\s*manualMarker\s*===\s*true\s*\)/);
|
||||
const assignIdx = block.search(/overrides\.zoneSections\s*=/);
|
||||
expect(gateIdx).toBeGreaterThan(-1);
|
||||
expect(assignIdx).toBeGreaterThan(gateIdx);
|
||||
});
|
||||
|
||||
it("handleGenerate filters forwarded zone_sections to valid zone_ids only (cross-layout safety)", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleGenerate");
|
||||
// A stale persisted layout could carry zone_ids that do not exist in
|
||||
// the current sourcePlan (e.g. horizontal-2 `top`/`bottom` while the
|
||||
// current layout is vertical-2 `left`/`right`). Those foreign keys
|
||||
// must be dropped before reaching the backend `--override-section-
|
||||
// assignment` so they cannot trigger PARTIAL_COVERAGE.
|
||||
expect(block).toMatch(/validZoneIds\s*=\s*new Set\(\s*sourcePlan\.zones\.map\(\(z\)\s*=>\s*z\.zone_id\)/);
|
||||
expect(block).toMatch(/if\s*\(!validZoneIds\.has\(zoneId\)\)\s*continue/);
|
||||
});
|
||||
|
||||
it("handleGenerate no longer contains the IMP-08 B-3 self-compare anti-pattern", () => {
|
||||
// Strip comments — the u7 docblock intentionally references the removed
|
||||
// identifiers (`defaultByZone` / `sameAsDefault` / `zoneSectionsDiff`)
|
||||
// in prose to explain the Stage 1 root cause for future readers; the
|
||||
// regression we guard against is the LIVE code re-emerging.
|
||||
const block = stripComments(sliceHandler(HOME_TSX, "handleGenerate"));
|
||||
// The Stage 1 root cause: these identifiers compared user input against
|
||||
// itself (sourcePlan === effectiveSlidePlan → zones === pendingZones,
|
||||
// both derived from the same overrides.zone_sections). u7 deleted the
|
||||
// entire block.
|
||||
expect(block).not.toMatch(/\bdefaultByZone\b/);
|
||||
expect(block).not.toMatch(/\bsameAsDefault\b/);
|
||||
expect(block).not.toMatch(/\bzoneSectionsDiff\b/);
|
||||
});
|
||||
|
||||
it("co-PUT payload contract: marker=true + zone_sections land in a single PUT body", async () => {
|
||||
fetchMock.mockResolvedValue(mockResponse({}));
|
||||
// Shape produced by handleSectionDrop after the u6 marker flip.
|
||||
void saveUserOverrides("03_demo", {
|
||||
zone_sections: { left: ["03-2"], right: ["03-1"] },
|
||||
manual_section_assignment: true,
|
||||
});
|
||||
vi.advanceTimersByTime(300);
|
||||
await drainMicrotasks();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const body = lastPutBody() as Record<string, unknown>;
|
||||
// Both axes in the same PUT body — exact equality, not arrayContaining,
|
||||
// because any extra axis would mean a foreign mutation leaked through.
|
||||
expect(Object.keys(body).sort()).toEqual(
|
||||
["manual_section_assignment", "zone_sections"].sort(),
|
||||
);
|
||||
expect(body.manual_section_assignment).toBe(true);
|
||||
expect(body.zone_sections).toEqual({ left: ["03-2"], right: ["03-1"] });
|
||||
});
|
||||
|
||||
it("co-PUT payload contract: marker=false carries explicitly through saveUserOverrides", async () => {
|
||||
// u12 will add the apply/cancel explicit `false` PUT; the typed client
|
||||
// must already propagate the literal `false` through the debounce
|
||||
// bucket. A truthiness-based coalesce in the bucket merge would drop
|
||||
// the value and re-open the stale-disk window. This locks the wire
|
||||
// contract independently of the u12 caller-site write.
|
||||
fetchMock.mockResolvedValue(mockResponse({}));
|
||||
void saveUserOverrides("03_demo", { manual_section_assignment: false });
|
||||
vi.advanceTimersByTime(300);
|
||||
await drainMicrotasks();
|
||||
const body = lastPutBody() as Record<string, unknown>;
|
||||
expect(Object.keys(body)).toEqual(["manual_section_assignment"]);
|
||||
expect(body.manual_section_assignment).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── IMP-55 #93 u12 — stale-disk marker reset on apply / cancel ───────────
|
||||
// u5 resets the in-memory marker on layout apply, and u3's seed via
|
||||
// `createInitialUserSelection` resets it on cancel. But the disk persists
|
||||
// independently — a prior drag wrote `true` via u6's co-PUT, so after a
|
||||
// page reload the u3 restore branch would re-seed `true` and the u7 gate
|
||||
// would forward auto-carried section assignments → PARTIAL_COVERAGE
|
||||
// regression. u12 closes that window by writing `manual_section_assignment:
|
||||
// false` to disk via saveUserOverrides on both apply and cancel paths.
|
||||
describe("IMP-55 #93 u12 — stale-disk marker reset on layout apply/cancel", () => {
|
||||
it("handleApplyPendingLayout source contains a marker=false saveUserOverrides PUT", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleApplyPendingLayout");
|
||||
// Stripped-comment source so the u5 docblock prose doesn't satisfy the
|
||||
// assertion — must be a real call expression.
|
||||
const code = stripComments(block);
|
||||
// Uploaded-file gate (mirrors the u6 / other handler pattern — the
|
||||
// demo-mode initial render path must not PUT to an empty key).
|
||||
expect(code).toMatch(
|
||||
/if\s*\(\s*p\.uploadedFile\s*\)[\s\S]*?saveUserOverrides\([\s\S]*?manual_section_assignment:\s*false[\s\S]*?\)/,
|
||||
);
|
||||
expect(code).toMatch(/deriveUserOverridesKey\(p\.uploadedFile\.name\)/);
|
||||
});
|
||||
|
||||
it("handleCancelPendingLayout source contains a marker=false saveUserOverrides PUT", () => {
|
||||
const block = sliceHandler(HOME_TSX, "handleCancelPendingLayout");
|
||||
const code = stripComments(block);
|
||||
// Cancel handler converts from arrow-body to function-body for the
|
||||
// disk PUT; the in-memory reset still comes from createInitialUserSelection.
|
||||
expect(code).toMatch(
|
||||
/if\s*\(\s*p\.uploadedFile\s*\)[\s\S]*?saveUserOverrides\([\s\S]*?manual_section_assignment:\s*false[\s\S]*?\)/,
|
||||
);
|
||||
expect(code).toMatch(/createInitialUserSelection\(p\.slidePlan\)/);
|
||||
});
|
||||
|
||||
it("apply path PUT payload: marker=false carries alone (no auto-carry leakage)", async () => {
|
||||
// The apply handler issues a dedicated PUT for the marker reset that is
|
||||
// independent of the (conditional) zone_geometries PUT and of the
|
||||
// in-memory zone_sections rewrite. The wire contract for this PUT must
|
||||
// contain only the marker — if zone_sections leaked into the same body
|
||||
// it would re-arm the u9 backend fallback gate against u12's intent.
|
||||
fetchMock.mockResolvedValue(mockResponse({}));
|
||||
void saveUserOverrides("03_demo", { manual_section_assignment: false });
|
||||
vi.advanceTimersByTime(300);
|
||||
await drainMicrotasks();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const body = lastPutBody() as Record<string, unknown>;
|
||||
expect(Object.keys(body)).toEqual(["manual_section_assignment"]);
|
||||
expect(body.manual_section_assignment).toBe(false);
|
||||
});
|
||||
|
||||
it("apply path PUT is unconditional (does NOT gate on hadPriorGeoms)", () => {
|
||||
// The u4 zone_geometries PUT inside handleApplyPendingLayout is
|
||||
// conditional (`p.uploadedFile && hadPriorGeoms`). The u12 marker PUT
|
||||
// must NOT inherit that gate — a stale disk `true` can exist without
|
||||
// any prior zone_geometries, so the reset must always fire.
|
||||
const code = stripComments(sliceHandler(HOME_TSX, "handleApplyPendingLayout"));
|
||||
// Locate the marker PUT and verify its enclosing `if` clause is just
|
||||
// `p.uploadedFile`, not the compound `... && hadPriorGeoms` guard.
|
||||
const markerCallMatch = code.match(
|
||||
/if\s*\(([^)]*)\)\s*\{[^}]*saveUserOverrides\([^)]*manual_section_assignment:\s*false[^)]*\)/,
|
||||
);
|
||||
expect(markerCallMatch).not.toBeNull();
|
||||
if (markerCallMatch) {
|
||||
expect(markerCallMatch[1].trim()).toBe("p.uploadedFile");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user