Two pieces of the "port v4's exact UI onto v5's backend" plan: - core/contract.py: new optional ModuleSpec.category field (closed set: continuity, guidance, conditioning, sampling, post, system), validated in core/schema.py, threaded through core/registry.py's FIELDS and to_manifest(). Presentation grouping, independent of STAGE (execution order) -- a module can be stage="sampling" and category="guidance" at once. Only the 4 existing modules that actually render a settings panel (alg, sharpen, appearance, full_control) got a category assigned; the other 12 have none to categorize (no SETTINGS block) and stay uncategorised, which is valid. - v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied verbatim into app/legacy/. core/routes.py's "/" route now serves legacy/index.html instead of the composer entry point; a <base> tag in that file resolves its own relative script/link tags against /funpack/app/legacy/ regardless of what URL it's served at, so every file moved as a unit with zero per-file path edits. Verified live: all 61 assets load 200 OK, the real v4 onboarding screen renders pixel-for- pixel (it's the actual DOM/CSS, not a reimplementation), and several v4 API calls (/api/media, /api/projects, /api/git/status) already succeed against v5's real backend by coincidence of shared route naming -- the rest 404/405 as expected, since api.js's ~90 functions haven't been rewired to v5's actual routes yet (next piece of this plan). The old composer entry point (app/index.html, app/composer/**) stays in the tree, unreferenced, until a later cleanup pass once nothing imports it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
3.2 KiB
JavaScript
80 lines
3.2 KiB
JavaScript
// Regression tests for the global-prompt scene→root matching (scene_align.js).
|
|
// Run: node --test movie_editor/frontend/scene_align.test.js
|
|
//
|
|
// These guard the "stale scene anchor after reorder/insert/delete" bug: a scene's render +
|
|
// anchor live under its root id, so the matcher must bind a parsed slot to the root that
|
|
// owns its CONTENT, never to whatever root sits at the same array index.
|
|
const test = require("node:test");
|
|
const assert = require("node:assert");
|
|
const { matchParsedToRoots } = require("./scene_align.js");
|
|
|
|
// Helper: resolve each parsed slot to the root's anchor label it would inherit ("new" = -1).
|
|
function anchors(roots, parsedTexts) {
|
|
const rootTexts = roots.map((r) => r.text);
|
|
const idx = matchParsedToRoots(parsedTexts, rootTexts);
|
|
return idx.map((ri) => (ri >= 0 ? roots[ri].anchor : "new"));
|
|
}
|
|
|
|
const A = { text: "a cat", anchor: "anchorA" };
|
|
const B = { text: "a dog", anchor: "anchorB" };
|
|
const C = { text: "a bird", anchor: "anchorC" };
|
|
|
|
test("reorder only — anchors follow content, not slot", () => {
|
|
assert.deepStrictEqual(anchors([A, B], ["a dog", "a cat"]), ["anchorB", "anchorA"]);
|
|
});
|
|
|
|
test("edit in place — render/anchor preserved", () => {
|
|
assert.deepStrictEqual(anchors([A], ["a black cat"]), ["anchorA"]);
|
|
});
|
|
|
|
test("insert at front — existing anchors stay, new slot is fresh", () => {
|
|
assert.deepStrictEqual(
|
|
anchors([A, B], ["new scene", "a cat", "a dog"]),
|
|
["new", "anchorA", "anchorB"],
|
|
);
|
|
});
|
|
|
|
test("delete middle — survivors keep anchors", () => {
|
|
assert.deepStrictEqual(anchors([A, B, C], ["a cat", "a bird"]), ["anchorA", "anchorC"]);
|
|
});
|
|
|
|
test("reorder + edit one — unchanged text pins it, edited one follows", () => {
|
|
assert.deepStrictEqual(anchors([A, B], ["a dog", "a big cat"]), ["anchorB", "anchorA"]);
|
|
});
|
|
|
|
test("edit + insert + keep — leftover edit takes leftover root, new is fresh", () => {
|
|
assert.deepStrictEqual(
|
|
anchors([A, B], ["a cat v2", "NEW", "a dog"]),
|
|
["anchorA", "new", "anchorB"],
|
|
);
|
|
});
|
|
|
|
test("irreducible (both reordered AND fully rewritten) — positional fallback", () => {
|
|
// No unchanged text to anchor on → genuinely ambiguous; positional guess is acceptable.
|
|
assert.deepStrictEqual(anchors([A, B], ["a big dog", "a cute cat"]), ["anchorA", "anchorB"]);
|
|
});
|
|
|
|
test("duplicate texts — nearest position breaks the tie", () => {
|
|
const X0 = { text: "x", anchor: "x0" };
|
|
const X2 = { text: "x", anchor: "x2" };
|
|
// roots at index 0 and 2; parsed at index 0 and 3 → 0↔x0, 3↔x2.
|
|
assert.deepStrictEqual(
|
|
anchors([X0, { text: "y", anchor: "y1" }, X2], ["x", "y", "z", "x"]),
|
|
["x0", "y1", "new", "x2"],
|
|
);
|
|
});
|
|
|
|
test("empty parsed text is positional only (never exact-matches a root)", () => {
|
|
// Blank slot must not steal a root by 'exact text'; it takes a leftover root positionally.
|
|
assert.deepStrictEqual(anchors([A, B], ["", "a cat"]), ["anchorB", "anchorA"]);
|
|
});
|
|
|
|
test("no old roots — every slot is fresh", () => {
|
|
assert.deepStrictEqual(anchors([], ["a", "b"]), ["new", "new"]);
|
|
});
|
|
|
|
test("each root used at most once", () => {
|
|
const idx = matchParsedToRoots(["a cat", "a cat"], ["a cat"]);
|
|
assert.deepStrictEqual(idx, [0, -1]); // second duplicate slot finds no root left
|
|
});
|