Files
SnapOtter/tests/qa/smoke.qa.spec.ts
T
SnapOtterandGitHub bd1838e40b 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.
2026-07-02 14:21:13 +08:00

68 lines
2.4 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
assertAudioPreview,
assertDocumentPreview,
assertImageRendered,
assertNoBrokenImages,
assertVideoPreview,
downloadResult,
fixture,
gotoTool,
imageInfo,
instrument,
isClean,
issuesSummary,
magicMatches,
processTool,
uploadFiles,
} from "./qa-helpers";
// Harness smoke: validates upload, per-modality preview oracles, processing,
// download + decode verification, and the console instrument against the live
// QA container. Also satisfies the Phase 0 "one tool per modality" smoke.
test("image: resize round-trip, previews, dimension oracle", async ({ page }) => {
const issues = instrument(page);
await gotoTool(page, "resize");
await uploadFiles(page, fixture("image", "formats", "sample.png"));
await assertNoBrokenImages(page);
await page.locator("#resize-width").fill("64");
const res = await processTool(page, "resize", "fast");
expect(res.ok, res.error).toBe(true);
await assertImageRendered(page, "Processed", "resize output");
const dl = await downloadResult(page, "resize");
expect(dl.size).toBeGreaterThan(0);
const dims = imageInfo(dl.path);
expect(dims.width, "resized width applied").toBe(64);
expect(isClean(issues), issuesSummary(issues)).toBe(true);
});
test("image: convert png -> webp, magic-byte oracle", async ({ page }) => {
await gotoTool(page, "convert");
await uploadFiles(page, fixture("image", "formats", "sample.png"));
await assertNoBrokenImages(page);
await page.locator("#convert-target-format").selectOption("webp");
const res = await processTool(page, "convert", "fast");
expect(res.ok, res.error).toBe(true);
const dl = await downloadResult(page, "convert");
expect(magicMatches(dl.buf, "webp"), `expected webp, got ${dl.name}`).toBe(true);
});
test("video: input preview decodes (tiny.mp4)", async ({ page }) => {
await gotoTool(page, "convert-video");
await uploadFiles(page, fixture("video", "formats", "tiny.mp4"));
await assertVideoPreview(page);
});
test("audio: input waveform ready (tiny.mp3)", async ({ page }) => {
await gotoTool(page, "convert-audio");
await uploadFiles(page, fixture("audio", "formats", "tiny.mp3"));
await assertAudioPreview(page);
});
test("document: pdf input preview renders (tiny.pdf)", async ({ page }) => {
await gotoTool(page, "rotate-pdf");
await uploadFiles(page, fixture("document", "formats", "tiny.pdf"));
await assertDocumentPreview(page);
});