Commit Graph
198 Commits
Author SHA1 Message Date
SnapOtter d4dc2eae5d fix(tests): section-prefix tool API URLs across overhauled suite
#276's overhauled integration tests + QA sweep scripts hit the old
sectionless /api/v1/tools/<id> routes, which 404 after #280 made routes
section-prefixed. Ran scripts/rewrite-tool-paths.ts for literal URLs and
converted dynamic /api/v1/tools/${id} constructions to apiToolPath().
Negative-test paths (nonexistent/fake tools) intentionally left raw.
2026-06-21 02:30:58 +08:00
SnapOtter 5ffa1d55ea Merge branch 'worktree-test+suite-overhaul-and-real-fixtures' into chore/consolidate-v2.0.0
# Conflicts:
#	tests/integration/generated/settings-matrix.test.ts
#	tests/integration/platform/api.test.ts
#	tests/integration/platform/concurrent.test.ts
#	tests/integration/platform/factory-multi-input.test.ts
#	tests/integration/security/adversarial-comprehensive.test.ts
#	tests/integration/security/adversarial-coverage-gaps.test.ts
#	tests/integration/security/adversarial-extended.test.ts
#	tests/integration/security/adversarial-final-gaps.test.ts
#	tests/integration/security/adversarial-matrix.test.ts
#	tests/integration/security/adversarial-security.test.ts
#	tests/integration/security/adversarial.test.ts
#	tests/integration/tools/image/color-adjustments.test.ts
2026-06-21 02:18:53 +08:00
SnapOtter 0ddcc7aef5 test(api): fall back to raw path for unknown tool ids in adversarial negative tests 2026-06-21 00:47:06 +08:00
SnapOtter c1c66efd3f chore: section-prefix remaining tool paths in docs, examples, benchmarks, and multi-input test 2026-06-21 00:30:10 +08:00
SnapOtter f80444791b test(api): section-prefix tool URLs across integration, docker, and e2e api specs 2026-06-20 12:24:50 +08:00
SnapOtter 414f126a33 docs(api): section-prefix OpenAPI tool paths 2026-06-20 11:55:22 +08:00
SnapOtterandGitHub 19d9ed181a fix(audio): low-samplerate ogg encode + post-2.0 QA hygiene
Quality-VBR ogg (libvorbis -q:a) fixes 8 kHz 'encoder setup failed' in both ogg paths; drop empty COOKIE_SECRET ENV (app auto-generates); emit real bundle extractedSize; fix stale image-pad/compress-pdf QA specs.
2026-06-20 10:41:50 +08:00
SnapOtter 34b006ded7 fix(test): resolve CI failures from the overhaul
- fixture-integrity: probe media via media-engine probeMedia (resolves the
  bundled static ffmpeg) instead of bare system ffprobe, which is ENOENT in CI;
  gate on ffmpegAvailable() like the other media tests
- a11y: regenerate a11y-baseline.json to include the mobile device keys (the
  baseline only had desktop keys, so the mobile a11y scan saw them as new)
- device-visual: tag @visual and exclude it from the PR mobile-smoke gate
  (darwin-only screenshots cannot pass on linux; nightly + update-visual-baselines
  still run it to seed linux goldens)
2026-06-20 09:54:49 +08:00
SnapOtter 941ed27912 test: reorganize fixture files into modality-first layout (phase 6b)
Move all fixture files from flat/mixed dirs (content/, media/, documents/,
formats/, hostile/, root loose) into the modality-first hierarchy:
image/{valid,formats,edge,hostile}, video/{valid,formats,hostile},
audio/{valid,formats,hostile}, document/{valid,formats,edge,hostile},
data/valid/, security/. Update index.ts paths, fixtureDir aliases,
all literal refs in 17 e2e/qa/script files, manifest.json, and the
three generator scripts. 163 files moved, 0 dropped, 100 new tests
from expanded document scan.
2026-06-20 05:51:17 +08:00
SnapOtter 1727a7a73e test: reorganize flat test files into purpose-based subdirectories (phase 6)
Group 245 flat integration tests and 25 loose unit tests into
discoverable subdirectories per spec section 6:

  integration/tools/{image,video,audio,document,data}/  (156 files)
  integration/platform/                                  (64 files)
  integration/generated/                                 (14 files)
  integration/security/                                  (10 files)

  unit/security/     (8 files, new subdir)
  unit/api/          (7 files moved in)
  unit/web/          (3 files moved in)
  unit/shared/       (6 files moved in)
  unit/image-engine/ (1 file moved in)

All moves via git mv (history preserved). Relative imports repaired
for both depth levels (platform/generated/security = +1, tools/ = +2):
static from-imports, dynamic import() calls, vi.mock() paths,
import.meta.dirname joins, and __dirname joins.

Vitest discovery unchanged (no test.include in config, recursive glob
matches subdirs, shard-by-hash unaffected). test-server.ts and
tool-route-drift.test.ts stay at integration root. fixtures/ untouched.

Parity gate: 13189 passing test names before = 13189 after (0 dropped).
2026-06-20 05:07:46 +08:00
SnapOtter b1afcfdf3c test: migrate 207 test files to typed fixture registry (phase 5)
Codemod rewrites all five ad-hoc fixture-path styles to import from the
typed registry (tests/fixtures/index.ts):
- join(__dirname, "..", "fixtures", ...) inline patterns (100 files)
- const FIXTURES = join(__dirname, ...) + join(FIXTURES, ...) (92 files)
- process.cwd()-based paths (10 files)
- path.resolve(__dirname, ...) patterns (5 files)

Registry extended with 8 new keys (portraitJpg, portraitHeic,
motorcycle, svgLogo, barcodeAvif, qrAvif, crossFormatChat,
multipageTiff) and directory accessors (fixtureDir, fixtureRoot).

Dynamic directory scanners (format-matrix-generated,
format-matrix-multimodal, hostile-inputs) preserved via fixtureDir
imports. No fixtures swapped, bytes identical.

Parity gate: dropped 0, net-new 388.
2026-06-20 04:20:17 +08:00
SnapOtter f950dac792 test: add launch-readiness guards and fix zh-CN/pt-BR i18n fallback (phase 4a)
Four launch gates for v2.0.0:

1. Catalog integrity (catalog-integrity.test.ts): asserts every TOOLS entry
   is fully wired end to end (API route + frontend registry + display mode +
   process fn or REGISTRY_EXEMPT). Count checked dynamically against
   TOOLS.length. All 157 tools pass.

2. i18n cross-locale parity (i18n-parity.test.ts): asserts every locale in
   SUPPORTED_LOCALES has the same key set as en.ts. Found and fixed a real
   bug: zh-CN and pt-BR exported only a camelCase named export (zhCN, ptBR)
   with no default export, so loadTranslations silently fell back to English
   for Chinese Simplified and Brazilian Portuguese users. Fixed by adding
   export default to both files. All 20 non-en locales now pass parity.

3. Cross-modality smoke (cross-modality-smoke.test.ts): one fast tool per
   modality (rotate/image, mute-video/video, convert-audio/audio,
   rotate-pdf/document, csv-json/data) plus an auth gate. Tools needing
   ffmpeg or qpdf are gated with skipIf. Ship/no-ship signal.

4. Migration launch gate: extended migrate-from-sqlite.test.ts with a
   representative 1.x SQLite database (3 users, 3 teams, 3 settings,
   2 roles, 2 sessions, 2 API keys, 2 pipelines, 4 jobs, 4 audit entries,
   4 user files) covering boolean/timestamp/JSON/NULL type conversions,
   column remapping (input_files->input_refs, progress real->jsonb), and
   multi-row round-trip verification. 9 new test cases.

Parity: 13260 passed, 0 dropped.
2026-06-20 02:38:30 +08:00
SnapOtter d09823bf7d test(fixtures): add Tier-2 depth tests for real heroes (phase 2) 2026-06-20 01:40:28 +08:00
SnapOtter 09b26718e1 test(fixtures): replace synthetic media with real CC-BY/CC0 content
- Video: Big Buck Bunny (Blender Foundation, CC-BY 3.0) 8s segment
  replaces ffmpeg testsrc2 synthetic; container variants hero.{mov,webm,mkv,avi}
- Speech: macOS TTS (CC0) replaces unknown-origin speech files;
  format variants speech.{flac,ogg,m4a,aac,opus} for depth tests
- Chat images: Sharp SVG mock chat UI (CC0) replaces third-party
  app screenshots with personal content
- Registry: video.hero.{mov,webm,mkv,avi}, audio.speech.{flac,ogg,m4a,aac,opus}
- Integrity guard: 19 ffprobe assertions (was 8)
- Parity: 13177 passed, 0 dropped
2026-06-20 01:24:59 +08:00
SnapOtter 108583bc1a test(fixtures): add real-fixture integrity guard (phase 1) 2026-06-19 20:14:26 +08:00
SnapOtterandGitHub 7579634633 test: fix 2.0 integration CI -- 202 async fallback + timeout hardening
Fixes all integration CI failures on the 2.0 branch.

## What was broken

Two independent root causes:

1. **202 assertion failures** -- Under 4-fork CI parallel load, the 30s
   `SYNC_WAIT_MS` sync window can expire before a BullMQ worker finishes a
   heavy encode (avif, heic), returning a legitimate `202 {jobId, async: true}`
   instead of `200`. Tests that hard-asserted `200` were spuriously failing.

2. **Vitest timeout race** -- `SYNC_WAIT_MS` (30s) and the default Vitest
   `testTimeout` (also 30s) fired simultaneously. Vitest won the race,
   reporting "Test timed out in 30000ms" instead of the test receiving the
   202 response.

## Fixes

- Added `isAsyncFallback()` helper to four integration test files; validates
  the `{async: true, jobId}` body shape and returns early so the synchronous
  200 path runs full assertions only when warranted.
- Set `vi.setConfig({ testTimeout: 60_000 })` at module level in
  `image-enhancement.test.ts` and `format-matrix-comprehensive.test.ts`,
  giving a 30s buffer between when `waitForJob()` returns 202 and when
  Vitest gives up.
- Bumped explicit matrix timeouts in `format-matrix.test.ts` and
  `new-formats.test.ts` from 30s to 60s for the same reason.
- Installed missing CI doc-engine binaries (qpdf, pandoc, libreoffice,
  pdfcpu) that were causing unrelated integration failures.
- Fixed E2E smoke specs for 2.0 UI changes (modality selector, tool routes,
  validation behavior).
2026-06-19 18:15:20 +08:00
SnapOtter de8bd79b04 test: update pre-existing stale specs for 2.0 multimodal + validation behavior
Fixes a backlog of integration/unit specs that asserted pre-2.0 behavior and
were failing CI (not caused by recent feature work):
- modality-aware empty-input error is 'No file(s) provided', not /no image/i
  (rotate, border, crop, resize, smart-crop, edge-cases, adversarial-extended,
  api, tool-factory-route)
- input validation rejects pre-enqueue with a clean 400 in 'error' (was a worker
  422 in 'details'): create-zip, extract-zip, merge-csvs
- resolveToolPool defaults unknown tools to the system pool (pool-routing)
- /upload and fetch-urls accept non-image content, validated per-tool at process
  time (api, fetch-urls)
- color-adjust legacy aliases were consolidated into adjust-colors: drop the
  removed-alias tests; retarget the format-preservation tests
- xml-to-csv gracefully converts a single non-repeating record to a 1-row CSV
- dropzone is multimodal; image-only filtering is opt-in via fileFilter
- factory-multi-input: register the synthetic test tools in the catalog so they
  route correctly (file modality for concat; image for the validation-prefix test)

Verified locally: unit 4546 passed, integration 8332 passed, typecheck + lint green.
2026-06-17 15:54:48 +08:00
SnapOtter d5c855d844 test: align batch empty-input assertion with modality-aware error
The batch endpoint became modality-aware, so an empty request returns the
generic "No files provided" rather than the legacy image-specific message.
The assertion still matched /no image/i (a pre-existing stale spec on main),
so it failed against the correct behavior. Match /no files/i instead.
2026-06-17 14:39:34 +08:00
SnapOtter 7e3249a998 test: fix stale epub-convert/to-epub async tests
These suites only ever skipped (pandoc is absent in every test env), which hid three latent bugs once the binaries are reachable:

- epub-convert and to-epub are executionHint "long" (async: 202 + jobId), but the tests asserted a synchronous 200 and read the response downloadUrl. Rewrite to the 202 -> poll job row -> download pattern.
- The job-row reads selected schema.jobs.errorMessage, which is not a column (the field is `error` jsonb {message,details}); drizzle threw on the bad select. Read error/error.message instead. (The pdf-chain test had the same bug.)
- The SSRF test asserted the remote <img> URL passes through, but pandoc strips the unmanifested remote ref. Assert the security-relevant property instead: the conversion completes from the book's own content and never fetches+inlines the remote resource (no data: URI).

Verified by building the test image with pandoc: all epub/to-epub tests pass.
2026-06-17 14:28:41 +08:00
SnapOtter 9c250d244f test: add regression coverage for the video QA fixes
friendlyError unit test (incl. the false-positive guard); gated integration tests for multi-file video batch and a multi-step video pipeline (regression for the modality-aware batch/pipeline fix). All pass locally; existing image batch (36) and pipeline (37) suites remain green, and the existing gif-to-video webm test now passes with the pix_fmt fix.
2026-06-17 14:28:41 +08:00
SnapOtterandGitHub 622c9f98a5 fix(ci): resolve pre-existing failures making main red (#250)
After #249 merged, main's CI is red on failures that predate this work (they
were cache-masked on the old branch). This applies only the missing fixes onto
current main (reverts nothing from #246/#248; saml/user-files are already fixed
on main):

- remove 8 dead landing unit tests importing @landing/app and @landing/components
  React paths deleted in the Astro migration
- format tool-factory.ts and json-xml.ts to clear the api lint errors
- accept a 202 async fallback in the image-enhancement large-image test (it
  exceeds the sync window on slower CI runners)
- relax the pipeline no-file assertion to match 'No image file provided' or
  'No file provided'
2026-06-16 16:09:45 +08:00
SnapOtterandGitHub d50e8e42a7 QA + image-tool depth pass: codec/eraser/PDF fixes, modality renames, 13 image tools deepened (#249)
* fix(media): mux container-correct codecs in video tools

Video tools hardcoded H.264 (and AAC) while keeping the input's container extension, so a .webm input produced an invalid file (ffmpeg exit 234: H.264 cannot be muxed into WebM). Add shared videoEncodeArgsForContainer/audioEncodeArgsForContainer helpers (vp9+opus for webm, theora+vorbis for ogv, h264+aac otherwise) and apply them across 14 tools; re-encode audio to AAC in burn-subtitles (forced mp4). Adds a webm regression test for change-fps.

* fix(eraser): recover Object Eraser when its progress SSE drops

The eraser used a bespoke EventSource with no recovery, so a dropped SSE left the UI stuck at ~25% forever even though the backend job had finished and saved its result. Add a resilient subscription (reconnect on tab refocus, which replays the cached terminal frame; 5-minute stall timeout) mirroring the standard processor's PR #203/#204 recovery.

* feat(ui): rename the Documents modality to PDF and Data to Files

Updates modality display names, the home-page tabs, the tool-page breadcrumb, and the homePage.documents/data + modalities labels across all 21 locales. URL slugs are unchanged for link stability.

* feat(compress-pdf): add quality and target-size compression modes

Mirror the image Compress tool: a quality slider (1-100) and a target file size, replacing the screen/ebook/printer preset. Adds gsCompressPdfQuality to doc-engine (quality maps to image downsample DPI, the dominant size lever for PDFs); target-size binary-searches the DPI for the highest quality under the target. The frontend reuses the shared CompressControls component, so no new translation strings are needed.

* feat(ocr-pdf): show the PDF preview and extracted text side by side

ocr-pdf fell back to the image viewer, which cannot render a PDF, so the right pane showed 'Preview not available' and the extracted text was only a download. It now uses a custom results view (custom-results display mode) rendering the input PDF via pdf.js (DocumentView gains an inputOnly prop, since the tool's output is a .txt) next to the extracted OCR text, with a copy button.

* feat(ui): link the modality breadcrumb to its tools tab

The modality segment of the tool breadcrumb (PDF, Image, Video, Audio, Files) is now a link to /?modality=<tab>. The home page reads the param, activates the matching tab, and cleans the URL, so it returns to the existing Tools page filtered to that modality without a new page. Handles the file modality whose tab key is 'data'.

* feat(circle-crop): add zoom/offset framing, border, background, and output size

Upgrade the circle-crop tool from a bare centered crop into a framing and
styling tool. New settings (all backward-compatible with the old empty
payload):

- zoom (1-5x) plus offsetX/offsetY (0-1) to control how tight the circle is
  and where it sits in the source image
- borderWidth (0-200px) plus borderColor for an optional ring
- background: transparent (clear corners) or a hex fill
- outputSize for a square output; omitted keeps native size

The settings panel gains an inline draggable circular preview that mirrors
the framing live, a zoom slider, a border slider with color, a
transparent/color background toggle, and an output-size field. Adds an
integration test covering output size, border, and a solid background.

* feat(image-tools): flesh out five thin tools (gif-webp, histogram, favicon, color-palette, lqip)

Tier A of the image-tool depth pass. Each of these was as bare as the old
circle-crop (empty settings, opaque or invisible output). Now:

- gif-webp: quality, lossless, and resize-percent controls; shows before/after size
- histogram: returns full per-channel bins + stats; the settings panel renders an
  inline interactive histogram with R/G/B/Luma toggles, linear/log scale, and a
  mean/median/stdev readout (server PNG still downloadable)
- favicon: background fill, padding, corner-radius, theme color, and a per-size
  checklist, with a live preview grid; the route applies the styling and honors
  the size filter
- color-palette: count (2-16) and hex/rgb/hsl format controls, median-cut
  extraction, a palette strip, and CSS/JSON export
- lqip-placeholder: blur/pixelate/solid strategies, format and quality; the
  output panel now surfaces the data URI with copy plus HTML/CSS snippets and a
  preview (previously the deliverable was never shown)

Also expose resultPayload from useToolProcessor so a tool can render the route's
extra result fields (histogram bins, lqip data URI) in its own panel. Updates the
five integration tests to cover the new settings.

* feat(image-tools): deepen five thin tools (duotone, vignette, pixelate, background-replace, blur-background)

Tier B of the image-tool depth pass.

- duotone: preset palettes, an intensity slider that blends the duotone with
  the original, and a true live duotone preview (a self-contained grayscale +
  lighten/darken overlay so the pane filter cannot wash it out)
- vignette: radius, softness, roundness, and center-x/y controls driving a
  rebuilt radial gradient, with a matching live overlay
- pixelate: a selection mode that exposes the route's region support via a
  draggable box over the image plus width/height sliders, so a face or plate
  can be pixelated in isolation
- background-replace: gradient backgrounds, edge feather, and webp output on top
  of the existing solid color; now shown before/after
- blur-background: edge feather and webp output; now shown before/after

The live previews for duotone and vignette needed onImageStyle to mount the
overlay branch in image-viewer. The duotone intensity blend and both AI tools'
edge feather were rewritten to splice the alpha channel through raw buffers;
joinChannel did not reliably re-tag the merged channel as alpha and a
raw-without-encoder buffer broke the next decode. Updates the five integration
tests.

* fix(data): rename Files modality to Data + 20 Data-tool bug fixes (#247)

* fix(ui): restore the Data modality name (revert Files rename)

The 'file' modality reverts to the 'Data' label in modality.ts, the home-page tab, and the tools.data + documentsAndFiles i18n keys across all 21 locales. The separate Documents to PDF rename is kept. The URL slug was already /data, so name and slug realign; the tool breadcrumb follows modality.ts automatically.

* fix(create-zip): require at least two files before enabling submit

create-zip enabled its submit button with a single file, but the backend rejects fewer than two files ('Zipping needs at least two files'), producing a 422 error. Gate the button on files.length >= 2 to match the sibling merge-csvs tool. Found during the Data-modality QA sweep.

* fix(data): resolve 17 bugs found in a deeper Data-tool review

Crashes (threw an internal error on otherwise-valid input):
- csv-json: a primitive JSON array like [1,2,3] threw "Unable to serialize"; now a clear error.
- json-xml: a null or primitive JSON root crashed the XML builder; now a clear 4xx.
- yaml-json: an empty or comment-only YAML returned undefined and threw on Buffer.from; now emits null.

Data loss / wrong output:
- csv-json: nested objects rendered as "[object Object]" (now serialized to JSON); heterogeneous objects dropped columns (now the union of all keys).
- xml-to-csv: leaked fast-xml-parser markers ("@_" on attributes, "#text") into CSV headers (now cleaned); a single-record XML failed to tabulate (now a 1-row table); heterogeneous records dropped columns (now the union of all keys).
- csv-excel: xlsx date cells were rendered in the server timezone via Date.toString (now ISO 8601, round-trippable).
- create-zip and extract-zip: filename/basename collisions overwrote zip entries and silently lost a file; dedup now checks generated names and guarantees uniqueness.
- chart-maker: negative values produced invalid/degenerate SVG that Sharp silently dropped; now rejected with a clear message.

Empty output / validation:
- split-csv: a header-only CSV produced an empty zip; now errors with "No data rows to split".
- extract-zip: a directory-only zip produced an empty zip; now errors with "No extractable files found".
- create-zip and merge-csvs: a single-file request fell through to the worker and returned 422; the factory now supports minInputs and returns 400 pre-enqueue.

UI:
- review-panel: the result card showed "Saved +X%" when the output grew; the savings row now appears only when the file is actually smaller (Original/Processed sizes always shown).

Found via two adversarial code-review passes over the 10 Data routes. All 24 fix + regression checks pass against a fresh Docker stack on :1359.

* fix(data): clean 400 for unsafe-zip entries; drop header on split keepHeader=false

- tool-factory: add an opt-in preValidate hook that runs after input prep and
  before enqueue. Throwing InputValidationError there returns its statusCode
  (400) instead of the worker's generic 422. BullMQ loses the error class across
  the job boundary, so InputValidationErrors thrown in the worker cannot be
  mapped to their status; pre-enqueue validation can.
- extract-zip: validate entry paths via preValidate, rejecting path-traversal
  and absolute-path archives (and unreadable/corrupt zips) with a clear 400. The
  processV2 guards remain as defense-in-depth for the pipeline/batch path.
- split-csv: keepHeader=false now drops the header (parts contain only data
  rows) instead of keeping it as the first data row of part-1.

Verified against a fresh Docker stack: unsafe / absolute / corrupt zips -> 400,
normal zip still 200; split keepHeader=false drops the header while true repeats
it in each part. No regressions across 51 fix + scenario checks.

* feat(image-tools): deepen image-pad and sprite-sheet, fix sprite-sheet multi-file submit

Tier C of the image-tool depth pass.

- image-pad: a custom W:H ratio alongside the presets, a background mode
  (solid color, transparent, or an Instagram-style blurred cover fill), and an
  extra padding margin. The settings panel gains a real live preview of the
  padded canvas (it previously declared live-preview but rendered nothing) via
  onImageStyle + onImageOverlay.
- sprite-sheet: PNG/WebP/JPEG output with a quality control, and the coordinate
  map it already computes is now returned and surfaced as Copy CSS (per-frame
  background-position rules) and Copy JSON exports.

Also fix a pre-existing sprite-sheet bug: with more than one image the panel
called processAllFiles, fanning out to the per-file batch route (422). It now
calls processFiles, which packs all images into a single sheet request (it is a
MULTI_FILE tool). Updates both integration tests.

* fix(media): preserve source sample rate after loudnorm (#243)

ffmpeg's loudnorm filter runs internally at 192 kHz and emits 192 kHz
unless the chain resamples back. normalize-audio and video-loudnorm
therefore produced 192 kHz output (4.3x larger files) regardless of the
input rate. Append aresample to restore the input's sample rate.
runMediaTool now exposes the input audio sample rate to its args callback.

* fix(color-palette): collapse solid-color images to one swatch

The median-cut bucket selector started bestRange at -1, so a uniform bucket
(range 0) still satisfied the > comparison and kept splitting, yielding N
identical swatches for a solid-color image. Start at 0 so only buckets with
real color spread are split.

* fix(lint): annotate implicit-any lets in saml and user-files

biome noImplicitAnyLet flagged the bare let in saml.ts (profile) and user-files.ts (stream); add derived type annotations (type-only, no behavior change). Latent on main via the turbo lint cache; surfaced when the Data changes busted the apps/api lint cache.
2026-06-16 15:16:13 +08:00
SnapOtterandGitHub 3fb8164fa5 feat: add OpenTelemetry distributed tracing (enterprise) (#232)
* feat(tracing): add OpenTelemetry dependencies and --import preload flag

* feat(enterprise): add distributed_tracing feature gate

* feat(tracing): add SDK bootstrap with enterprise gating

* fix(tracing): correct test coverage for enterprise-unavailable path and prevent double-init

Test 2 now mocks @snapotter/enterprise to throw an import error, exercising
the catch block in the preload. Test 3 imports with no endpoint so the preload
is a no-op, avoiding leaked SDK from double-initialization. Added idempotency
guard to initTracing() as a safety net.

* feat(tracing): add Pino trace mixin and shared logger

When OTel tracing is active, every Pino log line now includes traceId,
spanId, and traceFlags fields for log-to-trace correlation. The mixin
is a no-op when no SDK is registered (community users).

* feat(tracing): add _otel to ToolJobData and inject trace context at enqueue

Add optional _otel carrier field to ToolJobData for W3C trace context
propagation across BullMQ job boundaries. When an active OTel span exists,
propagation.inject() writes traceparent/tracestate into the job data before
queue.add(). When no SDK is registered (community edition), the carrier
stays empty and _otel remains undefined -- zero overhead.

* feat(tracing): extract trace context and create spans in BullMQ worker

* feat(tracing): inject trace context into Python sidecar calls

* feat(tracing): add trace context extraction to Python sidecar

* feat(tracing): add shutdownTracing to graceful shutdown sequence

* feat(tracing): enrich HTTP spans with tool_id and user_id attributes

* docs: add OpenTelemetry env var documentation to .env.example

* test(tracing): add lifecycle integration tests for trace propagation

* fix(tracing): inject trace context into pipeline and batch flow jobs

* fix(tracing): add sidecar.execute Node-side span and remove unnecessary comment

Wraps PythonDispatcher.run() with a sidecar.execute span on the Node
side so traces show the full round-trip (Node span -> Python span).
Also removes an obvious comment from logger.ts.
2026-06-15 12:53:06 +08:00
SnapOtter 112df13957 test: add enterprise feature test coverage
Tests for SCIM, GDPR lifecycle, legal hold, audit export/archival,
encryption, SIEM forwarding, MFA endpoints, IP allowlist, config
export/import, webhook management, and license validation.
2026-06-14 16:50:35 +08:00
SnapOtter f9ed5d125a Merge branch 'feat/enterprise-on-prem' 2026-06-14 16:10:15 +08:00
SnapOtter e7bef34918 style: apply Biome formatting to enterprise files 2026-06-14 14:53:44 +08:00
SnapOtter 954cfb01a6 fix: test server registration gaps, Redis subscriber cleanup, atomic settings upsert 2026-06-14 14:30:35 +08:00
SnapOtter a1b5c6d2c3 feat(enterprise): add SCIM 2.0 provisioning (Users + Groups) 2026-06-13 22:42:55 +08:00
SnapOtter dd2a50799a feat(enterprise): add GDPR user/team data purge with audit redaction 2026-06-13 21:13:22 +08:00
SnapOtter b6a8226837 feat(enterprise): add GDPR user data export (async) 2026-06-13 21:07:28 +08:00
SnapOtter fa7da7ce0c feat(enterprise): add legal hold with cleanup bypass 2026-06-13 21:01:04 +08:00
SnapOtter d3f30a2f5d feat(enterprise): add SIEM webhook forwarding with circuit breaker 2026-06-13 16:53:32 +08:00
SnapOtter 913dd6bbe1 feat(enterprise): add audit log export endpoint (CSV/JSON) 2026-06-13 16:45:11 +08:00
SnapOtter 5d5240e481 feat: extend importBundleArchive for site-packages and fixups 2026-06-13 16:32:45 +08:00
SnapOtter 36f083ba64 feat(audit): add TOOL_EXECUTED logging with opt-in setting
Add isToolAuditEnabled() helper that checks the auditToolOperations
DB setting (off by default) or falls back to the enterprise
audit_export feature flag. The createToolRoute factory now emits a
TOOL_EXECUTED audit entry on successful tool execution when enabled,
using a fire-and-forget pattern so a failed audit write never blocks
the tool response.
2026-06-13 16:31:49 +08:00
SnapOtter 4e14e00a4c test(matrix): multi-modality format matrix covering video, audio, document, and data tools 2026-06-13 12:59:45 +08:00
SnapOtter e6e4ff921d test(matrix): settings variation matrix for tool-specific option combinations
Static data-driven test covering 90 tools with 551 variations. For each
tool that has configurable settings, every enum value, boundary number,
and boolean flag is exercised through the HTTP endpoint. The test asserts
no 500s occur and validates response shape for 200/202 results.

Coverage spans image (29 tools), video (20), audio (15), PDF/document
(21), and data (5) modalities.
2026-06-13 12:01:01 +08:00
SnapOtter 51666cdd5f feat(tools): 2.0 phase 5 wave 5b - ai pool: ocr-pdf, transcription, background composites (5 tools) (#226) 2026-06-13 10:19:47 +08:00
SnapOtter 6e1b9865f1 feat(tools): 2.0 phase 5 wave 5a - image gap-fill (11 tools) (#225) 2026-06-13 10:19:16 +08:00
SnapOtter fc7c1f850e feat(tools): 2.0 phase 5 wave 4 - office, ebooks, data, archives (14 tools) (#224) 2026-06-13 10:19:11 +08:00
SnapOtter 638288e196 feat(tools): 2.0 phase 5 wave 3b - audio depth (14 tools) (#222) 2026-06-13 10:19:06 +08:00
SnapOtter 5f98b48593 feat(tools): 2.0 phase 5 wave 3a - video depth (22 tools) (#221) 2026-06-13 10:19:00 +08:00
SnapOtter 2f39e38162 feat(tools): 2.0 phase 5 wave 2 - pdf depth (21 tools) (#220) 2026-06-13 10:18:55 +08:00
SnapOtter ae1337901d feat(tools)!: SnapOtter 2.0 phase 4 wave 1: 45 core tools across all modalities (#219) 2026-06-13 10:18:49 +08:00
SnapOtter d647d8ed19 feat(modality)!: SnapOtter 2.0 phase 3 modality framework: media/doc engines, pool routing, display modes (#218) 2026-06-13 10:18:39 +08:00
SnapOtter c451b939c7 feat(jobs)!: SnapOtter 2.0 phase 2 job spine: async queues, worker pools, object storage, admin dashboard (#217) 2026-06-13 10:17:13 +08:00
SnapOtterandGitHub 1c724d5d21 feat(db)!: SnapOtter 2.0 phase 1 foundation: postgres, migrator, compose stack (#216)
* feat(infra): add dev compose stack with postgres and redis

* fix(infra): comment dev env defaults until wired; harden dev compose restart and start_period

* chore(deps): add pg driver and testcontainers for postgres migration

* feat(db): translate schema to drizzle pg-core (timestamptz, boolean, pgEnum, jsonb)

Schema translation (apps/api/src/db/schema.ts):
- sqlite-core -> pg-core, all 10 tables preserved 1:1
- integer(mode:'timestamp') -> timestamp({ withTimezone: true })
- integer(mode:'boolean') -> boolean
- jobs.status text enum -> pgEnum('job_status') with same 4 values
- 7 columns changed from text to jsonb: jobs.inputFiles, jobs.settings,
  pipelines.steps, apiKeys.permissions, roles.permissions,
  auditLog.details, userFiles.toolChain
- settings.value stays text, jobs.error stays text, jobs.progress stays real

jsonb call-site sweep (removed JSON.stringify on writes, JSON.parse on reads):
- apps/api/src/routes/roles.ts: permissions read/write (3 sites)
- apps/api/src/routes/api-keys.ts: permissions write + read (2 sites)
- apps/api/src/routes/audit-log.ts: details read (1 site)
- apps/api/src/routes/pipeline.ts: steps write + read (2 sites)
- apps/api/src/routes/progress.ts: inputFiles write (2 sites)
- apps/api/src/routes/tool-factory.ts: toolChain read + write (2 sites)
- apps/api/src/routes/user-files.ts: toolChain read + write (4 sites)
- apps/api/src/permissions.ts: roles.permissions read (1 site)
- apps/api/src/lib/audit.ts: details write (1 site)
- apps/api/src/plugins/auth.ts: apiKeys.permissions read (1 site)

* refactor(db): type jsonb columns via $type and note raw CTE conversion requirements

* feat(db): archive sqlite migrations and generate postgres baseline

* chore(db): dockerignore legacy migrations, add archive breadcrumb, fix trailing newline

* feat(db): pg pool connection, advisory-locked boot migrations, DATABASE_URL config

* fix(db): friendly fatal on unreachable postgres, idempotent closeDb, lock-key convention note

* refactor(db): async drizzle calls in plugins, lib, permissions

* fix(api): analytics never throws, typed permission guard, single-query session invalidation

* refactor(db): async drizzle calls across all routes and bootstrap

Convert every route file and index.ts from sync SQLite drizzle
patterns to async node-postgres drizzle:

- .all() removed (bare await on select)
- .get() converted to destructured [row] = await ...
- .run() removed (bare await on insert/update/delete)
- .changes replaced with .rowCount (null-guarded) in progress.ts
- sqlite import removed from user-files.ts; raw CTEs converted to
  await db.execute(sql`...`) with postgres-dialect recursive CTEs
- ChainRow types updated: tool_chain is parsed jsonb (string[] | null),
  created_at is Date (timestamptz) with no * 1000 conversion
- All requirePermission() guard calls awaited (security: unawaited
  async guard returns truthy Promise, bypassing permission check)
- All hasEffectivePermission() and getPermissions() calls awaited
- All auditLog() calls awaited (preserves write-before-response order)
- trackEvent() and captureException() left un-awaited (fire-and-forget
  by design, guaranteed never-throw)
- ensureAnonymousUser(), startCleanupCron(), recoverStaleJobs() awaited
  in bootstrap sequence
- ensureInstanceId() and ensureDefaultSettings() made async

Files converted: 14 (index.ts + 12 route files + tools/index.ts)

* fix(db): await async checkStorageQuota in user-files upload/save routes

* fix(db): await checkStorageQuota in save-result route (missed second call site)

* feat(db): sqlite-to-postgres migrator with CLI and first-boot import

* fix(db): migrator error context, honest force semantics, boot-hook fatal, null-variance tests

* test: run suite against per-file postgres databases via testcontainers

- Add tests/global-setup.ts: spins up a Postgres testcontainer,
  creates a migrated template database once per vitest run.
- Rewrite tests/setup/per-fork-env.ts: each test file (forks pool)
  clones the template into its own database via CREATE DATABASE ...
  TEMPLATE, preserving the same per-file isolation granularity.
- Update vitest.config.ts: add globalSetup, pg alias, update comment.
- Fix tests/integration/test-server.ts: remove DB_PATH mkdir, async
  runMigrations, async db operations, remove SQLite WAL checkpoint.
- Fix 21 unit test db/index mocks: add pool and closeDb exports.
- Fix 8 unit test files: add async/await for now-async permission,
  audit, and analytics functions.
- Fix 18 integration test files: convert sync .run()/.all()/.get()
  to async drizzle patterns, add async to callbacks.
- Production change: apps/api/src/routes/teams.ts: cast COUNT(*)
  to ::int so Postgres returns a number instead of bigint string.

* fix(db): seed built-in roles, reject NUL bytes, cast COUNT, serialize job persists

- Seed built-in roles (admin, editor, user) at boot via ensureBuiltinRoles()
  with onConflictDoNothing, restoring data that legacy SQLite migration 0007
  provided via INSERT statements (the pg baseline is DDL-only).
- Reject NUL bytes in login credentials with 401 (postgres rejects \x00 in
  text columns; valid usernames never contain NUL, matching 1.x behavior).
- Cast COUNT(*)::int in user-files, audit-log, and roles listing queries so
  postgres returns a JS number instead of bigint-as-string.
- Serialize fire-and-forget job progress DB writes per jobId so the final
  "completed" status is never overwritten by a late-arriving "processing"
  write (race condition exposed by async postgres round-trips).

* test: fix teams race, seed roles in test server, poll for job status

- Add missing await to resetTeams() in teams PUT beforeEach (the async
  delete raced with the subsequent insert under postgres).
- Call ensureBuiltinRoles() in test server bootstrap so integration tests
  have the same built-in roles as production.
- Replace fixed 100ms flushPersist delay with a polling helper that waits
  for terminal job status, eliminating timing-dependent failures caused by
  postgres network round-trip latency.

* test: make heic temp-file cleanup assertion resilient to concurrent workers

Use a set-based diff instead of raw file count when checking that
decodeHeic cleans up temp files. Other concurrent test workers can
create heic-in-*/heic-out-* files in the shared tmpdir, inflating the
"after" count and causing spurious failures under full-suite load.

* fix(db): align builtin-role seed to post-0010 legacy state; test polish

* feat(docker): three-container compose (app, postgres, redis) with boot wait and migrations

* fix(docker): set TEST_DATABASE_URL so containerized tests skip testcontainers

* chore(docker): test compose project name, clearer 1.x upgrade comment, unref probe timer

* feat(enterprise): enforce D15 license boundary; move s3 storage into packages/enterprise

* fix(enterprise): restore lazy aws-sdk loading; community installs load no s3 code at boot

* fix(enterprise): boundary check catches dynamic imports; document getS3 concurrency

* feat(db)!: SnapOtter 2.0 phase 1 foundation: postgres, migrator, compose stack

BREAKING CHANGE: SQLite is no longer the runtime database. Deployments now
require Postgres (and Redis, used from phase 2). Existing installs migrate
with SQLITE_MIGRATE_PATH or 'pnpm --filter @snapotter/api migrate:sqlite'.

* fix(ci): postgres service + fresh e2e database per run; ignore unfixable torch CVE-2025-3000
2026-06-13 10:15:23 +08:00
SnapOtterandGitHub 4ec39c556f test: testing overhaul -- CI e2e gates, parallel suites, generated matrices, mutation testing (#215)
Closes the "e2e never runs in CI" hole. Adds per-PR e2e smoke gate,
nightly full-suite workflows, parallel vitest forks (per-fork DBs),
Playwright parallel/serial/visual projects against production builds,
metadata-generated test suites (drift guards, hostile inputs, format
matrix, pairwise settings, property-based fuzz), Stryker mutation
testing, Schemathesis API fuzz, coverage ratchet, and fixes for three
session-poisoning bugs that caused 200+ serial-bucket failures.

Bug fix included: favicon/split/bulk-rename could hang clients forever
when ZIP streaming failed after reply.hijack().
2026-06-10 22:01:13 +08:00
SnapOtter 78679c57f4 fix(test): update import paths for cleanup.test.ts after move to integration 2026-06-10 00:24:42 +08:00
SnapOtter 5b3b3c7632 fix(test): mock db/index.js in unit tests that import API modules
Five unit tests crashed because they imported modules that transitively
reached db/index.ts, which eagerly calls `new Database()` at module load
time. Added vi.mock for db/index.js matching the pattern used by 16
other passing API unit tests.

Moved cleanup.test.ts to tests/integration/ since it uses a real SQLite
database, runs migrations, and inserts rows.
2026-06-09 23:34:36 +08:00