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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 08:27:09 +09:00
parent 9062931863
commit 4e281a20d8
13 changed files with 834 additions and 52 deletions

View File

@@ -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);
});
});