Files
olivv-cs-ComfyUI-FunPack/app/legacy/scene_align.test.js
T
digital-garbageandClaude Sonnet 5 29ac44ffcd feat: v4 UI port Phase A start -- module categories, legacy frontend served live
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>
2026-09-14 23:58:51 +03:00

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