think
16px
820px

Gateway P2 Task 7 (Job API + manager) — verify + fix report

NOTE: .superpowers/sdd/task-7-report.md already existed and holds an UNRELATED
report ("Task 7: Queue events + opt-in early SSE + keepalives", dated Jul 4).
To avoid clobbering it this report was written to task-7-jobs-report.md.

Branch: feat/gateway-p2-jobs-ocr
Commit: efa29d153328167750f5da58c9769768e55d43f4
Subject: feat(p2): Job API endpoints + manager single-job path + server wiring
Status: GREEN — build, vet, race (jobs+server), and full suite all pass. Committed.

Results

Step Command Result
Build go build ./... clean (BUILD_OK)
Vet go vet ./... clean (VET_OK)
Race — targeted go test -race ./internal/jobs/ ./internal/server/ ok jobs 3.655s, ok server 1.479s
Full suite go test ./... all ok

Full-suite package results:

?   cmd/gateway            [no test files]
ok  internal/audit         2.195s
ok  internal/config        0.018s
ok  internal/idem          0.013s
ok  internal/jobs          1.686s
ok  internal/pool          1.634s
ok  internal/proxy         2.568s
ok  internal/registry      0.049s
ok  internal/server        0.258s

Fixes made

1 fix — test fixture in internal/jobs/manager_test.go (mgrFixture, ~lines 48-54)

Symptom: TestManagerRunsJobToCompletion (and every other manager test) hung; go test
hit the 5-minute wall. The stack dump showed a worker parked in Manager.process at
manager.go:265 — the slot-admission select waiting on tk.Ready() — while the test's
deferred m.Stop(context.Background()) deadlocked in wg.Wait() because that worker never
returned.

Root cause: The fixture built pools directly from config.Slots{Total: 1}, leaving
BatchMax = 0. Every fixture job is submitted with config.ClassBatch. In
pool.localPool.pump (pool.go:124) a batch ticket is only admitted when
activeBatch < slots.BatchMax; with BatchMax = 0 that is never true, so batch jobs are
NEVER admitted and the worker blocks for the full 30-minute SlotWaitTimeout.

This is a fixture-only defect, not a manager/pool bug: production config never has
BatchMax = 0. config.go:132-133 normalizes BatchMax to Total whenever it is <= 0,
and main.go builds pools from that validated config. The fixture bypasses
config.Load/validation, so it must set BatchMax itself to mirror the normalized
production value.

Change: set BatchMax: 1 alongside Total: 1 on both fixture upstreams (t7m-ocr,
t7m-ext) — i.e. config.Slots{Total: 1, BatchMax: 1}, the exact value validation would
have produced. No test assertion was weakened; the single-slot admission discipline the
tests rely on (release-on-every-terminal-state, sequential 2-job runs) is preserved.

-               Slots: config.Slots{Total: 1}},
+               Slots: config.Slots{Total: 1, BatchMax: 1}},   (x2)

No changes were needed in manager.go, api.go, server.go, main.go, or api_test.go.

Note for review (latent, not triggered — no change made)

Manager.Stop(ctx) waits on wg.Wait() before calling m.cancel(), and
Manager.process's slot-admission select (manager.go:265) watches only tk.Ready(),
time.After(SlotWaitTimeout), and m.ctx.Done() — not stopClaim. If a worker is ever
blocked waiting for a slot at drain time, Stop will not cancel until SlotWaitTimeout
(default 30m) elapses, since cancel() runs only after wg.Wait() returns (or the Stop
ctx deadline fires — context.Background() has none). This was the amplifier that turned
the fixture bug into a full hang. It does not fire on the happy path (slot granted → worker
returns → Stop completes) and all tests are green, so per the "verify + fix defects, do not
redesign" scope it was left as-is. Worth a follow-up: have the slot-wait select also observe
stopClaim, or cancel before wg.Wait() in Stop.