test(ingestion): real-host bare-output re-check and the review-round contract
A metadata-disabled save, held open behind the fixture gate after SaveImage, waits unlinked while its run is open and links once the run's outputs are recorded. The fixture's monitor route lists the waiting files. TESTING.md covers the history re-check, the read index and what the repair tool's --apply does.
This commit is contained in:
+48
-12
@@ -3869,7 +3869,9 @@ fallback (`_get_fallback_prompt` is gone) and no unproven tracker entry.
|
||||
counts and tracks nothing for the positive side, logs at info (no "Could not
|
||||
save or track" warning), and marks its run key empty, so no earlier entry of
|
||||
that key can be used and a batch item keeps its queue place
|
||||
(`PromptTracker.mark_empty`). The queue hook counts nothing for it either.
|
||||
(`PromptTracker.mark_empty`). The queue hook counts nothing for it either. A
|
||||
save or tracking failure of a positive node marks its run key the same way, so
|
||||
an older entry of that key never stands in for the failed run.
|
||||
- **Run keys** (`utils/run_keys.py`): sha256 of a prompt node plus its whole
|
||||
upstream closure (`class_type` and `inputs`) in the API graph. Nodes register
|
||||
tracker entries under it; the monitor computes it from the image's embedded
|
||||
@@ -3881,14 +3883,36 @@ fallback (`_get_fallback_prompt` is gone) and no unproven tracker entry.
|
||||
snapshot's entry itself is used only when its run id is known
|
||||
(`comfy_execution.utils.get_executing_context`) and that run registered one
|
||||
entry for the key, which covers a cached re-run (sampler seed changed) without
|
||||
applying a batch's last item to every image. More than one positive
|
||||
PromptManager node in the image graph: never linked from the tracker.
|
||||
applying a batch's last item to every image. When the key's latest entry is a
|
||||
placeholder the image stays unlinked without consulting history (the same
|
||||
rule as a popped placeholder). More than one positive PromptManager node in
|
||||
the image graph, or any positive node without a key: never linked from the
|
||||
tracker. Links are followed exactly as ComfyUI's `is_link` defines them (a
|
||||
`str` node id and an `int` or `float` slot in a two-item list). Equal keys do
|
||||
not always mean equal text: the key can't see `IS_CHANGED`, so an upstream
|
||||
node that forces re-runs gives two runs one key; that is why the run named by
|
||||
the snapshot is required as well. A file event delivered after the next run
|
||||
already re-registered the key is the one remaining exposure (see the ledger).
|
||||
- **Recorded history** (`database/run_attribution.py`): a file is attributed when
|
||||
exactly one recorded run reported its output location (root key, subfolder,
|
||||
name), its mtime lies within that run (dequeue to terminal or callback, +-2 s),
|
||||
and every positive context names the same stored prompt (the live
|
||||
`positive_prompt_id` hint). The monitor tries it last; the scan tries it when the
|
||||
#170 browser-parser floor finds nothing, and never creates a prompt from it.
|
||||
#170 browser-parser floor finds nothing, and also for files with no prompt
|
||||
metadata at all (`--disable-metadata`, savers that embed none); it never
|
||||
creates a prompt. The lookup uses `idx_history_read_execution_run_outputs`
|
||||
(`filename, subfolder, root_key, path_style`), added by
|
||||
`migrate_execution_history`; the output schema check accepts exactly that
|
||||
optional index and still refuses any other. An older build refuses a library
|
||||
that has it, so downgrading after this change needs `DROP INDEX
|
||||
idx_history_read_execution_run_outputs` first.
|
||||
- **After the run ends.** An image with no run keys (no embedded API graph) that
|
||||
nothing identifies waits for its run: when the run's outputs are recorded
|
||||
(`write_execution_outputs_for_finish` notifies `subscribe_outputs_sealed`
|
||||
listeners after commit), each waiting image the run reported gets exactly one
|
||||
history-only re-check on the monitor's worker. At most 1000 images wait, each
|
||||
for at most an hour; shutdown forgets them. The image is registered before the
|
||||
first history check, so a run sealed in between is not missed.
|
||||
- **Blank positive.** A resolved pair whose stored positive is empty (the #342
|
||||
image with its Pair execution record) is `unresolved` for the scan and for
|
||||
`ingest_resolved_pair` (it used to raise in `_save_pair`).
|
||||
@@ -3901,23 +3925,35 @@ Tests: `tests/test_prompt_attribution.py` (nodes, tracker, monitor, run keys),
|
||||
`tests/comfyui/image-attribution.spec.mjs` (both renderers): a typed Pair image
|
||||
links, a Pair image whose positive is only linked prepend text from an unknown
|
||||
node stays unlinked with nothing stored, an unknown node feeding `text` links
|
||||
through its run key, and its cached re-run with a new sampler seed links again.
|
||||
The managed host config disables monitoring, so that case starts and stops the
|
||||
plugin's monitor through the owned fixture route `/pm_execution_fixture/monitor`.
|
||||
through its run key, and its cached re-run with a new sampler seed links again
|
||||
(the host's `execution_cached` event names the PromptManager node, under the
|
||||
fixture's Classic cache). A second case saves with metadata disabled and holds
|
||||
the run open behind the fixture gate after SaveImage: the output is waiting and
|
||||
unlinked while the run is open, and links to the stored prompt once the run's
|
||||
outputs are recorded. The managed host config disables monitoring, so these
|
||||
cases start and stop the plugin's monitor through the owned fixture route
|
||||
`/pm_execution_fixture/monitor` (its GET lists the waiting files).
|
||||
On origin/main the prepend image was linked to an unrelated prompt.
|
||||
|
||||
Repair: `tools/review_image_links.py <db> --output-dir <ComfyUI output> --ids
|
||||
<ids>` (or `--prompt <id>`) judges links by these rules (`keep`, `relink`,
|
||||
`unlink`, `conflict`, `missing`) on a backup copy and changes nothing.
|
||||
`--apply` writes `relink`/`unlink` only for ids named with `--ids`, after saving
|
||||
`<db>.pre-relink_<stamp>`. Queue-linked batch images can't be proven after the
|
||||
fact and read `unlink`, so review before applying. Stop ComfyUI first.
|
||||
`<db>.pre-relink_<stamp>`, and only while each image is still linked to the
|
||||
prompt it was judged on (otherwise it is counted as skipped). `relink` points the
|
||||
link record at the other prompt and drops its pair links. `unlink` removes only
|
||||
the image's link record (its `generated_images` row, the metadata snapshot stored
|
||||
in it and its pair links): that is how the app represents an unlinked image, the
|
||||
same as a file the scan could not attribute, so the file stays on disk and in the
|
||||
gallery as unlinked, and its favorite and tags (kept by file) are untouched. Every
|
||||
run prints this. Queue-linked batch images can't be proven after the fact and read
|
||||
`unlink`, so review before applying. Stop ComfyUI first.
|
||||
|
||||
## Durable execution ledger
|
||||
|
||||
`npm run --prefix tools/vendor test:executions` runs 50 real execution cases
|
||||
(25 each in Classic and Nodes 2.0, retaining all 48 prior execution cases and adding
|
||||
the #342 image-attribution case),
|
||||
`npm run --prefix tools/vendor test:executions` runs 52 real execution cases
|
||||
(26 each in Classic and Nodes 2.0, retaining all 48 prior execution cases and adding
|
||||
the two #342 image-attribution cases),
|
||||
alongside the existing 80 stock host cases (the original 78 plus the native
|
||||
editor host case) and six pack compatibility cases. Run
|
||||
these suites serially because they share report cleanup and owned host resources. Use the same pinned disposable `PM_COMFY_ROOT`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Deterministic nodes and queue controls installed only in an owned test host."""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -293,3 +294,12 @@ async def image_monitor(request: web.Request) -> web.Response:
|
||||
monitor.stop_monitoring()
|
||||
config.MONITORING_ENABLED = False
|
||||
return web.json_response({"monitoring": monitor.is_monitoring})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/pm_execution_fixture/monitor")
|
||||
async def image_monitor_state(request: web.Request) -> web.Response:
|
||||
"""File names of keyless images waiting for their run's recorded outputs."""
|
||||
monitor = _plugin_module("utils.image_monitor")._monitor_instance
|
||||
handler = getattr(monitor, "handler", None)
|
||||
waiting = handler.awaiting_history() if handler is not None else []
|
||||
return web.json_response({"awaiting": [os.path.basename(p) for p in waiting]})
|
||||
|
||||
@@ -108,3 +108,35 @@ test('an output whose positive is only linked prepend text stays unlinked, never
|
||||
expect(cached?.[1].nodes).toEqual(expect.arrayContaining(['11', '20']));
|
||||
await linkedTo(execution, rerun, llmText);
|
||||
});
|
||||
|
||||
test('an output saved without metadata links once its recorded run names a stored prompt', async ({ execution }, testInfo) => {
|
||||
const llmText = `bare output landscape ${testInfo.project.name} ${randomUUID()}`;
|
||||
const request = execution.request;
|
||||
expect((await request.post('/pm_execution_fixture/metadata', { data: { disabled: true } })).ok()).toBe(true);
|
||||
try {
|
||||
// No embedded graph: no run key can prove the tracker entry, so only the
|
||||
// run's recorded history can, and only once the run's outputs are sealed.
|
||||
// The gate after SaveImage holds the run open while the monitor looks.
|
||||
const prompt = graph({ kind: 'PromptManager', text: 'llm', llm: llmText, prefix: 'misslink/bare' });
|
||||
prompt['80'] = { class_type: 'PMHistoryImageOutput', inputs: { images: ['70', 0], mode: 'gate' } };
|
||||
expect((await request.post('/pm_execution_fixture/gate/hold')).ok()).toBe(true);
|
||||
const id = randomUUID();
|
||||
try {
|
||||
expect((await request.post('/prompt', { data: { prompt, prompt_id: id } })).ok()).toBe(true);
|
||||
let waiting = [];
|
||||
await expect.poll(async () => {
|
||||
waiting = (await (await request.get('/pm_execution_fixture/monitor')).json()).awaiting;
|
||||
return waiting.filter(name => name.startsWith('bare_')).length;
|
||||
}, { timeout: 20000, message: 'the monitor processed the bare output while its run was open' }).toBe(1);
|
||||
const [bare] = waiting.filter(name => name.startsWith('bare_'));
|
||||
expect((await readLinks(execution.databasePath, [bare])).links[bare]).toBeNull();
|
||||
expect((await request.post('/pm_execution_fixture/gate/release')).ok()).toBe(true);
|
||||
await linkedTo(execution, bare, llmText);
|
||||
expect((await (await request.get('/pm_execution_fixture/monitor')).json()).awaiting).not.toContain(bare);
|
||||
} finally {
|
||||
expect((await request.post('/pm_execution_fixture/gate/release')).ok()).toBe(true);
|
||||
}
|
||||
} finally {
|
||||
expect((await request.post('/pm_execution_fixture/metadata', { data: { disabled: false } })).ok()).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
Vendored
+2
-2
@@ -32,13 +32,13 @@ test('execution script discovers only the new matrix and preserves all 78 origin
|
||||
assert.match(executions, /\[classic-start\]/);
|
||||
assert.match(executions, /\[nodes2-start\]/);
|
||||
// + the fresh-host library location case, + the #342 image attribution case
|
||||
assert.match(executions, /Total: 50 tests in 4 files/);
|
||||
assert.match(executions, /Total: 52 tests in 4 files/);
|
||||
for (const project of ['classic-start', 'nodes2-start']) {
|
||||
const cases = executions.split('\n').filter(line => line.includes(`[${project}]`));
|
||||
assert.equal(cases.filter(line => line.includes('execution-history.spec.mjs')).length, 15);
|
||||
assert.equal(cases.filter(line => line.includes('execution-outputs.spec.mjs')).length, 8);
|
||||
assert.equal(cases.filter(line => line.includes('library-location.spec.mjs')).length, 1);
|
||||
assert.equal(cases.filter(line => line.includes('image-attribution.spec.mjs')).length, 1);
|
||||
assert.equal(cases.filter(line => line.includes('image-attribution.spec.mjs')).length, 2);
|
||||
}
|
||||
assert.doesNotMatch(executions, /prompt-manager.spec/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user