Files
yoi/web/workspace/tests/workspace-model.test.ts

462 lines
14 KiB
TypeScript

declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
readTextFile(path: URL): Promise<string>;
};
import {
parseCreateWorkspaceRepositoryResponse,
parseRepositoryApiError,
parseRepositoryListApiResult,
parseRepositoryListResponse,
parseWorkspaceCreateResponse,
parseWorkspaceDeletionOperationResponse,
parseWorkspaceDeletionPreflightResponse,
parseWorkspaceResponse,
REPOSITORY_API_LIMITS,
} from "../src/lib/workspace/api/workspace-model.ts";
import {
creationResponse,
currentInitialRepository,
} from "../src/lib/workspace/api/workspace-catalog.test-fixtures.ts";
Deno.test("workspace creation decodes absent and deleted initial repositories as explicit null", () => {
for (const replayed of [false, true]) {
const parsed = parseWorkspaceCreateResponse(
JSON.parse(JSON.stringify(creationResponse(null, replayed))),
);
if (parsed.repository !== null || parsed.replayed !== replayed) {
throw new Error("nullable creation response was not preserved");
}
}
});
Deno.test("workspace creation replay preserves the returned current repository record", () => {
const parsed = parseWorkspaceCreateResponse(
creationResponse(currentInitialRepository, true),
);
if (
JSON.stringify(parsed.repository) !==
JSON.stringify(currentInitialRepository)
) throw new Error("current repository record was replaced or lost");
});
Deno.test("workspace creation rejects missing and malformed repository response fields", () => {
for (
const value of [undefined, false, {}, {
...currentInitialRepository,
source_fingerprint: -1,
}]
) {
assertThrows(
() =>
parseWorkspaceCreateResponse({
...creationResponse(),
repository: value,
}),
"workspace create response.repository",
);
}
});
function assertThrows(operation: () => unknown, expected: string): void {
try {
operation();
} catch (error) {
if (error instanceof Error && error.message.includes(expected)) return;
throw error;
}
throw new Error("expected operation to throw");
}
const repositoryList = {
workspace_id: "w-a",
items: [{
repository_key: "main",
kind: "git",
provider: "git",
source: { kind: "local_path", uri: "/srv/alpha" },
source_fingerprint: "sha256:alpha",
observed_status: "ready",
record_authority: "workspace-control-plane",
}],
source: "workspace-control-plane",
diagnostics: [],
};
Deno.test("generated repository wrapper validates current Backend JSON", () => {
const parsed = parseRepositoryListResponse(repositoryList);
if (parsed.items[0]?.repository_key !== "main") {
throw new Error("repository key was not preserved");
}
if (parsed.items[0]?.source.kind !== "local_path") {
throw new Error("repository source kind was not preserved");
}
});
Deno.test("repository git optional nullable fields accept omission and null", () => {
const omitted = structuredClone(repositoryList) as Record<string, unknown>;
const omittedItems = omitted.items as Array<Record<string, unknown>>;
omittedItems[0].git = {
status: "available",
dirty: false,
remotes: [],
};
const omittedParsed = parseRepositoryListResponse(omitted);
if (
omittedParsed.items[0]?.git?.head !== undefined ||
omittedParsed.items[0]?.git?.branch !== undefined
) {
throw new Error("omitted git fields were not preserved");
}
const nullable = structuredClone(repositoryList) as Record<string, unknown>;
const nullableItems = nullable.items as Array<Record<string, unknown>>;
nullableItems[0].git = {
status: "available",
head: null,
branch: null,
dirty: false,
remotes: [],
};
const nullableParsed = parseRepositoryListResponse(nullable);
if (
nullableParsed.items[0]?.git?.head !== null ||
nullableParsed.items[0]?.git?.branch !== null
) {
throw new Error("null git fields were not preserved");
}
});
Deno.test("plain HTTP repository source kind fails closed at the JSON boundary", () => {
const stale = structuredClone(repositoryList) as Record<string, unknown>;
const items = stale.items as Array<Record<string, unknown>>;
items[0].source = {
kind: "http",
uri: "http://git.example.test/team/project.git",
};
assertThrows(
() => parseRepositoryListResponse(stale),
".source.kind is invalid",
);
});
Deno.test("stale repository aliases fail closed at the JSON boundary", () => {
const stale = structuredClone(repositoryList) as Record<string, unknown>;
const items = stale.items as Array<Record<string, unknown>>;
items[0].id = items[0].repository_key;
delete items[0].repository_key;
assertThrows(
() => parseRepositoryListResponse(stale),
".id is not part",
);
});
Deno.test("repository JSON rejects unknown fields", () => {
const stale = structuredClone(repositoryList) as Record<string, unknown>;
const items = stale.items as Array<Record<string, unknown>>;
items[0].unexpected_field = 1;
assertThrows(
() => parseRepositoryListResponse(stale),
".unexpected_field is not part of the wire contract",
);
});
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: {
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",
display_name: "Alpha",
record_authority: "workspace-control-plane",
schema_version: 46,
auth: {
Passkey: {
rp_id: "example.test",
origin: "https://example.test",
public_base_url: "https://example.test",
cookie_name: "yoi_session",
},
},
extension_points: {
store: "sqlite",
event_stream: { status: "available", note: "ready", diagnostics: [] },
host_worker_bridge: {
status: "available",
note: "ready",
diagnostics: [],
},
companion_console: {
status: "available",
note: "ready",
diagnostics: [],
},
},
};
assertThrows(
() => parseWorkspaceResponse(stale),
"permissions must be an object",
);
});
Deno.test("Workspace deletion DTOs fail closed and preserve durable operation state", () => {
const preflight = parseWorkspaceDeletionPreflightResponse({
workspace_id: "workspace-a",
display_name: "Alpha",
expected_workspace_updated_at: "2026-01-01T00:00:00Z",
can_delete: true,
resources: {
workers: 2,
workdirs: 1,
repositories: 1,
runtime_bindings: 1,
secrets: 0,
artifacts: 3,
},
blockers: [],
});
if (preflight.resources.workers !== 2) {
throw new Error("worker count was not preserved");
}
const operation = parseWorkspaceDeletionOperationResponse({
operation_id: "delete-alpha",
workspace_id: "workspace-a",
display_name: "Alpha",
state: "blocked",
resources: preflight.resources,
child_operation_ids: ["worker-remove:arcadia/7"],
blockers: [{
kind: "dirty_workdir",
resource_kind: "workdir",
resource_key: "WD-1",
message: "Workdir is dirty",
}],
failure_category: null,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:01:00Z",
completed_at: null,
});
if (operation.state !== "blocked") {
throw new Error("operation state was not preserved");
}
assertThrows(
() =>
parseWorkspaceDeletionPreflightResponse({
...preflight,
unexpected: true,
}),
"unexpected is not part",
);
assertThrows(
() =>
parseWorkspaceDeletionPreflightResponse({
...preflight,
expected_workspace_updated_at: "x".repeat(129),
}),
".expected_workspace_updated_at is too long",
);
for (const parse of [parseWorkspaceDeletionPreflightResponse, parseWorkspaceDeletionOperationResponse]) {
const value = parse === parseWorkspaceDeletionPreflightResponse ? preflight : operation;
assertThrows(
() => parse({ ...value, blockers: [{ ...operation.blockers[0], kind: "unknown_blocker" }] }),
".kind is invalid",
);
}
assertThrows(
() =>
parseWorkspaceDeletionOperationResponse({
...operation,
state: "unknown",
}),
".state is invalid",
);
assertThrows(
() =>
parseWorkspaceDeletionOperationResponse({
...operation,
operation_id: "x".repeat(129),
}),
".operation_id is too long",
);
assertThrows(
() =>
parseWorkspaceDeletionOperationResponse({
...operation,
blockers: Array.from({ length: 1025 }, () => operation.blockers[0]),
}),
".blockers has too many items",
);
});
Deno.test("Workspace settings exposes owner-gated typed destructive confirmation", async () => {
const source = (await Promise.all([
"../src/routes/w/[workspaceId]/settings/PageContent.svelte",
"../src/lib/workspace/settings/WorkspaceIdentitySettings.svelte",
].map((path) => Deno.readTextFile(new URL(path, import.meta.url))))).join(
"\n",
);
for (
const token of [
"permissions.delete_workspace",
"preflightWorkspaceDeletion",
"startWorkspaceDeletion",
"deletionConfirmation",
"disposeWorkspaceMultiplexer(workspaceId)",
"disposeWorkspaceWorkersStore(workspaceId)",
"sessionStorage.setItem(deletionStorageKey",
"storedDeletionRequest()",
"trackDeletion(request.operation_id)",
]
) {
if (!source.includes(token)) {
throw new Error(`Workspace deletion UI should include ${token}`);
}
}
});
Deno.test("Repository settings consume the validated shared wire shape", async () => {
const [loadSource, pageSource, apiSource, modelSource] = await Promise.all([
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/repositories/+page.ts",
import.meta.url,
),
),
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/repositories/PageContent.svelte",
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 (!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}`);
}
}
if (pageSource.includes("kind === 'http'")) {
throw new Error("Repository Access must not support plain HTTP sources");
}
for (
const staleToken of [
"repository.id",
"repository.display_name",
"repository.source.kind === 'remote_git'",
]
) {
if (loadSource.includes(staleToken) || pageSource.includes(staleToken)) {
throw new Error(`Repository settings must not use ${staleToken}`);
}
}
});