Download ComfyUI-MiniMaxH3-Context-Loop/tests/_checkpoint_local_output_js_test.mjs from dzmdd/Fast_deploy: direct link, hf CLI and curl.
- Browser
- Download file 37.9 kB
-
https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/tests/_checkpoint_local_output_js_test.mjs
- Command line
-
hf download hf://dzmdd/Fast_deploy/ComfyUI-MiniMaxH3-Context-Loop/tests/_checkpoint_local_output_js_test.mjs
-
curl -L -o _checkpoint_local_output_js_test.mjs https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/tests/_checkpoint_local_output_js_test.mjs
37.9 kB
| import assert from "node:assert/strict"; | |
| import fs from "node:fs"; | |
| import vm from "node:vm"; | |
| import * as core from "../web/h3_checkpoint_manager_core.mjs"; | |
| const a = "a".repeat(32), b = "b".repeat(32), c = "c".repeat(32), d = "d".repeat(32); | |
| const payload = { | |
| revisions:[ | |
| {scene:1, revision:a, active:true, ready:true}, | |
| {scene:2, revision:b, active:true, ready:true, parent:{scene:1, revision:a}}, | |
| {scene:2, revision:c, active:false, ready:true, parent:{scene:1, revision:a}}, | |
| {scene:3, revision:d, active:true, ready:true, parent:{scene:2, revision:b}}, | |
| ], | |
| scenes:[1, 2, 3].map(scene => ({scene})), | |
| branches:[ | |
| {label:"Active branch", active:true, path:[{scene:1, revision:a}, {scene:2, revision:b}, {scene:3, revision:d}]}, | |
| {label:"Alternate", active:false, path:[{scene:1, revision:a}, {scene:2, revision:c}]}, | |
| ], | |
| summary:{scene_count:3, revision_count:4, branch_count:2, bytes:0}, | |
| }; | |
| const local = core.checkpointLocalSelectionJson(payload, "demo", payload.revisions[2], {start:1, end:3}); | |
| assert.equal(core.checkpointLocalSelection(local).output_mode, "workflow_local"); | |
| assert.equal(core.checkpointOutputSelectionJson(local, payload, "other", payload.revisions[3]), local); | |
| assert.equal(core.checkpointLocalSelection(""), null); | |
| assert.equal(core.checkpointLocalSelection("invalid"), null); | |
| assert.equal(core.checkpointLocalSelection(core.checkpointSelectionJson(payload, "demo", payload.revisions[1])), null); | |
| assert.throws(() => core.checkpointLocalSelectionJson({...payload, revisions:payload.revisions.map( | |
| item => item.scene === 1 ? {...item, ready:false} : item)}, "demo", payload.revisions[2]), /available/); | |
| assert.throws(() => core.checkpointLocalSelectionJson(payload, "", payload.revisions[2]), /complete/); | |
| const chapterPin = core.checkpointLocalSelectionJson(payload, "demo", payload.revisions[3], {start:3, end:3}); | |
| const switchedPrefix = structuredClone(payload); | |
| switchedPrefix.revisions[1].active = false; | |
| switchedPrefix.revisions[2].active = true; | |
| assert.equal(core.checkpointOutputSelectionJson(chapterPin, switchedPrefix, "demo", payload.revisions[3], {start:3, end:3}), chapterPin, | |
| "previous chapters are pinned too, not rebuilt from new project pointers"); | |
| assert.equal(JSON.parse(chapterPin).lineage[1].revision, b); | |
| const chapterOnlyPin = core.checkpointLocalSelectionJson({ | |
| ...payload, revisions:payload.revisions.map(item => item.scene < 3 ? {...item, ready:false} : item), | |
| }, "demo", payload.revisions[3], {start:3, end:3}, "chapter"); | |
| assert.equal(JSON.parse(chapterOnlyPin).output_scope, "chapter"); | |
| assert.deepEqual(JSON.parse(chapterOnlyPin).lineage, JSON.parse(chapterPin).lineage, | |
| "prior immutable timing metadata remains pinned, but its media is not required"); | |
| assert.equal(core.checkpointOutputSelectionJson(chapterOnlyPin, switchedPrefix, "demo", payload.revisions[2]), chapterOnlyPin); | |
| // The whole extension is mounted with a lightweight DOM, so these exercise | |
| // the real handlers, async refresh and serialized widget (not extracted mocks). | |
| class Element { | |
| constructor(tag) { | |
| this.tag = tag; this.children = []; this.listeners = {}; this.dataset = {}; | |
| this.style = {setProperty(){}}; this.className = ""; this.textContent = ""; | |
| this.classList = { | |
| add:(name) => { this.className += ` ${name}`; }, | |
| toggle:(name, enabled) => { | |
| this.className = this.className.split(" ").filter(item => item !== name).join(" "); | |
| if (enabled) this.className += ` ${name}`; | |
| }, | |
| }; | |
| } | |
| append(...items) { this.children.push(...items); } | |
| replaceChildren(...items) { this.children = items; } | |
| addEventListener(name, callback) { this.listeners[name] = callback; } | |
| setAttribute(name, value) { this[name] = value; } | |
| removeAttribute(name) { delete this[name]; } | |
| load() {} | |
| pause() {} | |
| click() { assert.ok(!this.disabled, `${this.textContent} is disabled`); this.listeners.click(); } | |
| } | |
| const source = fs.readFileSync(new URL("../web/h3_chain_checkpoint_manager.js", import.meta.url), "utf8") | |
| .replace(/^import\s[\s\S]*?from\s+"[^"]+";\n/gm, ""); | |
| let currentGraph = structuredClone(payload), runs = ["demo", "other"], failRequests = false; | |
| let confirms = true, mutations = 0, dirty = 0; | |
| let attachResponse = null; | |
| let processingDeletion = false, deleteConflict = false, delayedProcessingPreview = null; | |
| let retainedPixelTakes = []; | |
| let snapshotRetirement = false, snapshotRetired = false, retirementError = 0, delayedRetirementPreview = null; | |
| const snapshotAddress = `h3_chains/demo/chapters/01_one/manifests/${a}.json`; | |
| const confirmations = []; | |
| let extension; | |
| const requests = []; | |
| const context = vm.createContext({ | |
| ...core, URLSearchParams, console, | |
| document:{head:new Element("head"), getElementById:() => null, createElement:tag => new Element(tag)}, | |
| app:{registerExtension(value){ extension = value; }, graph:{setDirtyCanvas(){}}}, | |
| api:{apiURL:path => path, fetchApi:async (path, options={}) => { | |
| requests.push({path, options}); | |
| if (failRequests) throw new Error("offline"); | |
| let data; | |
| if (path.endsWith("/runs")) data = {runs:runs.map(run_name => ({run_name, checkpoint_count:3}))}; | |
| else if (path.includes("/checkpoints?")) data = structuredClone(currentGraph); | |
| else if (path.endsWith("/processing-checkpoints/delete-preview") && processingDeletion) { | |
| const body = JSON.parse(options.body); | |
| data = {allowed:true, metadata_path:body.metadata_path, snapshot:"preview-token", | |
| owned_file_count:6, reclaimed_bytes:1024, files:[], not_deleted:["Originals and references"], | |
| retained_independent_takes:structuredClone(retainedPixelTakes)}; | |
| if (delayedProcessingPreview) { | |
| const wait = delayedProcessingPreview; delayedProcessingPreview = null; | |
| await wait; | |
| } | |
| } | |
| else if (path.endsWith("/processing-checkpoints/delete") && processingDeletion) { | |
| mutations++; | |
| const body = JSON.parse(options.body); | |
| assert.equal(body.snapshot, "preview-token"); | |
| assert.ok(currentGraph.processing_variants.some(item => item.key === body.metadata_path)); | |
| if (deleteConflict) return {ok:false, status:409, json:async () => ({error:"Preview changed; refresh", preview:{allowed:false}})}; | |
| currentGraph.processing_variants = currentGraph.processing_variants.filter(item => item.key !== body.metadata_path); | |
| data = {message:"Deleted processed version; originals unchanged.", reclaimed_bytes:1024}; | |
| } | |
| else if (path.endsWith("/chapter-snapshots/retire-preview") && snapshotRetirement) { | |
| assert.equal(JSON.parse(options.body).path, snapshotAddress); | |
| data = {allowed:true, path:snapshotAddress, snapshot:"retirement-token", chapter_number:1, | |
| chapter_manifest_id:a, retired_path:snapshotAddress.replace("/manifests/", "/retired_manifests/"), | |
| scenes:[{scene:2, revision:c, active:false}], message:"No clips are deleted."}; | |
| if (delayedRetirementPreview) { | |
| const wait = delayedRetirementPreview; delayedRetirementPreview = null; | |
| await wait; | |
| } | |
| } | |
| else if (path.endsWith("/chapter-snapshots/retire") && snapshotRetirement) { | |
| mutations++; | |
| const body = JSON.parse(options.body); | |
| assert.equal(body.path, snapshotAddress); | |
| assert.equal(body.snapshot, "retirement-token"); | |
| if (retirementError) return {ok:false, status:retirementError, | |
| json:async () => ({error:retirementError === 423 ? "Project is read only" : "Retirement preview changed"})}; | |
| snapshotRetired = true; | |
| data = {message:"Snapshot retired; no clips deleted."}; | |
| } | |
| else if (path.endsWith("/delete-preview") && snapshotRetirement) { | |
| data = {allowed:snapshotRetired, blockers:snapshotRetired ? [] : ["Snapshot pins this take"], files:[], | |
| chapter_references:snapshotRetired ? [] : [{number:1, snapshot:a, path:snapshotAddress}]}; | |
| } | |
| else if (path.endsWith("/delete-preview")) data = {allowed:false, blockers:["test"]}; | |
| else if (path.endsWith("/attribute") && attachResponse) { | |
| mutations++; | |
| const request = JSON.parse(options.body); | |
| assert.equal(request.parent_revision, b); | |
| assert.equal(request.candidate_revision, d); | |
| const alias = {...currentGraph.revisions.find(item => item.revision === d), | |
| revision:attachResponse.revision, parent:{scene:2, revision:b}}; | |
| currentGraph.revisions.push(alias); | |
| currentGraph.branches[0].path.push({scene:3, revision:alias.revision}); | |
| delete currentGraph.branches[0].attribution_slot; | |
| data = attachResponse; | |
| } | |
| else { mutations++; throw new Error(`Unexpected request ${path}`); } | |
| return {ok:true, json:async () => data}; | |
| }}, | |
| window:{setTimeout:callback => callback(), confirm:message => { confirmations.push(message); return confirms; }}, | |
| projectMutationOptions:(_node, _run, options) => { | |
| if (attachResponse || processingDeletion || snapshotRetirement) return options; | |
| mutations++; throw new Error("Local output attempted project mutation"); | |
| }, | |
| promptCompanionSync:{}, | |
| }); | |
| vm.runInContext(source, context); | |
| class NodeType {} | |
| await extension.beforeRegisterNodeDef(NodeType, {name:"MiniMaxH3ChainCheckpointManager"}); | |
| const settle = async () => { for (let i = 0; i < 12; i++) await new Promise(setImmediate); }; | |
| function makeNode(saved="", properties={}) { | |
| const node = {properties:{h3_checkpoint_manager_run:"demo", ...properties}, | |
| widgets:[{name:"selection_json", value:saved, callback:() => dirty++}], | |
| inputs:[], graph:{setDirtyCanvas:() => dirty++}, setSize(){}, | |
| addDOMWidget(_name, _type, root) { this.root = root; return {}; }, | |
| }; | |
| context.mount(node); | |
| return node; | |
| } | |
| function elements(node) { | |
| const flatten = item => [item, ...item.children.flatMap(flatten)]; | |
| return flatten(node.root); | |
| } | |
| const byText = (node, text) => elements(node).find(item => item.tag === "button" && item.textContent === text); | |
| const byClass = (node, name) => elements(node).find(item => item.className.split(" ").includes(name)); | |
| const select = (node, scene, revision) => byText(node, `S${scene} · ${revision.slice(0, 8)}`).click(); | |
| const value = node => node.widgets[0].value; | |
| const scoped = makeNode(chapterOnlyPin); | |
| await settle(); | |
| assert.equal(byClass(scoped, "h3cm-output-scope").value, "chapter"); | |
| select(scoped, 2, c); | |
| await settle(); | |
| const scopeControl = byClass(scoped, "h3cm-output-scope"); | |
| scopeControl.value = "project"; scopeControl.listeners.change(); | |
| assert.equal(JSON.parse(value(scoped)).lineage.at(-1).revision, d, | |
| "explicit scope changes retain the pinned tip, not the browsed revision"); | |
| scopeControl.value = "chapter"; scopeControl.listeners.change(); | |
| assert.match(byClass(scoped, "h3cm-output-summary").textContent, /scenes 3–3 \(1 clip; selected chapter only\)/); | |
| assert.equal(mutations, 0); | |
| const first = makeNode(), second = makeNode(); | |
| await settle(); | |
| assert.equal(JSON.parse(value(first)).lineage.at(-1).revision, d, "legacy default still follows selection"); | |
| const wholeActive = value(first); | |
| const wholeActiveSummary = byClass(first, "h3cm-output-summary").textContent; | |
| assert.match(wholeActiveSummary, /Will send to connected nodes: Original checkpoints/); | |
| assert.match(wholeActiveSummary, /demo · original branch through scene 3 \/ dddddddd/); | |
| assert.match(wholeActiveSummary, /scenes 1–3 \(3 clips;/); | |
| assert.match(wholeActiveSummary, /follows branch selection/); | |
| select(first, 2, b); await settle(); | |
| assert.equal(value(first), wholeActive, "previewing an earlier clip cannot truncate output"); | |
| assert.equal(byClass(first, "h3cm-output-summary").textContent, wholeActiveSummary); | |
| select(first, 1, a); await settle(); | |
| assert.equal(value(first), wholeActive, "even a shared ancestor is preview-only"); | |
| byText(first, "Use branch locally").click(); | |
| assert.equal(JSON.parse(value(first)).lineage.at(-1).revision, d, | |
| "Use branch locally chooses the clicked row's full branch, not the previewed ancestor"); | |
| byText(first, "Follow branch selection").click(); | |
| select(first, 2, c); | |
| await settle(); | |
| assert.equal(value(first), wholeActive, "previewing a different branch alone does not change output"); | |
| assert.equal(byClass(first, "h3cm-output-summary").textContent, wholeActiveSummary); | |
| byText(first, "Use branch locally").click(); | |
| const pin = value(first), otherBefore = value(second); | |
| assert.equal(pin, local); | |
| assert.ok(elements(first).some(item => item.textContent === "local output")); | |
| select(first, 3, d); | |
| await settle(); | |
| assert.equal(value(first), pin, "browsing does not move pinned output"); | |
| assert.match(byClass(first, "h3cm-output-summary").textContent, /through scene 2 \/ cccccccc/); | |
| assert.match(byClass(first, "h3cm-output-summary").textContent, /pinned to this workflow/); | |
| assert.equal(value(second), otherBefore, "other manager unaffected"); | |
| assert.equal(mutations, 0); | |
| assert.ok(dirty > 0, "widget changes are serialized and mark the workflow dirty"); | |
| const connectedPlan = {type:"MiniMaxH3ChainPlan", widgets:[ | |
| {name:"run_name", value:"other"}, {name:"plan_json", value:"unchanged Plan"}, | |
| ]}; | |
| first.inputs = [{link:1}]; | |
| first.graph.links = {1:{origin_id:99}}; | |
| first.graph.getNodeById = () => connectedPlan; | |
| first.onConnectionsChange(); | |
| await settle(); | |
| assert.equal(value(first), pin, "connecting a Plan from another Run must not move the local output"); | |
| assert.equal(connectedPlan.widgets[1].value, "unchanged Plan"); | |
| currentGraph.revisions[1].active = false; | |
| currentGraph.revisions[2].active = true; | |
| first._h3CheckpointManagerRefresh(); | |
| await settle(); | |
| assert.equal(value(first), pin, "project-wide active changes do not move local pin"); | |
| const reopened = makeNode(pin, {h3_checkpoint_manager_scene:3, h3_checkpoint_manager_revision:d}); | |
| await settle(); | |
| assert.equal(value(reopened), pin, "shorter pinned branch survives startup's deepest-tip preference"); | |
| assert.match(byClass(reopened, "h3cm-output-summary").textContent, /through scene 2 \/ cccccccc/); | |
| // ComfyUI reconfiguration replaces the saved widget value; it remains the | |
| // sole pin authority even when an old browse selection is still in memory. | |
| reopened.widgets[0].value = core.checkpointLocalSelectionJson(payload, "demo", payload.revisions[0], {start:1, end:3}); | |
| const newPin = value(reopened); | |
| reopened._h3CheckpointManagerRefresh(); | |
| await settle(); | |
| assert.equal(value(reopened), newPin); | |
| currentGraph.revisions = currentGraph.revisions.filter(item => item.revision !== c); | |
| first._h3CheckpointManagerRefresh(); | |
| await settle(); | |
| assert.equal(value(first), pin); | |
| assert.match(byClass(first, "h3cm-output-summary").textContent, /unavailable.*no fallback/); | |
| runs = ["other"]; | |
| first._h3CheckpointManagerRefresh(); | |
| await settle(); | |
| assert.equal(value(first), pin); | |
| assert.equal(byClass(first, "h3cm-run-select").value, "demo", "missing pinned run is not replaced by another run"); | |
| failRequests = true; | |
| first._h3CheckpointManagerRefresh(); | |
| await settle(); | |
| assert.equal(value(first), pin, "transient network errors retain pin"); | |
| failRequests = false; runs = ["demo", "other"]; currentGraph = structuredClone(payload); | |
| first._h3CheckpointManagerRefresh(); | |
| await settle(); | |
| const runSelect = byClass(first, "h3cm-run-select"); | |
| confirms = false; | |
| runSelect.value = "other"; runSelect.listeners.change(); | |
| assert.equal(runSelect.value, "demo"); | |
| assert.equal(value(first), pin); | |
| confirms = true; | |
| runSelect.value = "other"; runSelect.listeners.change(); | |
| await settle(); | |
| assert.equal(core.checkpointLocalSelection(value(first)), null); | |
| assert.equal(JSON.parse(value(first)).run_name, "other"); | |
| select(second, 2, c); await settle(); | |
| byText(second, "Use branch locally").click(); | |
| select(second, 3, d); await settle(); | |
| byText(second, "Follow branch selection").click(); | |
| assert.equal(core.checkpointLocalSelection(value(second)), null); | |
| assert.equal(JSON.parse(value(second)).lineage.at(-1).revision, d); | |
| assert.equal(mutations, 0, "local use, release, browsing and reload never call project mutation APIs"); | |
| assert.ok(requests.every(({path, options}) => !options.method || path.endsWith("/delete-preview"))); | |
| console.log("Checkpoint local output: real mounted UI, isolation, pin persistence, missing runs/takes, refresh and release pass"); | |
| // ComfyUI mounts nodes before applying the saved widget values. Unpinned | |
| // chapter output must be restored too, not overwritten by the mounted default. | |
| const restoredScope = makeNode(); | |
| await settle(); | |
| restoredScope.widgets[0].value = core.checkpointSelectionJson( | |
| payload, "demo", payload.revisions[2], {start:1, end:3}, "chapter"); | |
| restoredScope.properties.h3_checkpoint_manager_scene = 2; | |
| restoredScope.properties.h3_checkpoint_manager_revision = c; | |
| NodeType.prototype.onConfigure.call(restoredScope); | |
| await settle(); | |
| assert.equal(byClass(restoredScope, "h3cm-output-scope").value, "chapter", | |
| "un-pinned saved chapter scope must hydrate the mounted selector"); | |
| assert.equal(JSON.parse(value(restoredScope)).output_scope, "chapter"); | |
| assert.equal(JSON.parse(value(restoredScope)).lineage.at(-1).revision, c); | |
| // A late widget restore must not queue whole-project output while the visible | |
| // selector still says chapter. Check both API and saved-workflow serialization. | |
| const staleScope = core.checkpointSelectionJson(payload, "demo", payload.revisions[2]); | |
| restoredScope.widgets[0].value = staleScope; | |
| assert.equal(byClass(restoredScope, "h3cm-output-scope").value, "chapter"); | |
| assert.equal(JSON.parse(await restoredScope.widgets[0].serializeValue()).output_scope, "chapter"); | |
| restoredScope.widgets[0].value = staleScope; | |
| const savedScope = {widgets_values:[staleScope], widgets_values_named:{selection_json:staleScope}, properties:{}}; | |
| restoredScope.onSerialize(savedScope); | |
| assert.equal(JSON.parse(savedScope.widgets_values[0]).output_scope, "chapter"); | |
| assert.equal(JSON.parse(savedScope.widgets_values_named.selection_json).output_scope, "chapter"); | |
| assert.equal(savedScope.properties.h3_checkpoint_manager_output_scope, "chapter"); | |
| const emptyScope = makeNode("", {h3_checkpoint_manager_output_scope:"chapter"}); | |
| await settle(); | |
| assert.equal(byClass(emptyScope, "h3cm-output-scope").value, "chapter", | |
| "scope survives saving while no checkpoint lineage is available"); | |
| // Replacing widgets during reconfiguration must not leave handlers writing | |
| // into the old closed-over widget instead of the one ComfyUI queues. | |
| const detachedWidget = restoredScope.widgets[0]; | |
| restoredScope.widgets[0] = {name:"selection_json", value:staleScope}; | |
| restoredScope.properties.h3_checkpoint_manager_output_scope = "project"; | |
| NodeType.prototype.onConfigure.call(restoredScope); | |
| await settle(); | |
| const reboundScope = byClass(restoredScope, "h3cm-output-scope"); | |
| assert.equal(reboundScope.value, "project"); | |
| reboundScope.value = "chapter"; | |
| reboundScope.listeners.change(); | |
| assert.equal(JSON.parse(await restoredScope.widgets[0].serializeValue()).output_scope, "chapter"); | |
| assert.notEqual(restoredScope.widgets[0], detachedWidget); | |
| const malformed = '{"output_scope":"unknown","lineage":[]}'; | |
| restoredScope.widgets[0].value = malformed; | |
| assert.equal(await restoredScope.widgets[0].serializeValue(), malformed, "do not hide invalid selection data"); | |
| console.log("Checkpoint scope: configure, API serialization, workflow save and empty selection pass"); | |
| // Follow the actual candidate card -> attach -> refreshed alias -> queued | |
| // output path. Earlier chapters remain timing-only when scope is chapter. | |
| currentGraph = structuredClone(payload); | |
| currentGraph.editorial = {chapters:[ | |
| {id:"one", title:"Chapter 1", start_scene:1}, | |
| {id:"two", title:"Chapter 2", start_scene:2}, | |
| ]}; | |
| currentGraph.revisions[3].parent = {scene:2, revision:c}; | |
| currentGraph.revisions[3].active = false; | |
| currentGraph.branches[0].path.pop(); | |
| currentGraph.branches[0].attribution_slot = { | |
| scene:3, parent_scene:2, parent_revision:b, candidates:[{scene:3, revision:d}], | |
| }; | |
| currentGraph.branches[1].path.push({scene:3, revision:d}); | |
| const attaching = makeNode(core.checkpointSelectionJson( | |
| currentGraph, "demo", currentGraph.revisions[1], {start:2, end:3}, "chapter")); | |
| await settle(); | |
| byText(attaching, "S3 · reuse saved clip").click(); | |
| assert.equal(byClass(attaching, "h3cm-output-scope").value, "chapter"); | |
| attachResponse = {scene:3, revision:"e".repeat(32), message:"Attached"}; | |
| byText(attaching, "Attach selected candidate").click(); | |
| await settle(); | |
| const attachedOutput = JSON.parse(await attaching.widgets[0].serializeValue()); | |
| assert.equal(attachedOutput.output_scope, "chapter"); | |
| assert.equal(attachedOutput.scope_start_scene, 2); | |
| assert.equal(attachedOutput.lineage.at(-1).revision, attachResponse.revision); | |
| assert.equal(mutations, 1, "only explicit attachment writes; no automatic project activation"); | |
| console.log("Checkpoint attachment: candidate -> alias -> chapter-only API output passes"); | |
| // Processing tabs mount the real extension. Merely inspecting a derivative | |
| // must not retarget the execution widget, activate a branch, or delete a base. | |
| currentGraph = structuredClone(payload); | |
| currentGraph.processing_variants = [ | |
| {key:"demo/motion/one", scene:2, revision:"1".repeat(32), stage:"derope", profile:"motion", | |
| profile_path:"demo/upscaled/motion", originals:[{scene:2, revision:b}], ready:true, | |
| width:960, height:544, raw_frames:175, delivered_frames:175, latent_saved:false, context_steps:12, | |
| video:{filename:"motion.mp4"}, audio:{filename:"motion.wav"}}, | |
| {key:"demo/motion/two", scene:2, revision:"2".repeat(32), stage:"derope", profile:"motion2", | |
| profile_path:"demo/upscaled/motion2", originals:[{scene:2, revision:b}], ready:true, | |
| checkpoint_sha256:"2".repeat(64), processing_branch:{path:"demo/motion/two", kind:"metadata", | |
| lineage:[{scene:2, revision:"2".repeat(32), metadata_path:"demo/motion/two", checkpoint_sha256:"2".repeat(64)}]}, | |
| width:960, height:544, raw_frames:175, delivered_frames:175, latent_saved:true, latent_layout:"joint_av", | |
| video:{filename:"motion2.mp4"}}, | |
| {key:"demo/hq/one", scene:2, revision:"3".repeat(32), stage:"latent_upscale", profile:"hq", | |
| profile_path:"demo/upscaled/hq", originals:[{scene:2, revision:b}], ready:true, | |
| width:1920, height:1088, latent_saved:true, latent_layout:"single", video:{filename:"hq.mp4"}}, | |
| {key:"demo/pixel/orphan", scene:2, revision:"4".repeat(32), stage:"pixel_upscale", profile:"pixel", | |
| profile_path:"demo/upscaled/pixel", originals:[], ready:false, latent_saved:false, | |
| source_status:"original unavailable or source mismatch", video:{filename:"orphan.mp4"}}, | |
| ]; | |
| const variants = makeNode(); | |
| await settle(); | |
| select(variants, 2, b); | |
| await settle(); | |
| const originalOutput = value(variants), mutationsBefore = mutations; | |
| const originalSummary = byClass(variants, "h3cm-output-summary").textContent; | |
| byText(variants, "DeRoPE · 2").click(); | |
| assert.equal(byClass(variants, "h3cm-stage-tab")["role"], "tab"); | |
| assert.equal(value(variants), originalOutput); | |
| assert.equal(byClass(variants, "h3cm-output-summary").textContent, originalSummary, | |
| "browsing DeRoPE still describes the Original source that will be sent"); | |
| assert.equal(byClass(variants, "h3cm-preview").dataset.source, "/view?filename=motion.mp4&subfolder=&type=output"); | |
| assert.ok(elements(variants).some(item => /Continuation tail only/.test(item.textContent))); | |
| byText(variants, "Use DeRoPE branch locally").click(); | |
| assert.equal(value(variants), originalOutput, "an unusable processing branch never changes output"); | |
| assert.match(byClass(variants, "h3cm-status").textContent, /unambiguous saved branch/); | |
| assert.ok(byText(variants, "Make branch active (project)").disabled); | |
| assert.ok(byText(variants, "Delete processed version").disabled); | |
| select(variants, 2, "2".repeat(32)); | |
| assert.equal(value(variants), originalOutput); | |
| assert.equal(byClass(variants, "h3cm-audio").hidden, true, "no stale sidecar from another take"); | |
| assert.ok(elements(variants).some(item => /Full latent saved \(joint_av\)/.test(item.textContent))); | |
| const savedVariantProperties = structuredClone(variants.properties); | |
| const processingPin = makeNode(originalOutput, savedVariantProperties); | |
| await settle(); | |
| select(processingPin, 2, "2".repeat(32)); | |
| byText(processingPin, "Use DeRoPE branch locally").click(); | |
| const pinnedProcessing = JSON.parse(value(processingPin)); | |
| assert.equal(pinnedProcessing.processing_source.stage, "derope"); | |
| assert.equal(pinnedProcessing.output_mode, "workflow_local"); | |
| assert.equal(pinnedProcessing.lineage.at(-1).scene, 3, "processing preview does not trim the original branch"); | |
| assert.deepEqual(pinnedProcessing.processing_source.branch.lineage.map(item => item.scene), [2], "absent processing scenes remain original fallbacks"); | |
| const processedSummary = byClass(processingPin, "h3cm-output-summary").textContent; | |
| assert.match(processedSummary, /DeRoPE checkpoints for scenes 2; Original fallback for scenes 1, 3/); | |
| assert.match(processedSummary, /DeRoPE branch 22222222 \(demo\/upscaled\/motion2\)/); | |
| const restoredPin = makeNode(value(processingPin), structuredClone(processingPin.properties)); | |
| await settle(); | |
| assert.deepEqual(JSON.parse(value(restoredPin)), pinnedProcessing); | |
| byText(restoredPin, "Original · 4").click(); | |
| assert.deepEqual(JSON.parse(value(restoredPin)), pinnedProcessing, "tab switch cannot reset DeRoPE output"); | |
| assert.equal(byClass(restoredPin, "h3cm-output-summary").textContent, processedSummary); | |
| byText(restoredPin, "Use branch locally").click(); | |
| assert.equal(JSON.parse(value(restoredPin)).processing_source, undefined, "explicit Original selection resets source stage"); | |
| assert.match(byClass(restoredPin, "h3cm-output-summary").textContent, /Will send to connected nodes: Original checkpoints/); | |
| const reopenedVariant = makeNode(originalOutput, savedVariantProperties); | |
| await settle(); | |
| assert.equal(byClass(reopenedVariant, "h3cm-preview").dataset.source, "/view?filename=motion2.mp4&subfolder=&type=output"); | |
| assert.equal(reopenedVariant.properties.h3_checkpoint_manager_scene, 2, | |
| "restoring a processing view must not promote its source selection to the deepest original tip"); | |
| assert.equal(value(reopenedVariant), originalOutput, "reopening a processing tab never rewrites original output"); | |
| byText(variants, "S1 · not saved").click(); | |
| assert.equal(byClass(variants, "h3cm-preview").src, undefined, "missing stage does not impersonate original preview"); | |
| assert.equal(value(variants), originalOutput, "even browsing another scene in a derivative tab leaves output unchanged"); | |
| byText(variants, "Latent Upscale · 1").click(); | |
| select(variants, 2, "3".repeat(32)); | |
| assert.equal(value(variants), originalOutput); | |
| assert.match(byClass(variants, "h3cm-preview").src, /hq.mp4/); | |
| byText(variants, "Pixel Upscale · 1").click(); | |
| select(variants, 2, "4".repeat(32)); | |
| assert.ok(elements(variants).some(item => item.textContent === "original unavailable or source mismatch")); | |
| assert.equal(value(variants), originalOutput); | |
| assert.equal(byClass(variants, "h3cm-output-summary").textContent, originalSummary, | |
| "Pixel Upscale tab browsing cannot claim to send a processed source"); | |
| byText(reopenedVariant, "Original · 4").click(); | |
| assert.equal(value(reopenedVariant), originalOutput); | |
| assert.equal(mutations, mutationsBefore, "no processing-tab operation mutates the project"); | |
| byText(reopenedVariant, "DeRoPE · 2").click(); | |
| currentGraph.processing_variants = currentGraph.processing_variants.filter(item => item.key !== "demo/motion/two"); | |
| reopenedVariant._h3CheckpointManagerRefresh(); | |
| await settle(); | |
| assert.equal(byClass(reopenedVariant, "h3cm-preview").src, undefined, | |
| "a removed explicitly browsed take does not become another saved take on refresh"); | |
| assert.match(byClass(reopenedVariant, "h3cm-prompt").textContent, /previously browsed processing take is unavailable/); | |
| assert.equal(value(reopenedVariant), originalOutput); | |
| console.log("Checkpoint processing tabs: retained takes, previews, missing versions, saved view, and output isolation pass"); | |
| // Processing deletion uses its own endpoint, confirmation and immutable path. | |
| processingDeletion = true; | |
| const deleting = makeNode(); await settle(); | |
| select(deleting, 2, b); await settle(); | |
| byText(deleting, "Latent Upscale · 1").click(); await settle(); | |
| select(deleting, 2, "3".repeat(32)); await settle(); | |
| const outputBeforeDelete = value(deleting), originalsBeforeDelete = JSON.stringify(currentGraph.revisions); | |
| assert.equal(byText(deleting, "Delete processed version").disabled, false); | |
| const beforeCancel = mutations; | |
| confirms = false; | |
| byText(deleting, "Delete processed version").click(); await settle(); | |
| assert.equal(mutations, beforeCancel, "cancel does not send a deletion request"); | |
| confirms = true; deleteConflict = true; | |
| byText(deleting, "Delete processed version").click(); await settle(); | |
| assert.match(byClass(deleting, "h3cm-status").textContent, /Preview changed/); | |
| assert.equal(byText(deleting, "Delete processed version").disabled, true); | |
| assert.ok(currentGraph.processing_variants.some(v => v.key === "demo/hq/one")); | |
| deleteConflict = false; | |
| select(deleting, 2, "3".repeat(32)); await settle(); | |
| byText(deleting, "Delete processed version").click(); await settle(); | |
| assert.ok(!currentGraph.processing_variants.some(v => v.key === "demo/hq/one")); | |
| assert.equal(JSON.stringify(currentGraph.revisions), originalsBeforeDelete); | |
| assert.equal(value(deleting), outputBeforeDelete); | |
| assert.equal(byClass(deleting, "h3cm-preview").src, undefined); | |
| assert.equal(byText(deleting, "Delete processed version").disabled, true); | |
| assert.match(byClass(deleting, "h3cm-status").textContent, /Reclaimed/); | |
| // Orphaned versions must also be deletable (no original selection exists). | |
| byText(deleting, "Pixel Upscale · 1").click(); await settle(); | |
| select(deleting, 2, "4".repeat(32)); await settle(); | |
| assert.equal(byText(deleting, "Delete processed version").disabled, false); | |
| byText(deleting, "Delete processed version").click(); await settle(); | |
| assert.ok(!currentGraph.processing_variants.some(v => v.key === "demo/pixel/orphan")); | |
| // Sequence-order history must not force the user to delete independent later clips. | |
| const independentPixel = (scene, digit) => ({key:`demo/pixel/${scene}`, scene, | |
| revision:digit.repeat(32), stage:"pixel_upscale", profile:"pixel", | |
| profile_path:"demo/upscaled/pixel", originals:[{scene, revision:scene === 2 ? b : c}], | |
| ready:true, latent_saved:false, context_steps:0, video:{filename:`pixel-${scene}.mp4`}}); | |
| currentGraph.processing_variants.push(independentPixel(2, "5"), independentPixel(3, "6")); | |
| const laterPixelBeforeDelete = JSON.stringify(currentGraph.processing_variants.at(-1)); | |
| retainedPixelTakes = [{scene:3, revision:"6".repeat(32), metadata_path:"demo/pixel/3"}]; | |
| deleting._h3CheckpointManagerRefresh(); await settle(); | |
| select(deleting, 2, "5".repeat(32)); await settle(); | |
| assert.ok(elements(deleting).some(item => /Independent pixel takes kept: Scene 3 · 66666666/.test(item.textContent))); | |
| byText(deleting, "Delete processed version").click(); await settle(); | |
| assert.match(confirmations.at(-1), /Later independent pixel clips are kept/); | |
| assert.ok(!currentGraph.processing_variants.some(v => v.key === "demo/pixel/2")); | |
| assert.equal(JSON.stringify(currentGraph.processing_variants.find(v => v.key === "demo/pixel/3")), laterPixelBeforeDelete); | |
| assert.equal(JSON.stringify(currentGraph.revisions), originalsBeforeDelete); | |
| assert.equal(value(deleting), outputBeforeDelete); | |
| retainedPixelTakes = []; | |
| // A slow processing preview arriving after switching tabs cannot enable original deletion. | |
| let releasePreview; | |
| delayedProcessingPreview = new Promise(resolve => { releasePreview = resolve; }); | |
| byText(deleting, "DeRoPE · 1").click(); await settle(); | |
| byText(deleting, "Original · 4").click(); await settle(); | |
| releasePreview(); await settle(); | |
| assert.ok(byText(deleting, "Delete selected revision").disabled); | |
| assert.equal(value(deleting), outputBeforeDelete); | |
| processingDeletion = false; | |
| console.log("Processing deletion UI: preview, cancel, conflict, success, orphan/independent pixel cleanup, stale response and original/output isolation pass"); | |
| // Retirement is separate from media deletion and preserves the output selection. | |
| currentGraph = structuredClone(payload); | |
| snapshotRetirement = true; | |
| const retiring = makeNode(local); await settle(); | |
| select(retiring, 2, c); await settle(); | |
| const retireLabel = "Retire Chapter 1 snapshot aaaaaaaa…"; | |
| const retirementPin = value(retiring), retirementGraph = JSON.stringify(currentGraph); | |
| assert.equal(byText(retiring, "Delete selected revision").disabled, true); | |
| const beforeRetirementCancel = mutations; | |
| confirms = false; | |
| byText(retiring, retireLabel).click(); await settle(); | |
| assert.equal(mutations, beforeRetirementCancel); | |
| assert.equal(snapshotRetired, false); | |
| assert.match(confirmations.at(-1), /Scene 2 · cccccccc/); | |
| assert.match(confirmations.at(-1), /Deleting its inputs later makes full recovery unavailable/); | |
| assert.match(byClass(retiring, "h3cm-status").textContent, /cancelled/); | |
| confirms = true; | |
| for (const code of [423, 409]) { | |
| retirementError = code; | |
| byText(retiring, retireLabel).click(); await settle(); | |
| assert.equal(snapshotRetired, false); | |
| assert.equal(byText(retiring, "Delete selected revision").disabled, true); | |
| assert.match(byClass(retiring, "h3cm-status").textContent, code === 423 ? /read only/ : /preview changed/); | |
| } | |
| retirementError = 0; | |
| let releaseRetirement; | |
| delayedRetirementPreview = new Promise(resolve => { releaseRetirement = resolve; }); | |
| const beforeSlowRetirement = mutations, beforeSlowConfirmation = confirmations.length; | |
| byText(retiring, retireLabel).click(); await settle(); | |
| select(retiring, 3, d); await settle(); | |
| releaseRetirement(); await settle(); | |
| assert.equal(mutations, beforeSlowRetirement, "slow retirement preview cannot apply to another selection"); | |
| assert.equal(confirmations.length, beforeSlowConfirmation); | |
| select(retiring, 2, c); await settle(); | |
| byText(retiring, retireLabel).click(); await settle(); | |
| assert.equal(snapshotRetired, true); | |
| assert.equal(byText(retiring, retireLabel), undefined); | |
| assert.equal(byText(retiring, "Delete selected revision").disabled, false); | |
| assert.equal(JSON.stringify(currentGraph), retirementGraph, "retirement never deletes clips or changes active pointers"); | |
| assert.equal(value(retiring), retirementPin, "retirement never moves a workflow-local pin"); | |
| assert.match(byClass(retiring, "h3cm-status").textContent, /Snapshot retired/); | |
| snapshotRetirement = false; | |
| console.log("Chapter retirement UI: preview, cancel, ownership, conflict, stale response and output/media isolation pass"); | |
| // Reproduce the reported Chapter 2 pin at scene 8, shared with another branch. | |
| currentGraph = structuredClone(payload); | |
| currentGraph.revisions.forEach(item => { | |
| item.scene += 7; | |
| if (item.parent) item.parent.scene += 7; | |
| }); | |
| currentGraph.scenes = [8, 9, 10].map(scene => ({scene})); | |
| currentGraph.branches.forEach(branch => branch.path.forEach(item => item.scene += 7)); | |
| // Earlier chapters are immutable timing metadata in a chapter-only selection. | |
| const previous = Array.from({length:7}, (_, index) => ({ | |
| scene:index + 1, revision:String(index + 1).repeat(32), active:true, ready:false, | |
| })); | |
| currentGraph.revisions.unshift(...previous); | |
| currentGraph.editorial = {chapters:[{id:"two", title:"Chapter 2", start_scene:8}]}; | |
| const partialPin = core.checkpointLocalSelectionJson(currentGraph, "demo", | |
| currentGraph.revisions[7], {start:8, end:10}, "chapter"); | |
| const repair = makeNode(partialPin); | |
| await settle(); | |
| assert.equal(value(repair), partialPin, "never guess which descendant of an old shared pin was intended"); | |
| assert.match(byClass(repair, "h3cm-output-summary").textContent, /old partial selection/); | |
| const activeHeading = elements(repair).find(item => item.className.includes("h3cm-branch-head") | |
| && item.children.some(child => child.textContent === "Project active branch")); | |
| activeHeading.click(); await settle(); | |
| select(repair, 8, a); await settle(); | |
| byText(repair, "Use branch locally").click(); | |
| const fullPin = value(repair); | |
| assert.deepEqual(JSON.parse(fullPin).lineage.slice(-3).map(item => item.scene), [8, 9, 10]); | |
| assert.equal(JSON.parse(fullPin).lineage.at(-1).revision, d); | |
| assert.equal(JSON.parse(fullPin).scope_start_scene, 8); | |
| assert.equal(JSON.parse(fullPin).output_scope, "chapter"); | |
| assert.match(byClass(repair, "h3cm-output-summary").textContent, /scenes 8–10 \(3 clips; selected chapter only\)/); | |
| select(repair, 9, b); await settle(); | |
| assert.equal(value(repair), fullPin); | |
| repair._h3CheckpointManagerRefresh(); await settle(); | |
| assert.equal(value(repair), fullPin, "full branch remains selected through previews and refresh"); | |
| assert.equal(core.checkpointOutputBranchTip(currentGraph, currentGraph.revisions[7], {start:8, end:10}), null, | |
| "a shared ancestor alone cannot decide between two descendant branches"); | |
| // Selecting a branch header explicitly also chooses full output in follow mode. | |
| const following = makeNode(); await settle(); | |
| const alternateHeading = elements(following).find(item => item.className.includes("h3cm-branch-head") | |
| && item.children.some(child => child.textContent === "Branch cccccccc")); | |
| alternateHeading.click(); await settle(); | |
| assert.equal(JSON.parse(value(following)).lineage.at(-1).revision, c); | |
| select(following, 8, a); await settle(); | |
| assert.equal(JSON.parse(value(following)).lineage.at(-1).revision, c); | |
| console.log("Checkpoint branch range: previews never trim output; explicit full-branch selection repairs legacy scene-8 pins"); | |