mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: repair docker validation QA tooling, dispatcher crash-accounting, and image-enhancement RAW hang (#391)
Found and fixed during a full local Docker build validation (amd64/arm64, all four fleet targets, AI bundle installs, QA harness) and the follow-up bug sweep requested afterward. None of the affected scripts run in CI, so these had been silently broken indefinitely. - docker/feature-manifest.json: pythonVersion was a flat "3.11", but the amd64 base (Ubuntu 24.04) ships Python 3.12 while arm64 (Debian bookworm) ships 3.11. Changed to a per-arch object matching the file's existing convention. - tests/qa/api-sweep.mts and verify-ai.mts: bare "@snapotter/shared" import can't resolve since tests/ is not a pnpm workspace member, making both silently unrunnable via their own documented command on any fresh checkout. Switched to a relative import. - tests/qa/generate-ledger.mts: wrote to docs/qa/ without creating the directory first; docs/ is gitignored except COMMUNITY_GUIDE.md, so a fresh checkout threw ENOENT. - Seven QA Playwright spec files (input-preview, settings, settings-extended, multifile, output-preview, pipeline-ui, smoke) had ~115 fixture() calls using directory names that don't exist. Resolved every call programmatically against the real fixture tree. - packages/ai/src/bridge.ts: AI dispatcher restart (happens on every bundle install) was falsely counted as a crash, risking permanent dispatcher disable after enough legitimate restarts within the crash window. Added a shuttingDown flag checked at all three recordCrash() call sites. - packages/image-engine/src/operations/auto-enhance.ts: image-enhancement hung 40+ seconds on large RAW photos (confirmed on a real 20.2MP file) in Sharp's .clahe() step, whose cost scales with total pixel count regardless of tile size. Added a 16-megapixel cap above which CLAHE is skipped; verified against the real file (40+s -> 2.0s) with no regression to other RAW formats or normal-sized images. Fixing this surfaced a second, smaller bug where the saturation step's CLAHE compensation boost was keyed off the raw toggle instead of whether CLAHE actually ran. - Two QA-harness robustness gaps closed per "fix everything, even the small bugs": the passport-photo/erase-object input-preview tests now skip cleanly with a clear reason on a container without their AI bundle installed, and docker-compose.qa.yml's hardcoded project/container name (the actual root cause of a mid-validation container swap between two concurrent sessions) is now parameterized via QA_PROJECT_NAME. Full validation report is local-only per repo convention.
This commit is contained in:
@@ -1224,6 +1224,69 @@ describe("bridge - initDispatcher", () => {
|
||||
// spawn should only have been called once
|
||||
expect(spawn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not count an intentional shutdown() as a crash", async () => {
|
||||
// Regression test: shutdown() kills the child with SIGTERM, which Node
|
||||
// reports via the "close" event as code=null (not 0). The close handler
|
||||
// must not mistake that for a crash, or repeated legitimate restarts
|
||||
// (e.g. shutdownDispatcher() called on every AI bundle install in
|
||||
// routes/features.ts) would eventually trip MAX_CONSECUTIVE_CRASHES and
|
||||
// permanently disable the dispatcher with nothing having actually crashed.
|
||||
const mock = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mock.process);
|
||||
|
||||
const promise = initDispatcher();
|
||||
mock.stderr.emit("data", Buffer.from('{"ready": true, "gpu": false}\n'));
|
||||
await promise;
|
||||
|
||||
shutdownDispatcher();
|
||||
// Node reports a signal-killed process as code=null, signal="SIGTERM".
|
||||
mock.emitEvent("close", null, "SIGTERM");
|
||||
|
||||
expect(getDispatcherStatus().consecutiveCrashes).toBe(0);
|
||||
});
|
||||
|
||||
it("does not count a shutdown()-induced stdin EPIPE as a crash", async () => {
|
||||
// Regression: shutdown() ends stdin then SIGTERMs the child. On a real
|
||||
// pipe that teardown can surface as an EPIPE/ERR_STREAM_DESTROYED on the
|
||||
// stdin stream (observed in the container shutdown log as a spurious
|
||||
// "[bridge] Dispatcher crash #1"). The "close" handler alone is guarded,
|
||||
// but the stdin error handler must be too -- otherwise shutdownDispatcher()
|
||||
// on every AI bundle install accrues false crashes toward the disable cap.
|
||||
const mock = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mock.process);
|
||||
|
||||
const promise = initDispatcher();
|
||||
mock.stderr.emit("data", Buffer.from('{"ready": true, "gpu": false}\n'));
|
||||
await promise;
|
||||
|
||||
shutdownDispatcher();
|
||||
const epipe = new Error("write EPIPE") as NodeJS.ErrnoException;
|
||||
epipe.code = "EPIPE";
|
||||
mock.stdin.emit("error", epipe);
|
||||
|
||||
expect(getDispatcherStatus().consecutiveCrashes).toBe(0);
|
||||
expect(getDispatcherStatus().failed).toBe(false);
|
||||
});
|
||||
|
||||
it("does not count a shutdown()-induced process error as a crash", async () => {
|
||||
// The proc "error" handler (non-ENOENT) also records crashes; a kill during
|
||||
// shutdown can emit one (e.g. ESRCH), which must not be counted.
|
||||
const mock = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mock.process);
|
||||
|
||||
const promise = initDispatcher();
|
||||
mock.stderr.emit("data", Buffer.from('{"ready": true, "gpu": false}\n'));
|
||||
await promise;
|
||||
|
||||
shutdownDispatcher();
|
||||
const err = new Error("kill ESRCH") as NodeJS.ErrnoException;
|
||||
err.code = "ESRCH";
|
||||
mock.emitEvent("error", err);
|
||||
|
||||
expect(getDispatcherStatus().consecutiveCrashes).toBe(0);
|
||||
expect(getDispatcherStatus().failed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Dispatcher stdin JSON-RPC protocol ──────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user