Files
olivv-cs-ComfyUI-FunPack/app/legacy/engine_defaults.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

109 lines
4.2 KiB
JavaScript

// A knob left at its default is not a setting — it is the absence of one.
//
// Reported from a rental: the H3 gain dials could not be cleared. Once touched, a value was
// written into the project file and stayed there; typing the default back in stored the
// default rather than removing the key, so the value outlived the refinement key it came
// from and looked like learned state that would not clear.
//
// The learned values themselves have always lived only in the refinement key. What leaked
// into the project was the MANUAL side of the same dials, which from the user's chair is the
// same knob.
const test = require("node:test");
const assert = require("node:assert");
// The rule under test, extracted so it can run without the DOM the renderer needs.
function writeSamplerInput(store, k, value, immediate) {
if (value === k.default) {
store.unsetSamplerInput(k.name);
if (immediate) store.flushSave?.();
return;
}
(immediate ? store.setSamplerInputNow : store.setSamplerInput)(k.name, value);
}
function fakeStore() {
const calls = [];
return {
calls,
unsetSamplerInput: (n) => calls.push(["unset", n]),
setSamplerInput: (n, v) => calls.push(["set", n, v]),
setSamplerInputNow: (n, v) => calls.push(["setNow", n, v]),
flushSave: () => calls.push(["flush"]),
};
}
const GAIN = { name: "h3_gain_video", default: 1.0 };
const BIAS = { name: "h3_taste_bias", default: 0.0 };
test("returning a dial to its default removes it from the project", () => {
const s = fakeStore();
writeSamplerInput(s, GAIN, 1.0, true);
assert.deepStrictEqual(s.calls[0], ["unset", "h3_gain_video"]);
});
test("a default is never stored as a value", () => {
const s = fakeStore();
writeSamplerInput(s, GAIN, 1.0, true);
assert.ok(!s.calls.some((c) => c[0] === "set" || c[0] === "setNow"));
});
test("a real change is still stored", () => {
const s = fakeStore();
writeSamplerInput(s, GAIN, 0.85, true);
assert.deepStrictEqual(s.calls[0], ["setNow", "h3_gain_video", 0.85]);
});
test("a zero default clears too", () => {
// h3_taste_bias is centred on 0, not 1 — a rule keyed on truthiness would never clear it.
const s = fakeStore();
writeSamplerInput(s, BIAS, 0.0, true);
assert.deepStrictEqual(s.calls[0], ["unset", "h3_taste_bias"]);
});
test("a non-zero value on a zero-default knob is stored", () => {
const s = fakeStore();
writeSamplerInput(s, BIAS, -0.2, true);
assert.deepStrictEqual(s.calls[0], ["setNow", "h3_taste_bias", -0.2]);
});
test("clearing on commit flushes, so the removal reaches disk", () => {
const s = fakeStore();
writeSamplerInput(s, GAIN, 1.0, true);
assert.ok(s.calls.some((c) => c[0] === "flush"));
});
test("clearing while typing does not flush", () => {
// Quiet while the caret is in the field, same rule the text knobs already follow.
const s = fakeStore();
writeSamplerInput(s, GAIN, 1.0, false);
assert.ok(!s.calls.some((c) => c[0] === "flush"));
});
test("a store without flushSave does not throw", () => {
const s = fakeStore();
delete s.flushSave;
assert.doesNotThrow(() => writeSamplerInput(s, GAIN, 1.0, true));
});
test("booleans clear when set back to their default", () => {
const s = fakeStore();
writeSamplerInput(s, { name: "segmented_detailing", default: false }, false, true);
assert.deepStrictEqual(s.calls[0], ["unset", "segmented_detailing"]);
});
// ── handing ownership back ──────────────────────────────────────────────────
//
// Reported from a rental as a question, which is the best kind: "save as manual, load it,
// switch to learned — and nothing gets cleared. Is it so?" It was. The six values stayed in
// the project, inert (learned mode reads the key, never the dials) and invisible (the dials
// hide in learned mode), until the next time the project was opened. Correct output, state
// nobody could see. That is the failure this whole rule exists to prevent.
test("an ordinary knob returning to default clears only itself", () => {
const s = fakeStore();
writeSamplerInput(s, GAIN, 1.0, true);
assert.deepStrictEqual(s.calls.filter((c) => c[0] === "unset"), [["unset", "h3_gain_video"]]);
});