Files
SnapOtter/tests
SnapOtterandGitHub fd39f66f46 fix(ai-bundles): lock the numpy-1.x ABI closure so the OCR bundle can't strand scipy (#437)
* fix(ai-bundles): lock the numpy-1.x ABI closure so the OCR bundle can't strand scipy

The OCR bundle installs paddleocr[doc-parser] 3.4, whose dependency closure drags
numpy 1.26.4 up to 2.5.1 and pulls scipy/scikit-learn/pandas wheels built against
the numpy 2.x ABI. build-bundle.sh re-pinned only numpy (basePackages), so those
numpy-2.x wheels stayed behind; the by-dir-name site-packages diff then shipped
them, and once merged onto the numpy==1.26.4 base they raise "numpy.dtype size
changed" on import.

Because the dispatcher pre-imports every ML library at startup and disables all AI
after 5 crashes in 60s, one stranded scipy takes down every AI tool, not just OCR
(observed on a CPU host: remove-background worked before the OCR bundle and broke
after). All-7 installs escaped it through last-writer-wins ordering; a subset
install did not, which is why it surfaced only intermittently.

Fix: add a manifest "constraints" list (numpy, scipy, scikit-learn, scikit-image,
pandas pinned to numpy-1.x-ABI versions) and apply it via PIP_CONSTRAINT to every
bundle pip install, so no bundle can pull a numpy-2.x wheel. paddleocr 3.4.1 still
resolves cleanly under the lock and the pinned stack imports without ABI error on
numpy 1.26.4 (validated on py3.12). Also import scipy/sklearn in the OCR path of
verify-bundle.sh so CI catches this class in isolation, and add a manifest
regression test.

Note: the published bundles must be rebuilt and republished (ai-bundles.yml) for
this to reach already-installed bases.

Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav

* chore(ai-bundles): sync OCR manifest sha256 to the rebuilt numpy-1.x bundles

Rebuilt the OCR bundle for both arches with the numpy-1.x-ABI constraints from
this PR and republished the tars to deepsafe/feature-bundles/v2.0.0, then updated
the baked manifest sha256 and sizes so installs verify against the fixed archives:

  amd64-gpu  5.93 GB  sha 2a00a3184f6a635f1fa9ae2a6517ad740a11f9e5ff58c098d2fd369a2bb1e16b
  arm64-cpu  1.98 GB  sha 6868c264069dcb74c6675c0b1f58dc1c9f60d9aa4459725e3dbde07a99a6a09a

Both tars ship scipy 1.12.0 / scikit-learn 1.4.2 / pandas 2.2.2 (numpy-1.x-ABI)
and zero numpy-2.x wheels, verified by listing the archive contents.

Stopgap note: these tars were built against the ghcr.io latest base (the 2.0.0
image is not published to GHCR), so they are not byte-identical to what the CI
build will produce. When ai-bundles.yml rebuilds at the 2.0.0 release, it will
mint fresh sha256 values and this manifest must be re-synced to them.

Claude-Session: https://claude.ai/code/session_01UvVCMNUBrgpghk8gye5gav
2026-07-05 11:52:27 +00:00
..
2026-07-04 13:45:47 +08:00

SnapOtter Test Suite

Directory Structure

tests/
  unit/                    # Vitest unit tests (~185 files)
    ai/                    # AI sidecar bridge + bundle tests
    api/                   # API route/config/middleware tests
    doc-engine/            # doc-engine package tests
    features/              # Feature flag + enterprise tests
    fixtures/              # Fixture guard tests (resolve/budget/manifest)
    image-engine/          # image-engine operation tests
    media-engine/          # media-engine FFmpeg wrapper tests
    security/              # Security-focused unit tests
    shared/                # Shared constants/i18n/permissions tests
    web/                   # React component tests (tsx)
  integration/             # Full-API integration tests (~250 files)
    fixtures/              # Fixture integrity tests (decode through Sharp/ffprobe/qpdf)
    generated/             # Generated test matrices (format x tool, pairwise settings)
    platform/              # Platform tests (jobs, pipelines, batch, auth, OIDC)
    security/              # Security integration tests (SSRF, XXE, path traversal)
    tools/                 # Per-tool integration tests
  e2e/                     # Playwright specs (main app, Chromium primary)
  e2e-analytics/           # Analytics flow e2e specs
  e2e-docker/              # Docker container e2e specs
  e2e-docs/                # VitePress docs site e2e specs
  e2e-editor/              # Konva image editor e2e specs
  e2e-landing/             # Astro landing site e2e specs
  fixtures/                # Shared test fixtures (see below)
  helpers/                 # Test utilities (pairwise, zod-pict, tool defaults)
  setup/                   # Vitest setup (per-fork DB clone, BullMQ prefix isolation)
  baseline/                # Phase 0 baseline snapshots for parity verification
  benchmark/               # Performance benchmarks
  qa/                      # QA checklists and manual test plans

Fixtures

Fixtures live in tests/fixtures/ in a modality-first layout:

tests/fixtures/
  index.ts              # Typed registry -- import paths from here, not raw strings
  manifest.json         # sha256 + bytes + provenance for all valid/ assets
  gen-manifest.mjs      # Re-stamps manifest hashes from disk
  gen-synthetic-content.mjs  # Regenerates CC0 synthetic content (QR, barcode, OCR, etc.)
  LICENSES.md           # Provenance audit trail
  image/
    valid/              # Real images for depth tests (portraits, scenes, OCR, etc.)
    formats/            # Tier-1 tiny format samples (sample.png, sample.arw, ...)
    edge/               # Edge-case images (1x1, blank, extreme aspect)
    hostile/            # Malformed/bomb/polyglot files for rejection tests
  video/
    valid/              # Real videos (Big Buck Bunny CC-BY heroes, TTS speech)
    formats/            # Tier-1 tiny format samples (tiny.mp4, tiny.webm, ...)
    hostile/            # Truncated/malformed video files
  audio/
    valid/              # Real audio (TTS speech in 6 formats, tagged MP3)
    formats/            # Tier-1 tiny format samples (tiny.mp3, tiny.wav, ...)
    hostile/            # Zero-byte/malformed audio files
  document/
    valid/              # PDFs (3-page, 6-page, encrypted, OCR-scanned)
    formats/            # Tier-1 tiny doc samples (tiny.docx, tiny.epub, ...)
    edge/               # Edge-case docs (remote-img.html for SSRF tests)
    hostile/            # Truncated/garbage documents
  data/
    valid/              # Data files (CSV, JSON, XML, YAML, TSV, ZIP)
  security/             # XXE/SSRF test vectors (SVG)

Two-Tier Fixture Strategy

Tier-1: Tiny Synthetics (in formats/, edge/, hostile/) Small, project-generated files for format-compatibility matrices and fast unit tests. These are always-regeneratable by the generator scripts and are used for breadth.

Tier-2: Real Heroes (in valid/) Content-representative files for integration and fidelity tests. Includes:

  • Big Buck Bunny video clips (CC-BY, Blender Foundation)
  • macOS TTS speech in 6 audio formats (CC0)
  • Project-generated synthetics (QR codes, barcodes, OCR text images, PDFs)
  • Portrait/scene photos for AI tool tests (some UNVERIFIED-REVIEW, see manifest)

Every valid/ file is tracked in manifest.json with sha256 hash, byte count, source URL, and license. The manifest guard test (fixture-manifest.test.ts) verifies disk matches manifest on every run.

Typed Fixture Registry

Always import fixture paths from tests/fixtures/index.ts:

import { fixtures, readFixture, fixtureDir } from "../../fixtures/index.js";

// Named paths (type-checked, IDE autocomplete)
const pngPath = fixtures.image.base.png200;
const heroMp4 = fixtures.video.hero.mp4;

// Read file bytes
const buffer = readFixture(fixtures.image.base.png200);

// Format accessor (for matrix iteration)
const arwPath = fixtures.image.formats("arw");
const mp4Path = fixtures.video.tiny("mp4");

// Directory paths (for glob/scan patterns)
const hostileDir = fixtureDir.image.hostile;

Guard Tests

Four guard tests protect fixture integrity:

Guard Location What it checks
fixture-resolve unit/fixtures/ Every registry path exists and is non-empty
fixture-budget unit/fixtures/ Per-extension size caps prevent bloat
fixture-manifest unit/fixtures/ sha256/bytes in manifest.json match disk
fixture-integrity integration/fixtures/ Real heroes decode through Sharp/ffprobe/qpdf

Additional parity guards:

Guard Location What it checks
tool-registry-drift unit/shared/ Frontend registry matches shared TOOLS catalog
tool-route-drift integration/ API routes match shared TOOLS catalog
i18n-completeness unit/shared/ All 21 locales have all required keys

Adding a Fixture

  1. Add the file to the appropriate modality directory (image/valid/, video/formats/, etc.)
  2. Add a named key in tests/fixtures/index.ts
  3. Run node tests/fixtures/gen-manifest.mjs (for valid/ files)
  4. Fill in sourceUrl and license in manifest.json
  5. Update LICENSES.md if the file has third-party origin
  6. Run guards: pnpm vitest run tests/unit/fixtures/

Generator Scripts

Three generators produce deterministic synthetics. All are idempotent and skip files that already exist (to avoid breaking manifest hashes from encoder-version differences). Pass --force to gen-synthetic-content.mjs to overwrite.

Script Scope Outputs
scripts/generate-test-fixtures.mjs Tier-1 format samples video/formats/tiny.mp4, audio/formats/tiny.{mp3,wav}, document/formats/tiny.{docx,xlsx,epub,html,md}, document/valid/encrypted.pdf
scripts/generate-hostile-fixtures.mjs Hostile inputs image/hostile/{truncated.jpg,zero-byte.png,garbage.jpg,png-bytes.jpg,bomb-50000x50000.png}
tests/fixtures/gen-synthetic-content.mjs CC0 synthetics QR codes, barcodes, OCR text images, PDFs, SVG logo, multi-face placeholder, animated GIF, tagged MP3, metadata MP4

The generators do NOT touch committed real heroes (BBB videos, TTS speech, UNVERIFIED-REVIEW photos).

Running Tests

# Full suite
pnpm test                    # All unit + integration tests
pnpm test:unit               # Unit tests only
pnpm test:integration        # Integration tests only

# Single file
pnpm vitest run tests/unit/my-test.test.ts
pnpm vitest run tests/integration/my-test.test.ts

# Fixture guards only
pnpm vitest run tests/unit/fixtures

# E2E (Playwright)
pnpm test:e2e                # Main app (chromium + serial + visual projects)
pnpm test:e2e:landing        # Landing site
pnpm test:e2e:docs           # VitePress docs
pnpm test:e2e:analytics      # Analytics flows
pnpm test:docker             # Full Docker container test

# Editor e2e (no root alias)
pnpm playwright test --config playwright.editor.config.ts

# Single e2e spec
pnpm playwright test tests/e2e/my-test.spec.ts

Environment Requirements

Unit and integration tests need Postgres + Redis:

docker compose -f docker-compose.dev.yml up -d
export DATA_DIR=/tmp/so-test-data && mkdir -p "$DATA_DIR"

Each Vitest fork gets its own Postgres DB clone + workspace + isolated BULLMQ_PREFIX via tests/setup/per-fork-env.ts (testcontainers for Postgres). 30s test timeouts.

E2E Projects

The main Playwright config defines three projects:

  • chromium: Parallel-safe specs, 2 workers (override via PW_WORKERS)
  • chromium-serial: Global-state specs, 1 worker
  • chromium-visual: Screenshot comparison specs (platform-suffixed baselines)

Auth setup project logs in and saves storage state. The webServer block spins up both dev servers with a fresh test DB.

Environment Switches

Variable Effect
FULL_MATRIX=1 Run all format x tool combinations (nightly)
FUZZ=1 + FUZZ_RUNS=N Property-based fuzz testing (nightly)
PW_WORKERS=N Override Playwright worker count

CI Pipeline

PRs run: lint, typecheck, unit, integration (4 shards), e2e smoke. Nightly: full e2e, cross-browser, docker suite, full matrix + fuzz, coverage.