fix(nightly): qpdf in test image, NUL-byte settings, AI sub-path fuzz exclude (#348)

Third round - the prior fixes unblocked these deeper failures on the nightly:

- Docker E2E: the patches/ fix (#346) let the build finish, so tests now
  run - and fail with 'spawnSync qpdf ENOENT'. Dockerfile.test installed
  imagemagick/ghostscript/exiftool but never qpdf, which the PDF tools and
  fixture-integrity checks need. Add it.
- NUL-byte 500 (real robustness bug Schemathesis found): a settings string
  containing U+0000 hits the jobs.settings jsonb insert and Postgres rejects
  it ('invalid byte sequence for encoding UTF8: 0x00'), 500ing tools like
  html-to-image. Strip NUL bytes from settings before the insert (NUL is
  never meaningful in tool settings). api typecheck passes.
- Schemathesis: the AI exclude (#346) only anchored on the tool id at the
  path end, so AI sub-endpoints like /passport-photo/analyze were still
  fuzzed and 501'd. Extend the regex to allow an optional sub-path.
This commit is contained in:
SnapOtter
2026-06-24 21:59:02 +08:00
committed by GitHub
parent 0f98f60c33
commit 078743d6b2
3 changed files with 20 additions and 2 deletions
+18 -1
View File
@@ -18,6 +18,23 @@ import { POOLS, type Pool, queueName, type ToolJobData, type ToolJobResult } fro
const queueEventsMap = new Map<Pool, QueueEvents>();
/**
* Recursively strip NUL (U+0000) bytes from a value. Postgres rejects NUL in
* text/jsonb ("invalid byte sequence for encoding UTF8: 0x00"), so a tool whose
* settings contain a NUL (e.g. a fuzzed string field) would 500 on the jobs
* insert. NUL is never meaningful in tool settings, so drop it.
*/
function stripNulBytes<T>(value: T): T {
if (typeof value === "string") return value.replace(/\0/g, "") as T;
if (Array.isArray(value)) return value.map(stripNulBytes) as T;
if (value && typeof value === "object") {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) out[k] = stripNulBytes(v);
return out as T;
}
return value;
}
function getQueueEvents(pool: Pool): QueueEvents {
let qe = queueEventsMap.get(pool);
if (!qe) {
@@ -110,7 +127,7 @@ export async function enqueueToolJob(data: ToolJobData): Promise<Job<ToolJobData
type: data.kind,
status: "queued",
inputRefs: data.inputRefs,
settings: (data.dbSettings ?? data.settings) as Record<string, unknown>,
settings: stripNulBytes((data.dbSettings ?? data.settings) as Record<string, unknown>),
});
// Fire-and-forget: compute deleteAfter from team retention override