Files
DemonGatanjieu-Anomalous_Mo…/tests/mcp_run.mjs
T
DemonGatanjieuandClaude Opus 5.5 0740e7c5c8 Give each try of a batch its own seed
run_workflow's batch queued the same prompt N times when the sampler's
seed was held "fixed": ComfyUI ran it once and the user had one picture
to pick from. While a batch is queued a fixed seed counts up instead
(the native control moves it per prompt); afterwards the control and the
seed are as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:36:47 +08:00

138 lines
6.6 KiB
JavaScript

import assert from 'node:assert/strict';
import { queueTracked, varySeeds } from '../web/modules/mcp_run.js';
// Only the host is substituted: its queue drains batches, emits request IDs, and calls widgets around submissions.
function host({ failAt = 0, manual = false, refuse = null } = {}) {
const api = new EventTarget();
const events = (name, detail) => api.dispatchEvent(Object.assign(new Event(name), { detail }));
let count = 0;
const hooks = [];
api.queuePrompt = async () => {
const n = ++count;
await Promise.resolve();
if (n === failAt) throw Object.assign(new Error('invalid graph'), refuse ? { response: refuse, toString: () => 'Prompt outputs failed validation' } : {});
return { prompt_id: `p${n}` };
};
const app = {
nextQueueRequestId: 0, processingQueue: false, queueItems: [],
async queuePrompt(number, batch) {
const requestId = this.nextQueueRequestId++;
this.queueItems.push({ number, batch, requestId });
events('promptQueueing', { requestId });
if (this.processingQueue) return false;
this.processingQueue = true;
try {
while (this.queueItems.length) {
const item = this.queueItems.pop();
let accepted = 0;
for (let i = 0; i < item.batch; i++) {
hooks.push('before');
try {
const result = await api.queuePrompt(item.number, {});
assert.ok(result.prompt_id);
if (manual && count === 1) void this.queuePrompt(0, 1);
} catch { break; }
accepted++;
hooks.push('after');
}
if (accepted) events('promptQueued', { requestId: item.requestId });
}
} finally { this.processingQueue = false; }
return true;
},
};
return { app, api, hooks };
}
for (const manual of [false, true]) {
const { app, api, hooks } = host({ manual });
const original = api.queuePrompt;
const result = await queueTracked(app, api, 2);
assert.deepEqual(result.prompt_ids, ['p1', 'p2']);
assert.equal(result.queued, 2);
assert.equal(api.queuePrompt, original);
await new Promise(setImmediate); // a later manual batch may still be draining
assert.deepEqual(hooks, Array(manual ? 3 : 2).fill(['before', 'after']).flat());
}
{
const { app, api } = host({ manual: true });
const submit = api.queuePrompt;
let release;
const held = new Promise(resolve => { release = resolve; });
let count = 0;
api.queuePrompt = async function (...args) {
if (++count === 3) await held;
return submit.apply(this, args);
};
const result = await queueTracked(app, api, 2);
assert.deepEqual(result.prompt_ids, ['p1', 'p2']);
assert.equal(app.processingQueue, true, 'AI receipt does not wait for the manual batch behind it');
release();
await new Promise(setImmediate);
}
{
const { app, api } = host({ failAt: 2, manual: true });
const result = await queueTracked(app, api, 3);
assert.deepEqual(result.prompt_ids, ['p1'], 'manual submission after a partial failure is not captured');
assert.match(result.submission_error, /invalid graph/);
}
{
const { app, api } = host({ failAt: 1 });
assert.equal((await queueTracked(app, api)).queued, 0);
const result = await queueTracked(app, api);
assert.deepEqual(result.prompt_ids, ['p2'], 'failed capture leaves no listeners or wrapper behind');
}
{
const { app, api } = host();
const first = queueTracked(app, api, 2);
await assert.rejects(queueTracked(app, api), /another batch/);
assert.deepEqual((await first).prompt_ids, ['p1', 'p2']);
delete app.nextQueueRequestId;
await assert.rejects(queueTracked(app, api), /Update the frontend/);
}
{
// ComfyUI's refusal goes on as data too, for the server to say what to do next.
const refuse = { error: { type: 'prompt_outputs_failed_validation', message: 'Prompt outputs failed validation', details: '' },
node_errors: { 3: { class_type: 'KSampler', errors: [{ type: 'required_input_missing', message: 'Required input is missing', details: 'model', extra_info: { input_name: 'model' } }] } } };
const result = await queueTracked(...Object.values(host({ failAt: 1, refuse })).slice(0, 2));
assert.equal(result.queued, 0);
assert.equal(result.refusal.type, 'prompt_outputs_failed_validation');
assert.deepEqual(result.refusal.nodes, [{ node: '3', class_type: 'KSampler', type: 'required_input_missing', message: 'Required input is missing',
details: 'model', input: 'model', value: undefined }]);
}
console.log('mcp_run: exact receipts, manual interleaving, partial failure, busy guard and cleanup passed');
// A batch of tries with a seed held fixed: it counts up while queued (N pictures, not one), then is as it was.
{
const seedNode = (control, value = 7) => {
const seed = { name: 'seed', value };
seed.linkedWidgets = [{ name: 'control_after_generate', value: control }];
return { widgets: [seed, { name: 'steps', value: 20 }] };
};
const held = seedNode('fixed');
const random = seedNode('randomize');
const { app, api } = host();
app.graph = { _nodes: [held, random] };
const seen = [];
const submit = api.queuePrompt;
api.queuePrompt = async (...args) => {
seen.push(held.widgets[0].linkedWidgets[0].value);
held.widgets[0].value += 1; // what the native 'increment' control does after each prompt
return submit(...args);
};
await queueTracked(app, api, 3);
assert.deepEqual(seen, ['increment', 'increment', 'increment']);
assert.deepEqual([held.widgets[0].value, held.widgets[0].linkedWidgets[0].value], [7, 'fixed'], 'the canvas keeps its seed');
assert.equal(random.widgets[0].linkedWidgets[0].value, 'randomize', 'a seed that changes anyway is left alone');
const undo = varySeeds({ _nodes: [held] });
assert.equal(held.widgets[0].linkedWidgets[0].value, 'increment');
undo();
assert.equal(held.widgets[0].linkedWidgets[0].value, 'fixed');
const single = host();
single.app.graph = { _nodes: [held] };
single.api.queuePrompt = async (...args) => { seen.push(held.widgets[0].linkedWidgets[0].value); return { prompt_id: 'one' }; };
await queueTracked(single.app, single.api, 1);
assert.equal(seen.at(-1), 'fixed', 'one run keeps the seed it was given');
}
console.log('mcp_run: seeds ok');