From 9de2afbfc61378d98f6ae510942c2a5fd8590b01 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 1 Sep 2026 02:37:44 +0900 Subject: [PATCH] fix: validate ticket repository summaries --- .../src/lib/workspace/api/workspace-model.ts | 17 +++++++++++++++++ .../[workspaceId]/tickets/[ticketId]/+page.ts | 12 ++++++------ web/workspace/tests/workspace-model.test.ts | 19 +++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/web/workspace/src/lib/workspace/api/workspace-model.ts b/web/workspace/src/lib/workspace/api/workspace-model.ts index 11317781..2721396c 100644 --- a/web/workspace/src/lib/workspace/api/workspace-model.ts +++ b/web/workspace/src/lib/workspace/api/workspace-model.ts @@ -1,3 +1,4 @@ +import type { ApiResult } from "$lib/workspace/api/http"; import type { Diagnostic, GitCommitSummary, @@ -529,6 +530,22 @@ export function parseRepositoryListResponse( }; } +export function parseRepositoryListApiResult( + result: ApiResult, +): ApiResult { + if (result.data === null) return { data: null, error: result.error }; + try { + return { data: parseRepositoryListResponse(result.data), error: null }; + } catch (cause) { + return { + data: null, + error: cause instanceof Error + ? cause.message + : "invalid repository list response", + }; + } +} + export function parseRepositoryDetailResponse( value: unknown, ): RepositoryDetailResponse { diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts index 3f5bb339..c3ce05a1 100644 --- a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.ts @@ -1,14 +1,12 @@ import { redirect } from "@sveltejs/kit"; import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; +import { parseRepositoryListApiResult } from "$lib/workspace/api/workspace-model"; import { canonicalResourceReference, resourceKey, } from "$lib/workspace/resource-links"; import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel"; -import type { - RepositoryListResponse, - TicketDetail, -} from "$lib/workspace/sidebar/types"; +import type { TicketDetail } from "$lib/workspace/sidebar/types"; import type { PageLoad } from "./$types"; export const load = (async ({ fetch, params }) => { @@ -17,9 +15,9 @@ export const load = (async ({ fetch, params }) => { params.workspaceId, `/tickets/${encodeURIComponent(reference)}`, ); - const [ticket, repositories, orchestrator] = await Promise.all([ + const [ticket, repositoriesRaw, orchestrator] = await Promise.all([ loadJson(fetch, ticketPath), - loadJson( + loadJson( fetch, workspaceApiPath(params.workspaceId, "/repositories"), ), @@ -42,6 +40,8 @@ export const load = (async ({ fetch, params }) => { ); } } + const repositories = parseRepositoryListApiResult(repositoriesRaw); + return { workspaceId: params.workspaceId, ticketId: ticket.data?.id ?? reference, diff --git a/web/workspace/tests/workspace-model.test.ts b/web/workspace/tests/workspace-model.test.ts index 31b94b79..1a182a1c 100644 --- a/web/workspace/tests/workspace-model.test.ts +++ b/web/workspace/tests/workspace-model.test.ts @@ -3,6 +3,7 @@ declare const Deno: { }; import { + parseRepositoryListApiResult, parseRepositoryListResponse, parseWorkspaceResponse, } from "../src/lib/workspace/api/workspace-model.ts"; @@ -55,6 +56,24 @@ Deno.test("stale repository aliases fail closed at the JSON boundary", () => { ); }); +Deno.test("repository API result converts stale payloads into bounded page errors", () => { + const result = parseRepositoryListApiResult({ + data: { + workspace_id: "w-a", + items: { main: repositoryList.items[0] }, + source: "workspace-control-plane", + diagnostics: [], + }, + error: null, + }); + if (result.data !== null) { + throw new Error("stale payload must not reach the page"); + } + if (!result.error?.includes("items must be an array")) { + throw new Error(`unexpected bounded error: ${result.error}`); + } +}); + Deno.test("workspace response requires the permission projection", () => { const stale = { workspace_id: "w-a",