Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
174 changes: 174 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 @@ -104,7 +104,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 @@ -17,6 +17,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 format:check && pnpm run lint:oxc && pnpm run lint:tsc && pnpm run architecture:check",
Expand All @@ -32,7 +33,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:update-baseline": "node scripts/measure-architecture-performance.mjs --update-baseline",
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