Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6ef2cb9
feat(e2e): add per-family end-to-end regression runner
Pfannkuchensack Oct 17, 2026
92c293e
Merge branch 'main' into feat/family-e2e
Pfannkuchensack Oct 17, 2026
2f8d0c2
fix(e2e): snapshot the template database through the database layer
Pfannkuchensack Oct 17, 2026
4c98e3c
Merge remote-tracking branch 'origin/feat/family-e2e' into feat/famil…
Pfannkuchensack Oct 17, 2026
727cbbd
Merge branch 'main' into feat/family-e2e
Pfannkuchensack Oct 18, 2026
0d96e0a
fix(e2e): set the new project settings in exported graphs
Pfannkuchensack Oct 18, 2026
4de82a9
Merge remote-tracking branch 'upstream/main' into feat/family-e2e
Pfannkuchensack Oct 20, 2026
1227de3
test(e2e): pick a Z-Image-valid encoder in the graph export test
Pfannkuchensack Oct 20, 2026
afc05b3
feat(e2e): bundle runs as shareable zips and record model identity
Pfannkuchensack Oct 21, 2026
a91a6a8
feat(e2e): describe bundle format and read only same-profile runs
Pfannkuchensack Oct 21, 2026
8b5fe87
test(e2e): place the fake memory peak inside the measured run window
Pfannkuchensack Oct 21, 2026
cbbb998
feat(e2e): mark expected changes and report self-referenced baselines
Pfannkuchensack Oct 21, 2026
eb6f45d
feat(e2e): ship each case's reference metadata in bundles
Pfannkuchensack Oct 21, 2026
7efc2d2
Merge branch 'main' into feat/family-e2e
Pfannkuchensack Oct 21, 2026
6c9b69b
Merge branch 'main' into feat/family-e2e
Pfannkuchensack Oct 22, 2026
b2a02cc
feat(e2e): sample GPU memory through NVML on Linux NVIDIA
Pfannkuchensack Oct 22, 2026
8d997fc
fix(e2e): count weightless tokenizer loads as resident
Pfannkuchensack Oct 22, 2026
a43c2ba
fix(e2e): redact every credential value and kill orphaned server proc…
Pfannkuchensack Oct 23, 2026
ac37f77
fix(e2e): check server liveness without os.waitid on macOS
Pfannkuchensack Oct 23, 2026
591d190
fix(e2e): treat a refused group kill as done when only zombies remain
Pfannkuchensack Oct 23, 2026
b6f94b7
fix(e2e): stop a server a killed runner left before resuming its run
Pfannkuchensack Oct 24, 2026
4a8dea8
Merge branch 'main' into feat/family-e2e
Pfannkuchensack Oct 26, 2026
c3b7533
Merge remote-tracking branch 'upstream/main' into feat/family-e2e
Pfannkuchensack Oct 27, 2026
e558c51
fix(family-e2e): type exported reference images like the Reference Im…
Pfannkuchensack Oct 27, 2026
d492e4b
fix(family-e2e): close the race between start, stop and resume
Pfannkuchensack Oct 27, 2026
279b062
fix(family-e2e): read the run status under the run lock
Pfannkuchensack Oct 27, 2026
1d96951
Merge branch 'main' into feat/family-e2e
Pfannkuchensack Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Main agent also owns machine-wide resources: dependency installs, generated API
- `-n logical` fits CI runners that own the machine. Dev box: cap workers (`-n 4`, or plain `pytest`). Set `OMP_NUM_THREADS` when a run must stay light. Never start a suite during a generation.
- `uv run --no-sync mypy scripts/invokeai-web.py` uses current exclusions. Optional diagnostic only: not a CI gate, not full type coverage.
- Pick checks by behavior. Run required milestone gates before review. Rerun unchanged checks only with a reason.
- Family E2E runs (`scripts/family_e2e`, guide: `docs/src/content/docs/development/Guides/family-e2e.mdx`) after changes to what a model computes or how it loads: model-manager loading, probing, configs; quantization; attention; model cache, working memory; invocations that run models; webv2 graph builders. Offer a run of the affected families, never start one unasked; none for docs-, test- or UI-only changes. Offer only where a manifest is set up; otherwise name the missing run under open limits in the final report. After a yes: run the base commit and `accept` it as reference (its bundle then reports BASELINE), then the branch, with `expect: changed` on the cases the PR is meant to change. PR description gets the `summary.md` table; attach the `bundle` zip. Until A/B runs exist, time differences under about 10 % between sessions are noise.
- Once test dependencies exist, install hooks per checkout: `uv run --no-sync pre-commit install`. Committed config does not install hooks.

## Scratch files and repository hygiene
Expand Down
181 changes: 181 additions & 0 deletions docs/src/content/docs/development/Guides/family-e2e.mdx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion invokeai/frontend/webv2/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Feature interfaces stay ordinary module interfaces unless two adapters add lever
| Nodes | Custom-node catalog/templates and execution lifecycle; Workflow editor and node manager | Platform; descriptors, catalog commands, execution snapshots and manager contribution |
| Queue | Backend queue state, realtime interpretation, submission/reconciliation and Queue UI; App runtime, Workbench aggregate/shell, Generation/Gallery/Workflow | Platform and public envelopes/capabilities from Identity, Models, Generation, Workflow, Upscale; backend port, queue reads/commands and widget contribution |
| Gallery | Image collection, selection, board policy and Gallery UI; App adapter, Generation, Workflow, Upscale, Models, Workbench image actions | Platform and Queue's public progress reads; gallery reads/commands and widget contribution |
| Generation | Generation settings, validation, graph compilation and Generate UI; Workbench invocation, Queue, Gallery, Workflow, Upscale | Platform and public Models/Gallery/Queue interfaces; settings/validation, compiled submission envelope and widget contribution |
| Generation | Generation settings, validation, graph compilation and Generate UI; Workbench invocation, Queue, Gallery, Workflow, Upscale | Platform and public Models/Gallery/Queue interfaces; settings/validation, compiled submission envelope, offline graph export and widget contribution |
| Workflow | Workflow document, validation, compilation, preview/editor/library and explicit publication; App adapter, Workbench invocation, Nodes | Platform and public Nodes/Models/Gallery/Queue/Generation interfaces; document reads/commands, compiled submission envelope and widget contribution |
| Upscale | Upscale settings, validation, compilation and UI; Workbench invocation and Queue | Platform and public Models/Gallery/Queue interfaces; value/validation reads, submission command and widget contribution |
| Intermediates | Per-project intermediates read models, cleanup previews/operations and the Settings section that manages them; Workbench settings, project menus, account administration | Platform plus Identity's public session/capability reads; query keys, read models, the focus intent, the browser hold lease and the `settingsContribution` metadata whose loader owns the section body |
3 changes: 2 additions & 1 deletion invokeai/frontend/webv2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"fix": "pnpm run lint:oxc:fix && pnpm run format:write",
"format:check": "oxfmt --check .",
"format:write": "oxfmt --write .",
"graphs:export": "node scripts/export-graphs.mjs",
"i18n:report": "node scripts/i18n-report.mjs",
"i18n:report:verbose": "node scripts/i18n-report.mjs --verbose",
"lint": "pnpm run lint:code && pnpm run architecture:check",
Expand All @@ -35,7 +36,7 @@
"test:all": "pnpm run test && pnpm run test:browser && pnpm run test:fixtures",
"test:browser": "vitest run --config vitest.browser.config.mts",
"test:browser:update-goldens": "CANVAS_GOLDEN_UPDATE=1 vitest run --config vitest.browser.config.mts src/workbench/canvas-engine/render/golden",
"test:fixtures": "node --test scripts/mock-backend-fixtures.test.mjs scripts/preview-server.test.mjs scripts/run-project-file-journey.test.mjs scripts/locale-assets-plugin.test.mjs",
"test:fixtures": "node --test scripts/mock-backend-fixtures.test.mjs scripts/preview-server.test.mjs scripts/run-project-file-journey.test.mjs scripts/locale-assets-plugin.test.mjs scripts/export-graphs.test.mjs",
"test:performance:architecture": "pnpm run test:performance:build && pnpm run test:performance:browser",
"test:performance:browser": "node scripts/measure-architecture-performance.mjs",
"test:performance:browser:profile": "node scripts/measure-architecture-performance.mjs --profile",
Expand Down
80 changes: 80 additions & 0 deletions invokeai/frontend/webv2/scripts/export-graphs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* `pnpm graphs:export <request.json> <response.json>` compiles offline export cases into the enqueue batches the UI
* would send (src/app/graphExport.ts). Vite's module runner loads the TypeScript sources with the app's aliases and
* opens no port. Exit 0 once the response is written, with per-case errors inside it; exit 2 for an unreadable or
* malformed request, with no response written.
*/

import { readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { loadConfigFromFile, runnerImport } from 'vite';

const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const EXIT_MALFORMED_REQUEST = 2;

const rejectRequest = (message) => {
process.stderr.write(`graphs:export: ${message}\n`);
process.exitCode = EXIT_MALFORMED_REQUEST;
};

const readRequest = async (path) => {
try {
return JSON.parse(await readFile(path, 'utf8'));
} catch (error) {
rejectRequest(`cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
};

/** Only the aliases and defines: plugins transform the browser bundle, not these pure modules. */
const loadExporter = async () => {
const { config } = await loadConfigFromFile(
{ command: 'serve', mode: 'development' },
resolve(PACKAGE_ROOT, 'vite.config.mts'),
PACKAGE_ROOT,
'error'
);
const { module } = await runnerImport(resolve(PACKAGE_ROOT, 'src/app/graphExport.ts'), {
define: config.define,
logLevel: 'error',
resolve: { alias: config.resolve.alias },
root: PACKAGE_ROOT,
});

return module;
};

const main = async () => {
const [requestPath, responsePath, ...extra] = process.argv.slice(2);

if (!requestPath || !responsePath || extra.length > 0) {
rejectRequest('usage: pnpm graphs:export <request.json> <response.json>');
return;
}

const request = await readRequest(requestPath);

if (request === undefined) {
return;
}

const { exportGraphs, GraphExportRequestError } = await loadExporter();
let response;

try {
response = exportGraphs(request);
} catch (error) {
if (error instanceof GraphExportRequestError) {
rejectRequest(`malformed request: ${error.message}`);
return;
}

throw error;
}

await writeFile(responsePath, `${JSON.stringify(response, null, 2)}\n`);
};

await main();
74 changes: 74 additions & 0 deletions invokeai/frontend/webv2/scripts/export-graphs.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, resolve } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';

const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const CLI = resolve(PACKAGE_ROOT, 'scripts/export-graphs.mjs');
const CAPABILITIES = resolve(PACKAGE_ROOT, 'src/features/generation/core/__fixtures__/architectureCapabilities.json');

const runCli = async (context, requestText) => {
const directory = await mkdtemp(resolve(tmpdir(), 'invokeai-graphs-export-'));
context.after(() => rm(directory, { force: true, recursive: true }));
const requestPath = resolve(directory, 'request.json');
const responsePath = resolve(directory, 'response.json');
await writeFile(requestPath, requestText);

const { code, stderr } = await promisify(execFile)(process.execPath, [CLI, requestPath, responsePath], {
cwd: PACKAGE_ROOT,
}).then(
({ stderr }) => ({ code: 0, stderr }),
(error) => ({ code: error.code, stderr: error.stderr })
);
const response = await access(responsePath).then(
async () => JSON.parse(await readFile(responsePath, 'utf8')),
() => null
);

return { code, response, stderr };
};

test('writes per-case results through the real module graph and exits 0', async (context) => {
const capabilities = JSON.parse(await readFile(CAPABILITIES, 'utf8'));
const sdxl = { base: 'sdxl', format: 'diffusers', hash: 'blake3:sdxl', key: 'sdxl', name: 'SDXL', type: 'main' };
const testCase = { height: 1024, mode: 'txt2img', positive_prompt: 'a fox', seed: 7, steps: 4, width: 1024 };
const request = {
capabilities,
cases: [
{ ...testCase, id: 'ok', model_key: 'sdxl' },
{ ...testCase, id: 'missing', model_key: 'missing' },
],
models: [sdxl],
version: 1,
};

const { code, response } = await runCli(context, JSON.stringify(request));

assert.equal(code, 0);
assert.equal(response.version, 1);
assert.deepEqual(
response.cases.map((result) => [result.id, 'batch' in result]),
[
['ok', true],
['missing', false],
]
);
assert.match(response.cases[1].error, /model_key "missing"/);
});

for (const [label, requestText] of [
['unparsable JSON', '{ "version": 1,'],
['a malformed request', JSON.stringify({ cases: [], models: [], version: 1 })],
]) {
test(`rejects ${label} with exit 2 and writes no response`, async (context) => {
const { code, response, stderr } = await runCli(context, requestText);

assert.equal(code, 2);
assert.equal(response, null);
assert.match(stderr, /^graphs:export: /);
});
}
Loading
Loading