Follow-up to #191: it tightened page/limit/per-page, but five filter flags still used bare z.coerce.number() and accepted negative and fractional values that were sent raw to the portals (e.g. --jobage=-5, --radius=2.5).
jobindex --jobage, jobnet --radius, jobdanmark --category/--jobtitle-id and jobbank --company now use .int().min(1), mirroring #191. Adds 8 network-free regression tests (a negative and a fractional case per flag) using the same validation-error pattern as the existing cli-flag-validation suites.
* test(cli): pin the 429/5xx retry contract in all six portal CLIs
The portal-skill contract requires backoff on 429/5xx, and every CLI
implements it - a retry loop with exponential delay and jitter - but
nothing verified the loops actually retry, stop retrying on plain
4xx, or give up after the documented attempt budget. A regression
here is invisible: a CLI that stops retrying still works on every
healthy request.
Each CLI gains tests/retry-backoff.test.ts, network-free, using the
request-timeout.test.ts pattern from #197 (import the fetch wrapper,
stub globalThis.fetch): a stubbed fetch counts attempts, and a
stubbed setTimeout fires immediately so the exhaustion case does not
sleep through the real 500ms -> 5s/8s backoff schedule (tests run in
milliseconds, not ~17s).
Three assertions per fetch wrapper, adapted to each CLI's documented
semantics:
- a 429 is retried and the next attempt's result is returned
- a plain 4xx is not retried (jobbank's fetchWithUA RETURNS the
response for callers to handle - pinned as such; linkedin's
htmlFetch returns "" on 404; freehire's apiGet returns null)
- persistent 5xx gives up after the initial attempt plus six
retries (7 fetch calls) with the status in the error
freehire additionally pins its documented graceful-degradation
contract: a connection failure fails fast with no retry. jobdanmark
exercises both apiFetch and apiPost, which carry separate copies of
the loop that could drift apart.
Mutation-checked: changing maxRetries in jobindex makes the
exhaustion test fail, so the tests distinguish the current behavior
from a silently altered one.
Verified: bun test green in all six CLIs (jobindex 19, jobnet 20,
jobbank 20, jobdanmark 21, linkedin 21, freehire 31 - 0 fail);
tsc --noEmit clean in all six; python3 tools/lint_skills.py OK.
* test(jobindex): pin apiFetch's retry loop alongside htmlFetch's
Review parity gap: jobindex carries two separate copies of the retry
loop and only htmlFetch was exercised, so apiFetch's retry budget
could drift silently - the same situation jobdanmark's test already
handles for its apiFetch/apiPost pair.
apiFetch gets the same three assertions, adapted to its documented
semantics (JSON return on success, throw on plain 4xx): a 429 is
retried and the next attempt's parsed body returned, a 400 is not
retried, persistent 5xx gives up after the initial attempt plus six
retries (7 calls).
Mutation-checked on the new axis: changing apiFetch's maxRetries
(the file's first copy of the loop) fails its exhaustion test while
htmlFetch's tests stay green, so each wrapper is now pinned
independently.
Verified: bun test 30 pass / 0 fail (full jobindex suite);
tsc --noEmit clean.
None of the board CLIs set a fetch timeout, and the retry loops react only to HTTP status codes, not to a connection that is accepted then never responds (black-holed TCP, hung TLS, stalled proxy) - so await fetch(...) never settles and the command hangs with no output and no exit. freehire's helper even documented a fast-degrade contract its try/catch didn't deliver on a mid-flight stall. Adds signal: AbortSignal.timeout(15000) to every fetch across all six CLIs, with network-free tests asserting the signal is present on each request wrapper.
By @thejesh23. Verified: 8 timeout tests pass locally with fetch stubbed (no network), and would fail on the pre-fix code.
Closes#196
Bare z.coerce.number() accepted negative and fractional values for count/pagination flags, and slice(0, limit) with a negative limit silently dropped trailing results instead of erroring. Tightens the schemas to .int().min(1) across all four Danish portal CLIs (including jobnet occupations --per-page) with network-free validation tests.
By @ayobamiseun.
Extend the offline CLI contract tests to the two remaining Danish
portal CLIs. Both implement the documented error contract (JSON
errors on stderr, exit 1) but had no test locking it in:
- jobindex-search: search without --query, detail without an ID,
and bunli numeric-option validation (--page not-a-number)
- jobnet-search: detail without an ID, occupations without
--search-string, suggestions without --query, and numeric-option
validation
All asserted paths exit before any network request, matching the
no-live-portal-requests CI policy. Assertions were written against
observed CLI output, not assumed shapes.