fix(catalyst-ui): JobDetail fetches /jobs/{id} with RAW colon, not %3A (#305 follow-up 3)
The browser auto-encodes `:` to `%3A` when encodeURIComponent is applied to a path segment. Chi's router does NOT decode %3A before matching the route, so every JobDetail fetch returned 404 against the catalyst-api. Live evidence (Playwright network log on otech wizard, 2026-04-30): GET https://console.openova.io/sovereign/api/v1/deployments/ ce476aaf80731a46/jobs/ce476aaf80731a46%3Ainstall-seaweedfs → 404 Internal probe with the raw colon: wget http://localhost:8080/api/v1/deployments/.../jobs/ ce476aaf80731a46:install-seaweedfs → 200 Result on the live deployment: every JobDetail page rendered the "Execution metadata pending" placeholder even though the catalyst-api DID have a valid execution to surface. Bug is in the FE encoder, not the backend or the route. Fix: - useJobDetail inserts jobId raw into the URL template. The colon is RFC 3986 path-safe so this is correct per spec. - deploymentId stays encodeURIComponent'd defensively (it's a hex string, no-op in practice, but the encode is cheap insurance). - Test now asserts the URL contains the raw `:` and rejects %3A. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
87c8626d92
commit
dc2656cf3e
@ -296,10 +296,12 @@ describe('JobDetail — Exec Log tab wires the real execution id (regression for
|
||||
json: () => Promise.resolve({ jobs: [job] }),
|
||||
} as unknown as Response)
|
||||
}
|
||||
// /jobs/{jobId} (detail) → emit the executions[].
|
||||
// /jobs/{jobId} (detail) → emit the executions[]. The jobId is
|
||||
// inserted raw (NOT encodeURIComponent'd) so the colon survives
|
||||
// — chi's path matcher rejects %3A. See useJobDetail.ts.
|
||||
if (
|
||||
url.endsWith(
|
||||
`/v1/deployments/${encodeURIComponent(deploymentId)}/jobs/${encodeURIComponent(jobId)}`,
|
||||
`/v1/deployments/${encodeURIComponent(deploymentId)}/jobs/${jobId}`,
|
||||
)
|
||||
) {
|
||||
return Promise.resolve({
|
||||
@ -376,6 +378,12 @@ describe('JobDetail — Exec Log tab wires the real execution id (regression for
|
||||
.toBe(true)
|
||||
// Synthetic `:latest` id MUST NOT appear in any URL.
|
||||
expect(seenUrls.some((u) => u.includes(`${jobId}:latest`))).toBe(false)
|
||||
// The jobId in the detail-fetch URL must use the RAW colon, not
|
||||
// %3A — chi's path matcher does not decode %3A and would 404
|
||||
// every detail fetch.
|
||||
const detailHits = seenUrls.filter((u) => u.includes('/v1/deployments/') && u.includes('/jobs/'))
|
||||
expect(detailHits.some((u) => u.endsWith(`/jobs/${jobId}`))).toBe(true)
|
||||
expect(detailHits.some((u) => u.includes('%3A'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -96,9 +96,15 @@ async function defaultFetchJobDetail(
|
||||
deploymentId: string,
|
||||
jobId: string,
|
||||
): Promise<JobDetailResponse> {
|
||||
// jobId is the canonical "<deploymentId>:<jobName>" string. The colon
|
||||
// is RFC 3986 path-safe, but encodeURIComponent turns it into %3A,
|
||||
// which chi's path matcher does NOT decode before route lookup —
|
||||
// every detail fetch returns 404 with the encoded form. Insert the
|
||||
// jobId raw; deploymentId is a 16-byte hex with no special chars so
|
||||
// encoding it is a no-op.
|
||||
const url =
|
||||
`${API_BASE}/v1/deployments/${encodeURIComponent(deploymentId)}` +
|
||||
`/jobs/${encodeURIComponent(jobId)}`
|
||||
`/jobs/${jobId}`
|
||||
const res = await fetch(url, { headers: { Accept: 'application/json' } })
|
||||
if (res.status === 404) {
|
||||
throw new JobNotFoundError(deploymentId, jobId)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user