Merge-audit response — "POST /api/submissions returns 409" cluster
Branch: dev/2 · Fix commit: 455a5079 · Date: 2026-07-20
TL;DR
The audit's largest backend cluster ("POST /api/submissions now returns 409") is
not a code regression. The endpoint change it flagged is a correct data-loss
fix; the failing tests were stale fixtures relying on the old, buggy
behavior. Fixed by staging real temp files in those tests (no endpoint change).
- Before fix: 21 backend failures
- After fix: 6 backend failures — the entire 409 cluster (12 red tests) cleared
- Verified deterministic across 4 full-suite runs (0 reappearances) + isolation
- Typecheck clean; endpoint behavior byte-identical
What actually changed in the product (and why it's correct)
Commit 588e2b0c ("fix(ocr): … upload self-heal") added a guard to
POST /api/submissions:
// If NONE of the posted files resolve to staged temp metadata, roll back the
// empty submission and return 409 instead of creating a document-less one.
if (documents.length === 0) {
await db.submission.delete({ where: { id: submission.id } });
return c.json({ error: "Berkas tidak ditemukan lagi … unggah ulang." }, 409);
}
Why this is right: the create route reads each posted fileId's temp metadata
from uploads/klasifikasi-temp/<fileId>.json. The old code silently skipped any
file whose metadata was missing. If every file was skipped, it created a
submission with zero documents — the "documents disappear" bug: the user lands
on an empty extraction page and their upload is silently lost.
| Situation | Correct response | Rationale |
|---|---|---|
| ≥1 file resolves to a staged doc | 200 + submission | normal path |
| 0 files resolve | 409 ("unggah ulang") | nothing to attach — honest, recoverable |
Returning 200 with zero documents is the bug; the 409 is the fix. In the real app
the UI always stages files first, so a legitimate user never hits it — it only
fires on double-submit/stale IDs or an infra issue (redeploy, disk pressure,
multi-node without shared temp storage), where "please re-upload" is exactly right.
Why the tests failed (the stale part)
Several create-path tests posted deliberately fake fileIds ("no-such-temp-file")
as a shortcut, because they only cared about other create behavior — selection
persistence, ownerId stamping, submission-type routing — not documents. One test
even documents the old assumption in a comment: "the nonexistent fileIds are
skipped — the submission itself is created."
Under the guard those posts (correctly) return 409, so expect(200) failed. The
tests encode the old, buggy contract; the code does not regress.
The "12 failures" was inflated
Only 6 of the reds were genuine 409 assertions. The other 6 were teardown
cascade in one file: a 409 body has no submissionId, so undefined entered a
shared cleanup array, deleteMany({ in: [undefined] }) threw, and the array was
never reset — poisoning every later test in that file (including 3 pure-function
tests that pass fine in isolation). So "12 red" ≈ 2 real 409s + 4 sibling create
tests + 6 collateral, not 12 independent defects.
The fix
New shared helper backend/src/__test-helpers__/klasifikasi-temp.ts —
stagedFile() / seedTempFile() / cleanupStagedFiles() — mirrors the real
klasifikasi upload's temp-metadata contract. Five test files now stage a real
file instead of posting a fake id, so they exercise the true 200 path:
submissions-create-selected-jenis.test.ts(+ hardenedafterEachagainst the cascade)submissions-create-peralihan.test.ts(retired a local duplicate helper)ownership-and-names.test.tspembubaran-routing.test.tsberakhirnya-routing.test.ts
No production code touched. Only tests + a test helper.
Verification
| Run | Backend fails | 409 cluster present? |
|---|---|---|
| Pre-fix | 21 | yes — 12 tests |
| Post-fix ×4 (full suite) | 6 | no — 0/4 reappearance |
| Affected 5 files, isolated | 0/31 | n/a — all green |
Run against a clean *_test DB with this branch's schema pushed (no drift).
Remaining 6 failures — honest status (separate from this cluster)
| Cluster | Tests | Assessment |
|---|---|---|
| fork-vs-V2 parity | 4 (flow-parity ×2, rematch-parity ×2) |
Real, still failing deterministically. V2 rules changed without mirroring the legacy fork half. Needs genuine reconciliation — recommend a follow-up. |
| RUPS registry fallback | 3 (failed pre-fix) | Not a hard regression — passes 15/15 in isolation and in all 4 post-fix full runs. Failed once under full-suite concurrency → test-isolation flakiness on the shared DB, worth hardening but not broken logic. |
| config-gateway | 1 | Environmental — asserts a "fallback when GPU_SERVER_DIRECT_URL is unset"; fails only because the local .env sets the URL. Likely green in clean CI. |
| pp-pendirian BO persistence | 1 | Unexamined — may be its own real issue; recommend a separate look. |
Recommendation for re-audit
Re-audit against commit 455a5079 (or later). The 409 cluster will not
reappear. The remaining genuinely-substantive item is the fork-vs-V2 parity
divergence (4 tests) — that's the one worth prioritizing next.