Task 6 report — ClassifyAdapter (interactive-priority classify path)
Branch: feat/gateway-p2-jobs-ocr · base HEAD: 57e973d · package internal/jobs
Note: this file previously held a stale P1 streaming report reusing the same
task-6filename; it has been replaced with the P2 gateway Task 6 report.
What shipped
internal/jobs/adapter_classify.go— newClassifyAdapterimplementingAdapter.internal/jobs/adapter_classify_test.go— 10 table-free, namespace-safe tests (t6-*tenants,T6_*env) with anhttpteststub classifier.internal/jobs/adapter.go(MODIFY) — extracted the shared synchronous POST coresyncPost(...)and refactoredSyncHTTPAdapteronto it (see "How it shares" below).
How it shares vs. differs from SyncHTTPAdapter
Classify is a plain sync request/response call, so rather than copy-paste I extracted the common core into a package-level helper and pointed BOTH adapters at it:
syncPost(ctx, client, upstream, job, payload, target, maxBytes) (json.RawMessage, error)
syncPost owns everything the two adapters share, per the Task 4 sync contract:
- builds POST target with the payload;
- forwards the job's submit Content-Type (default application/json);
- server-side API-key injection from Upstream.APIKeyEnv (Authorization: Bearer …) — a client key is never forwarded;
- error mapping: 2xx → body verbatim; 429/503 → AdapterError{Retryable:true, RetryAfter}; anything else → AdapterError{Retryable:false, Code:"UPSTREAM_<n>"};
- ctx deadline/cancel → prompt ADAPTER_TIMEOUT (no goroutine/body leak);
- bounded body read via the shared readCapped/io.LimitReader helper (from adapter_azuredi.go) — no unbounded io.ReadAll; body closed on every path.
ClassifyAdapter stays a distinct type the manager can select for classify-operation upstreams. It differs from SyncHTTPAdapter only in:
- default submit path /classify (vs. /ocr), still overridable by Upstream.SubmitPath;
- being the type the manager routes classify to and applies interactive-priority defaults around (at submit, Task 7).
No priority/class is hardcoded in the adapter — per the brief, Job.Class is decided at submit in Task 7. The adapter just calls report("processing", 0) then syncPost.
Side benefit: the refactor gives SyncHTTPAdapter the same bounded read (it previously used an unbounded io.ReadAll, flagged in Task 4). Behavior is otherwise byte-identical; all pre-existing sync/azure tests stay green.
Verbatim passthrough proof (incl. low-confidence — NOT gated)
TestClassifyAdapter_Verbatim asserts a full {label,confidence,scores{…}} body returns byte-for-byte, with default path /classify, POST, intact payload, forwarded content-type, and report(processing,0) first.
TestClassifyAdapter_LowConfidencePassthrough is the contract-critical case: the stub returns {"label":"unknown","confidence":0.3} and the test asserts the result bytes are identical (string(res) == lowConf) and re-parse to {unknown, 0.3}. The gateway does not editorialize or gate on confidence — the OCR engine applies its own classifierMinConfidence downstream.
RED → GREEN
- RED: with
adapter_classify.gomoved aside,go test ./internal/jobs/→undefined: ClassifyAdapter(build failed). - GREEN: impl restored → all 10
TestClassifyAdapter_*pass under-race; fullinternal/jobspackage passes-race.
Test coverage: verbatim 200; low-confidence passthrough (unchanged); 500 non-retryable w/ code+status; 429 retryable w/ Retry-After; 503 retryable; ctx timeout & ctx cancel (prompt ADAPTER_TIMEOUT, no leak, 2s watchdog); API key injected when env set; absent when unset; bounded read caps an 8×-oversize body at MaxBytes.
Gate results
go build ./...— OKgo vet ./...— OKgo test -race ./internal/jobs/...— ok (classify + all pre-existing jobs tests)
Concerns / notes for downstream
- The
syncPostrefactor touches shipped Task 4 code. It is behavior-preserving except for adding the bounded read (a strict improvement, matching the Azure DI adapter's cap). Worth a glance in the whole-branch review. defaultSyncMaxBytes = 32<<20is shared by both sync + classify; classify responses are tiny, so this is generous headroom, not a tight bound. Overridable per-adapter viaMaxBytes.- Interactive-class default for classify is not here by design; it must be wired at submit in Task 7 (
default classify → interactive).
Files
internal/jobs/adapter_classify.go(new)internal/jobs/adapter_classify_test.go(new)internal/jobs/adapter.go(modified — sharedsyncPostextraction)