feat: generate Repository frontend types from OpenAPI

This commit is contained in:
2026-09-18 20:26:20 +09:00
20 changed files with 1339 additions and 208 deletions
+139
View File
@@ -0,0 +1,139 @@
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
import {
createWorkspaceRepository,
loadWorkspaceRepositoryList,
REPOSITORY_API_MAX_RESPONSE_BYTES,
} from "../src/lib/workspace/api/repositories.ts";
const repository = {
repository_key: "main",
kind: "git",
provider: "git",
source: { kind: "local_path", uri: "/srv/main" },
source_revision: 1,
source_fingerprint: "sha256:main",
observed_status: "ready",
record_authority: "workspace-control-plane",
};
Deno.test("Repository API loaders parse unknown JSON through the strict generated contract", async () => {
const valid = await loadWorkspaceRepositoryList(
() =>
Promise.resolve(
Response.json({
workspace_id: "workspace-a",
items: [repository],
source: "workspace-control-plane",
diagnostics: [],
}),
),
"workspace-a",
);
if (valid.data?.items[0]?.repository_key !== "main") {
throw new Error(`unexpected load result: ${valid.error}`);
}
const invalid = await loadWorkspaceRepositoryList(
() =>
Promise.resolve(
Response.json({
workspace_id: "workspace-a",
items: [{ ...repository, observed_status: "future_status" }],
source: "workspace-control-plane",
diagnostics: [],
}),
),
"workspace-a",
);
if (
invalid.data !== null ||
!invalid.error?.includes("Repository API response is invalid")
) {
throw new Error("unknown enum values must fail closed in the loader");
}
});
Deno.test("Repository create client validates request, response, and typed API errors", async () => {
const success = await createWorkspaceRepository(
(_input, init) => {
const body = typeof init === "object" && init !== null && "body" in init
? init.body
: undefined;
const request: unknown = JSON.parse(String(body));
if (
typeof request !== "object" || request === null ||
!("repository_key" in request) || request.repository_key !== "main"
) {
throw new Error("typed create request was not serialized");
}
return Promise.resolve(
Response.json({
workspace_id: "workspace-a",
repository_key: "main",
replayed: false,
}),
);
},
"workspace-a",
{ repository_key: "main", source: "/srv/main", default_ref: null },
);
if (success.data?.repository_key !== "main" || success.data.replayed) {
throw new Error(`unexpected create result: ${success.error}`);
}
const rejected = await createWorkspaceRepository(
() =>
Promise.resolve(
Response.json(
{ error: "invalid_source", message: "source is invalid" },
{ status: 400 },
),
),
"workspace-a",
{ repository_key: "main", source: "bad", default_ref: null },
);
if (rejected.data !== null || rejected.error !== "source is invalid") {
throw new Error("typed RepositoryApiError message was not preserved");
}
const invalid = await createWorkspaceRepository(
() =>
Promise.resolve(
Response.json({
workspace_id: "workspace-a",
repository_key: "main",
replayed: null,
}),
),
"workspace-a",
{ repository_key: "main", source: "/srv/main", default_ref: null },
);
if (
invalid.data !== null ||
invalid.error !== "Repository API response is invalid"
) {
throw new Error("invalid create success payload must fail closed");
}
});
Deno.test("Repository API clients bound response bodies before parsing JSON", async () => {
const result = await loadWorkspaceRepositoryList(
() =>
Promise.resolve(
new Response("{}", {
headers: {
"content-length": String(REPOSITORY_API_MAX_RESPONSE_BYTES + 1),
},
}),
),
"workspace-a",
);
if (
result.data !== null || !result.error?.includes("exceeds its byte limit")
) {
throw new Error(`oversized response was not rejected: ${result.error}`);
}
});
+100 -2
View File
@@ -4,11 +4,14 @@ declare const Deno: {
};
import {
parseCreateWorkspaceRepositoryResponse,
parseRepositoryApiError,
parseRepositoryListApiResult,
parseRepositoryListResponse,
parseWorkspaceDeletionOperationResponse,
parseWorkspaceDeletionPreflightResponse,
parseWorkspaceResponse,
REPOSITORY_API_LIMITS,
} from "../src/lib/workspace/api/workspace-model.ts";
function assertThrows(operation: () => unknown, expected: string): void {
@@ -71,6 +74,81 @@ Deno.test("stale repository aliases fail closed at the JSON boundary", () => {
);
});
Deno.test("repository source revisions enforce the OpenAPI integer range", () => {
const negative = structuredClone(repositoryList) as Record<string, unknown>;
const negativeItems = negative.items as Array<Record<string, unknown>>;
negativeItems[0].source_revision = -1;
assertThrows(
() => parseRepositoryListResponse(negative),
"must be between 0 and Number.MAX_SAFE_INTEGER",
);
const unsafe = structuredClone(repositoryList) as Record<string, unknown>;
const unsafeItems = unsafe.items as Array<Record<string, unknown>>;
unsafeItems[0].source_revision = Number.MAX_SAFE_INTEGER + 1;
assertThrows(
() => parseRepositoryListResponse(unsafe),
"must be a safe integer",
);
});
Deno.test("repository create/error responses use the generated strict contract", () => {
const created = parseCreateWorkspaceRepositoryResponse({
workspace_id: repositoryList.workspace_id,
repository_key: "main",
replayed: false,
});
if (created.replayed || created.repository_key !== "main") {
throw new Error("create response fields were not preserved");
}
const error = parseRepositoryApiError({
error: "invalid_request",
message: "source is invalid",
});
if (error.error !== "invalid_request") {
throw new Error("error response fields were not preserved");
}
assertThrows(
() =>
parseCreateWorkspaceRepositoryResponse({
workspace_id: repositoryList.workspace_id,
repository_key: "main",
replayed: null,
}),
".replayed must be a boolean",
);
assertThrows(
() =>
parseRepositoryApiError({
error: "invalid_request",
message: "source is invalid",
context: {},
}),
".context is not part",
);
});
Deno.test("repository responses retain bounded strings and collections", () => {
const oversizedString = structuredClone(repositoryList);
oversizedString.items[0].provider = "x".repeat(
REPOSITORY_API_LIMITS.maxStringCodeUnits + 1,
);
assertThrows(
() => parseRepositoryListResponse(oversizedString),
"string limit",
);
const oversizedCollection = structuredClone(repositoryList);
oversizedCollection.items = Array.from(
{ length: REPOSITORY_API_LIMITS.maxCollectionEntries + 1 },
() => repositoryList.items[0],
);
assertThrows(
() => parseRepositoryListResponse(oversizedCollection),
"collection limit",
);
});
Deno.test("repository API result converts stale payloads into bounded page errors", () => {
const result = parseRepositoryListApiResult({
data: {
@@ -227,7 +305,7 @@ Deno.test("Workspace settings exposes owner-gated typed destructive confirmation
});
Deno.test("Repository settings consume the validated shared wire shape", async () => {
const [loadSource, pageSource] = await Promise.all([
const [loadSource, pageSource, apiSource, modelSource] = await Promise.all([
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/repositories/+page.ts",
@@ -240,21 +318,41 @@ Deno.test("Repository settings consume the validated shared wire shape", async (
import.meta.url,
),
),
Deno.readTextFile(
new URL("../src/lib/workspace/api/repositories.ts", import.meta.url),
),
Deno.readTextFile(
new URL("../src/lib/workspace/api/workspace-model.ts", import.meta.url),
),
]);
const targetSources = `${loadSource}\n${pageSource}\n${apiSource}`;
for (
const token of [
"loadWorkspaceRepositoryList",
"createWorkspaceRepository",
"parseRepositoryListResponse",
"parseCreateWorkspaceRepositoryResponse",
"parseRepositoryApiError",
"repository.repository_key",
"repository.observed_status",
"sourceLabel(repository.source.kind)",
"supportsRepositoryAccess(repository.source.kind)",
]
) {
if (!loadSource.includes(token) && !pageSource.includes(token)) {
if (!targetSources.includes(token) && !modelSource.includes(token)) {
throw new Error(`Repository settings should include ${token}`);
}
}
if (
pageSource.includes("generated/legacy-server-api") ||
pageSource.includes("response.json()") ||
pageSource.includes(" as CreateWorkspaceRepository")
) {
throw new Error(
"Repository create UI must use the generated contract and strict API client",
);
}
for (const kind of ["ssh", "https"]) {
if (!pageSource.includes(`kind === '${kind}'`)) {
throw new Error(`Repository Access should support ${kind}`);