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:
@@ -198,7 +198,7 @@ test.describe("Image browser-native input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "resize");
|
||||
await uploadFiles(page, fixture("formats", filename));
|
||||
await uploadFiles(page, fixture("image", "formats", filename));
|
||||
await assertImageRendered(page, filename, `input preview ${ext}`);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -249,7 +249,7 @@ test.describe("Image server-decode input preview", () => {
|
||||
test.setTimeout(90_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "resize");
|
||||
await uploadFiles(page, fixture("formats", filename));
|
||||
await uploadFiles(page, fixture("image", "formats", filename));
|
||||
const status = await assertServerDecodePreview(page, filename);
|
||||
expect(
|
||||
["rendered", "fallback"],
|
||||
@@ -278,7 +278,7 @@ test.describe("Video native input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "convert-video");
|
||||
await uploadFiles(page, fixture("media", filename));
|
||||
await uploadFiles(page, fixture("video", "formats", filename));
|
||||
await assertVideoPreview(page);
|
||||
assertClean(issues, ext);
|
||||
});
|
||||
@@ -314,7 +314,7 @@ test.describe("Video non-native input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "convert-video");
|
||||
await uploadFiles(page, fixture("media", filename));
|
||||
await uploadFiles(page, fixture("video", "formats", filename));
|
||||
await assertVideoNonNativePreview(page);
|
||||
assertClean(issues, ext);
|
||||
});
|
||||
@@ -342,7 +342,7 @@ test.describe("Audio decodable input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "convert-audio");
|
||||
await uploadFiles(page, fixture("media", filename));
|
||||
await uploadFiles(page, fixture("audio", "formats", filename));
|
||||
await assertAudioPreview(page);
|
||||
assertClean(issues, ext);
|
||||
});
|
||||
@@ -368,7 +368,7 @@ test.describe("Audio undecodable input preview (graceful fallback)", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "convert-audio");
|
||||
await uploadFiles(page, fixture("media", filename));
|
||||
await uploadFiles(page, fixture("audio", "formats", filename));
|
||||
// F8: WaveSurfer error/timeout surfaces a graceful "cannot be previewed"
|
||||
// message instead of a dead disabled play button.
|
||||
const fallback = page.getByText(/cannot be previewed in the browser/i).first();
|
||||
@@ -390,7 +390,7 @@ test.describe("Document PDF input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "rotate-pdf");
|
||||
await uploadFiles(page, fixture("documents", "tiny.pdf"));
|
||||
await uploadFiles(page, fixture("document", "formats", "tiny.pdf"));
|
||||
await assertDocumentPreview(page);
|
||||
assertClean(issues, "pdf");
|
||||
});
|
||||
@@ -416,7 +416,7 @@ test.describe("Document non-PDF input preview (word-to-pdf)", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "word-to-pdf");
|
||||
await uploadFiles(page, fixture("documents", filename));
|
||||
await uploadFiles(page, fixture("document", "formats", filename));
|
||||
await assertNonPdfDocumentPreview(page);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -437,7 +437,7 @@ test.describe("Document non-PDF input preview (excel-to-pdf)", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "excel-to-pdf");
|
||||
await uploadFiles(page, fixture("documents", filename));
|
||||
await uploadFiles(page, fixture("document", "formats", filename));
|
||||
await assertNonPdfDocumentPreview(page);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -458,7 +458,7 @@ test.describe("Document non-PDF input preview (powerpoint-to-pdf)", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "powerpoint-to-pdf");
|
||||
await uploadFiles(page, fixture("documents", filename));
|
||||
await uploadFiles(page, fixture("document", "formats", filename));
|
||||
await assertNonPdfDocumentPreview(page);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -478,7 +478,7 @@ test.describe("Document non-PDF input preview (html-to-pdf)", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "html-to-pdf");
|
||||
await uploadFiles(page, fixture("documents", filename));
|
||||
await uploadFiles(page, fixture("document", "formats", filename));
|
||||
await assertNonPdfDocumentPreview(page);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -498,7 +498,7 @@ test.describe("Document non-PDF input preview (markdown-to-pdf)", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "markdown-to-pdf");
|
||||
await uploadFiles(page, fixture("documents", filename));
|
||||
await uploadFiles(page, fixture("document", "formats", filename));
|
||||
await assertNonPdfDocumentPreview(page);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -512,7 +512,7 @@ test.describe("Document non-PDF input preview (epub-convert)", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "epub-convert");
|
||||
await uploadFiles(page, fixture("documents", "tiny.epub"));
|
||||
await uploadFiles(page, fixture("document", "formats", "tiny.epub"));
|
||||
// epub-convert uses no-comparison mode, not document viewer.
|
||||
// Just verify file accepted and no crash.
|
||||
await page.waitForTimeout(3_000);
|
||||
@@ -538,7 +538,7 @@ test.describe("File/data input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "csv-json");
|
||||
await uploadFiles(page, fixture("data", filename));
|
||||
await uploadFiles(page, fixture("data", "valid", filename));
|
||||
await page.waitForTimeout(2_000);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -550,7 +550,7 @@ test.describe("File/data input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "json-xml");
|
||||
await uploadFiles(page, fixture("data", "tiny.xml"));
|
||||
await uploadFiles(page, fixture("data", "valid", "tiny.xml"));
|
||||
await page.waitForTimeout(2_000);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "xml");
|
||||
@@ -567,7 +567,7 @@ test.describe("File/data input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "yaml-json");
|
||||
await uploadFiles(page, fixture("data", filename));
|
||||
await uploadFiles(page, fixture("data", "valid", filename));
|
||||
await page.waitForTimeout(2_000);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, ext);
|
||||
@@ -579,7 +579,7 @@ test.describe("File/data input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "extract-zip");
|
||||
await uploadFiles(page, fixture("data", "tiny.zip"));
|
||||
await uploadFiles(page, fixture("data", "valid", "tiny.zip"));
|
||||
await page.waitForTimeout(2_000);
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "zip");
|
||||
@@ -624,7 +624,7 @@ test.describe("Custom custom-results tools input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "image-to-base64");
|
||||
await uploadFiles(page, fixture("formats", "sample.png"));
|
||||
await uploadFiles(page, fixture("image", "formats", "sample.png"));
|
||||
await assertImageRendered(page, "sample.png", "image-to-base64 input");
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "image-to-base64");
|
||||
@@ -636,7 +636,7 @@ test.describe("Custom custom-results tools input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "find-duplicates");
|
||||
await uploadFiles(page, fixture("formats", "sample.png"));
|
||||
await uploadFiles(page, fixture("image", "formats", "sample.png"));
|
||||
await assertCustomResultsPreview(page, "find-duplicates");
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "find-duplicates");
|
||||
@@ -648,7 +648,7 @@ test.describe("Custom custom-results tools input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "pdf-to-image");
|
||||
await uploadFiles(page, fixture("documents", "tiny.pdf"));
|
||||
await uploadFiles(page, fixture("document", "formats", "tiny.pdf"));
|
||||
// Try document preview first; fall back to custom-results oracle
|
||||
try {
|
||||
await assertDocumentPreview(page);
|
||||
@@ -665,7 +665,21 @@ test.describe("Custom custom-results tools input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "passport-photo");
|
||||
await uploadFiles(page, fixture("formats", "sample.png"));
|
||||
// Guard against route rot: a wrong/404 route must fail loudly, not silently skip.
|
||||
await expect(page.getByRole("heading", { name: "404" })).toHaveCount(0);
|
||||
// The country dropdown is only present once the tool's real UI has loaded;
|
||||
// on a container without the background-removal/face-detection bundles
|
||||
// installed, this page shows an install prompt instead (no dropzone), which
|
||||
// would otherwise fail uploadFiles() with an unclear timeout.
|
||||
const ready = await page
|
||||
.getByText("Country")
|
||||
.waitFor({ state: "visible", timeout: 15_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (!ready) {
|
||||
test.skip(true, "background-removal or face-detection feature bundle not installed");
|
||||
}
|
||||
await uploadFiles(page, fixture("image", "formats", "sample.png"));
|
||||
await assertCustomResultsPreview(page, "passport-photo");
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "passport-photo");
|
||||
@@ -682,7 +696,7 @@ test.describe("Custom interactive tools input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "crop");
|
||||
await uploadFiles(page, fixture("formats", "sample.png"));
|
||||
await uploadFiles(page, fixture("image", "formats", "sample.png"));
|
||||
await assertInteractivePreview(page, "crop");
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "crop");
|
||||
@@ -692,7 +706,21 @@ test.describe("Custom interactive tools input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "erase-object");
|
||||
await uploadFiles(page, fixture("formats", "sample.png"));
|
||||
// Guard against route rot: a wrong/404 route must fail loudly, not silently skip.
|
||||
await expect(page.getByRole("heading", { name: "404" })).toHaveCount(0);
|
||||
// The submit button is only present once the tool's real UI has loaded; on
|
||||
// a container without the object-eraser-colorize bundle installed, this
|
||||
// page shows an install prompt instead (no dropzone), which would
|
||||
// otherwise fail uploadFiles() with an unclear timeout.
|
||||
const ready = await page
|
||||
.getByTestId("erase-object-submit")
|
||||
.waitFor({ state: "visible", timeout: 15_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (!ready) {
|
||||
test.skip(true, "object-eraser-colorize feature bundle not installed");
|
||||
}
|
||||
await uploadFiles(page, fixture("image", "formats", "sample.png"));
|
||||
await assertInteractivePreview(page, "erase-object");
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "erase-object");
|
||||
@@ -702,7 +730,7 @@ test.describe("Custom interactive tools input preview", () => {
|
||||
test.setTimeout(60_000);
|
||||
const issues = instrument(page);
|
||||
await gotoTool(page, "split");
|
||||
await uploadFiles(page, fixture("formats", "sample.png"));
|
||||
await uploadFiles(page, fixture("image", "formats", "sample.png"));
|
||||
await assertInteractivePreview(page, "split");
|
||||
await assertNoBrokenImages(page);
|
||||
assertClean(issues, "split");
|
||||
|
||||
Reference in New Issue
Block a user