fix: validate workspace profile responses

This commit is contained in:
2026-09-02 15:24:33 +09:00
parent bad37ddc7d
commit 538da1f2b2
7 changed files with 506 additions and 143 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev", "dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts", "test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts",
"build": "deno run -A npm:vite@7.2.7 build", "build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview" "preview": "deno run -A npm:vite@7.2.7 preview"
}, },
@@ -1,8 +1,6 @@
export type Diagnostic = { import type { Diagnostic as WorkspaceApiDiagnostic } from "$lib/generated/workspace-api";
severity: "info" | "warning" | "error";
code: string; export type Diagnostic = WorkspaceApiDiagnostic;
message: string;
};
export type SettingsSectionId = export type SettingsSectionId =
| "runtimes" | "runtimes"
@@ -1,69 +1,335 @@
import type { import type {
Diagnostic,
DiagnosticSeverity,
ProfileSettingsResponse, ProfileSettingsResponse,
UpdateWorkspaceMetadataRequest,
WorkspaceMetadataMutationResponse, WorkspaceMetadataMutationResponse,
WorkspaceMetadataSettingsResponse, WorkspaceMetadataSettingsResponse,
} from "./profile-types"; WorkspaceProfileSourceProvenance,
WorkspaceProfileSourceSummary,
WorkspaceProfileSummary,
} from "$lib/generated/workspace-api";
export type WorkspaceProfileApi = { export class ProfileApiError extends Error {
getMetadata(workspaceId: string): Promise<WorkspaceMetadataSettingsResponse>; constructor(
updateMetadata( message: string,
workspaceId: string, readonly status: number,
displayName: string, ) {
expectedRevision: string, super(message);
): Promise<WorkspaceMetadataMutationResponse>; this.name = "ProfileApiError";
getProfiles(workspaceId: string): Promise<ProfileSettingsResponse>;
};
async function requestJson<T>(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<T> {
const response = await fetch(input, init);
if (!response.ok) {
throw new Error(`request failed: ${response.status}`);
} }
return (await response.json()) as T;
} }
export async function fetchWorkspaceMetadataSettings( type JsonRecord = Record<string, unknown>;
function record(value: unknown, context: string): JsonRecord {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value as JsonRecord;
}
function exactKeys(
value: JsonRecord,
required: readonly string[],
optional: readonly string[],
context: string,
): void {
const allowed = new Set([...required, ...optional]);
if (
required.some((key) => !(key in value)) ||
Object.keys(value).some((key) => !allowed.has(key))
) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
}
function stringValue(value: unknown, context: string): string {
if (typeof value !== "string") {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value;
}
function booleanValue(value: unknown, context: string): boolean {
if (typeof value !== "boolean") {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value;
}
function optionalString(
value: unknown,
context: string,
): string | null | undefined {
if (value === undefined || value === null) return value;
return stringValue(value, context);
}
function optionalRevision(
value: unknown,
context: string,
): number | null | undefined {
if (value === undefined || value === null) return value;
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value as number;
}
function arrayValue<T>(
value: unknown,
parser: (item: unknown) => T,
context: string,
): T[] {
if (!Array.isArray(value)) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value.map(parser);
}
function parseDiagnostic(value: unknown): Diagnostic {
const item = record(value, "Workspace settings");
exactKeys(item, ["code", "severity", "message"], [], "Workspace settings");
const severity = stringValue(item.severity, "Workspace settings");
if (!(["info", "warning", "error"] as string[]).includes(severity)) {
throw new ProfileApiError(
"Workspace settings returned an invalid response.",
502,
);
}
return {
code: stringValue(item.code, "Workspace settings"),
severity: severity as DiagnosticSeverity,
message: stringValue(item.message, "Workspace settings"),
};
}
export function parseWorkspaceMetadataSettingsResponse(
value: unknown,
): WorkspaceMetadataSettingsResponse {
const item = record(value, "Workspace metadata");
exactKeys(
item,
[
"workspace_id",
"display_name",
"created_at",
"revision",
"source",
"diagnostics",
],
[],
"Workspace metadata",
);
return {
workspace_id: stringValue(item.workspace_id, "Workspace metadata"),
display_name: stringValue(item.display_name, "Workspace metadata"),
created_at: stringValue(item.created_at, "Workspace metadata"),
revision: stringValue(item.revision, "Workspace metadata"),
source: stringValue(item.source, "Workspace metadata"),
diagnostics: arrayValue(
item.diagnostics,
parseDiagnostic,
"Workspace metadata",
),
};
}
export function parseWorkspaceMetadataMutationResponse(
value: unknown,
): WorkspaceMetadataMutationResponse {
const item = record(value, "Workspace metadata update");
exactKeys(
item,
["workspace", "diagnostics"],
[],
"Workspace metadata update",
);
return {
workspace: parseWorkspaceMetadataSettingsResponse(item.workspace),
diagnostics: arrayValue(
item.diagnostics,
parseDiagnostic,
"Workspace metadata update",
),
};
}
function parseWorkspaceProfileSummary(value: unknown): WorkspaceProfileSummary {
const item = record(value, "Profile catalog");
exactKeys(
item,
[
"profile_id",
"selector",
"label",
"source_kind",
"editable",
"is_default",
"diagnostics",
],
["profile_source_id", "description"],
"Profile catalog",
);
return {
profile_id: stringValue(item.profile_id, "Profile catalog"),
selector: stringValue(item.selector, "Profile catalog"),
label: stringValue(item.label, "Profile catalog"),
source_kind: stringValue(item.source_kind, "Profile catalog"),
profile_source_id: optionalString(
item.profile_source_id,
"Profile catalog",
),
description: optionalString(item.description, "Profile catalog"),
editable: booleanValue(item.editable, "Profile catalog"),
is_default: booleanValue(item.is_default, "Profile catalog"),
diagnostics: arrayValue(
item.diagnostics,
parseDiagnostic,
"Profile catalog",
),
};
}
function parseWorkspaceProfileSourceSummary(
value: unknown,
): WorkspaceProfileSourceSummary {
const item = record(value, "Profile source catalog");
exactKeys(
item,
[
"profile_source_id",
"display_path",
"kind",
"content_type",
"content_digest",
"provenance",
"editable",
"revision",
"size_bytes",
"diagnostics",
],
[],
"Profile source catalog",
);
const provenance = stringValue(item.provenance, "Profile source catalog");
if (provenance !== "project_profile_source_tree") {
throw new ProfileApiError(
"Profile source catalog returned an invalid response.",
502,
);
}
const sizeBytes = optionalRevision(item.size_bytes, "Profile source catalog");
if (sizeBytes === undefined || sizeBytes === null) {
throw new ProfileApiError(
"Profile source catalog returned an invalid response.",
502,
);
}
return {
profile_source_id: stringValue(
item.profile_source_id,
"Profile source catalog",
),
display_path: stringValue(item.display_path, "Profile source catalog"),
kind: stringValue(item.kind, "Profile source catalog"),
content_type: stringValue(item.content_type, "Profile source catalog"),
content_digest: stringValue(item.content_digest, "Profile source catalog"),
provenance: provenance as WorkspaceProfileSourceProvenance,
editable: booleanValue(item.editable, "Profile source catalog"),
revision: stringValue(item.revision, "Profile source catalog"),
size_bytes: sizeBytes,
diagnostics: arrayValue(
item.diagnostics,
parseDiagnostic,
"Profile source catalog",
),
};
}
export function parseProfileSettingsResponse(
value: unknown,
): ProfileSettingsResponse {
const item = record(value, "Profile settings");
exactKeys(
item,
["workspace_id", "registry_revision", "profiles", "sources", "diagnostics"],
["config_revision", "tree_digest", "projection_digest", "default_profile"],
"Profile settings",
);
return {
workspace_id: stringValue(item.workspace_id, "Profile settings"),
registry_revision: stringValue(item.registry_revision, "Profile settings"),
config_revision: optionalRevision(item.config_revision, "Profile settings"),
tree_digest: optionalString(item.tree_digest, "Profile settings"),
projection_digest: optionalString(
item.projection_digest,
"Profile settings",
),
default_profile: optionalString(item.default_profile, "Profile settings"),
profiles: arrayValue(
item.profiles,
parseWorkspaceProfileSummary,
"Profile settings",
),
sources: arrayValue(
item.sources,
parseWorkspaceProfileSourceSummary,
"Profile settings",
),
diagnostics: arrayValue(
item.diagnostics,
parseDiagnostic,
"Profile settings",
),
};
}
async function parseResponse<T>(
response: Response,
parser: (value: unknown) => T,
): Promise<T> {
if (!response.ok) {
throw new ProfileApiError(
(await response.text()) || response.statusText,
response.status,
);
}
return parser(await response.json() as unknown);
}
export async function fetchWorkspaceMetadata(
workspaceId: string, workspaceId: string,
): Promise<WorkspaceMetadataSettingsResponse> { ): Promise<WorkspaceMetadataSettingsResponse> {
return await requestJson<WorkspaceMetadataSettingsResponse>( return await parseResponse(
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`, await fetch(`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`),
parseWorkspaceMetadataSettingsResponse,
); );
} }
export async function updateWorkspaceMetadataSettings( export async function updateWorkspaceMetadata(
workspaceId: string, workspaceId: string,
request: { display_name: string; revision: string }, request: UpdateWorkspaceMetadataRequest,
): Promise<WorkspaceMetadataMutationResponse> { ): Promise<WorkspaceMetadataMutationResponse> {
return await requestJson<WorkspaceMetadataMutationResponse>( return await parseResponse(
`/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`, await fetch(
{ `/api/w/${encodeURIComponent(workspaceId)}/settings/workspace`,
method: "PUT", {
headers: { "content-type": "application/json" }, method: "PUT",
body: JSON.stringify(request), headers: { "content-type": "application/json" },
}, body: JSON.stringify(request),
},
),
parseWorkspaceMetadataMutationResponse,
); );
} }
export async function fetchProfileSettings( export async function fetchProfileSettings(
workspaceId: string, workspaceId: string,
): Promise<ProfileSettingsResponse> { ): Promise<ProfileSettingsResponse> {
return await requestJson<ProfileSettingsResponse>( return await parseResponse(
`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`, await fetch(`/api/w/${encodeURIComponent(workspaceId)}/settings/profiles`),
parseProfileSettingsResponse,
); );
} }
export function createWorkspaceProfileApi(): WorkspaceProfileApi {
return {
getMetadata: fetchWorkspaceMetadataSettings,
async updateMetadata(workspaceId, displayName, expectedRevision) {
return await updateWorkspaceMetadataSettings(workspaceId, {
display_name: displayName,
revision: expectedRevision,
});
},
getProfiles: fetchProfileSettings,
};
}
@@ -1,52 +0,0 @@
import type { Diagnostic } from "./model";
export type WorkspaceMetadataSettingsResponse = {
workspace_id: string;
display_name: string;
created_at: string;
revision: string;
source: string;
diagnostics: Diagnostic[];
};
export type WorkspaceMetadataMutationResponse = {
workspace: WorkspaceMetadataSettingsResponse;
diagnostics: Diagnostic[];
};
export type WorkspaceProfileSummary = {
profile_id: string;
selector: string;
label: string;
source_kind: "builtin" | "project" | string;
profile_source_id?: string | null;
description?: string | null;
editable: boolean;
is_default: boolean;
diagnostics: Diagnostic[];
};
export type WorkspaceProfileSourceSummary = {
profile_source_id: string;
display_path: string;
kind: "virtual_config" | string;
content_type: string;
content_digest: string;
provenance: "project_profile_source_tree" | string;
editable: boolean;
revision: string;
size_bytes: number;
diagnostics: Diagnostic[];
};
export type ProfileSettingsResponse = {
workspace_id: string;
registry_revision: string;
config_revision?: number | null;
tree_digest?: string | null;
projection_digest?: string | null;
default_profile?: string | null;
profiles: WorkspaceProfileSummary[];
sources: WorkspaceProfileSourceSummary[];
diagnostics: Diagnostic[];
};
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import DiagnosticsList from "$lib/workspace/settings/DiagnosticsList.svelte"; import DiagnosticsList from "$lib/workspace/settings/DiagnosticsList.svelte";
import { settingsSectionHref } from "$lib/workspace/settings/model"; import { settingsSectionHref } from "$lib/workspace/settings/model";
import type { ProfileSettingsResponse } from "$lib/generated/workspace-api";
import { fetchProfileSettings } from "$lib/workspace/settings/profile-api"; import { fetchProfileSettings } from "$lib/workspace/settings/profile-api";
import type { ProfileSettingsResponse } from "$lib/workspace/settings/profile-types";
import type { PageProps } from "./$types"; import type { PageProps } from "./$types";
let { data }: PageProps = $props(); let { data }: PageProps = $props();
@@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import type {
Diagnostic,
WorkspaceMetadataSettingsResponse,
} from '$lib/generated/workspace-api';
import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte'; import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import { import {
fetchWorkspaceMetadataSettings, fetchWorkspaceMetadata,
updateWorkspaceMetadataSettings updateWorkspaceMetadata,
} from '$lib/workspace/settings/profile-api'; } from '$lib/workspace/settings/profile-api';
import type { Diagnostic } from '$lib/workspace/settings/model';
import type { WorkspaceMetadataSettingsResponse } from '$lib/workspace/settings/profile-types';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
@@ -28,7 +30,7 @@
loading = true; loading = true;
message = null; message = null;
try { try {
const response = await fetchWorkspaceMetadataSettings(workspaceId); const response = await fetchWorkspaceMetadata(workspaceId);
if (!cancelled) { if (!cancelled) {
workspaceMetadata = response; workspaceMetadata = response;
displayNameDraft = response.display_name; displayNameDraft = response.display_name;
@@ -53,7 +55,7 @@
submitting = true; submitting = true;
message = null; message = null;
try { try {
const response = await updateWorkspaceMetadataSettings(workspaceId, { const response = await updateWorkspaceMetadata(workspaceId, {
display_name: displayNameDraft, display_name: displayNameDraft,
revision: workspaceMetadata.revision revision: workspaceMetadata.revision
}); });
+181 -32
View File
@@ -2,45 +2,194 @@ declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void; test(name: string, fn: () => void | Promise<void>): void;
}; };
import { createWorkspaceProfileApi } from "../src/lib/workspace/settings/profile-api.ts"; function assertEquals(actual: unknown, expected: unknown): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
);
}
}
Deno.test("workspace profile API delegates metadata calls to current route contract", async () => { function assertThrows<T extends Error>(
fn: () => unknown,
errorType: abstract new (...args: never[]) => T,
): void {
try {
fn();
} catch (error) {
if (error instanceof errorType) return;
throw error;
}
throw new Error(`expected ${errorType.name} to be thrown`);
}
import {
fetchProfileSettings,
fetchWorkspaceMetadata,
parseProfileSettingsResponse,
parseWorkspaceMetadataSettingsResponse,
ProfileApiError,
updateWorkspaceMetadata,
} from "../src/lib/workspace/settings/profile-api.ts";
const diagnostic = {
code: "ok",
severity: "info",
message: "ready",
};
function profileSettingsFixture(): Record<string, unknown> {
return {
workspace_id: "workspace 1",
registry_revision: "config-source:7:tree:projection",
config_revision: 7,
tree_digest: "tree",
projection_digest: "projection",
default_profile: "workspace:coder",
profiles: [{
profile_id: "workspace:coder",
selector: "workspace:coder",
label: "Coder",
source_kind: "project",
profile_source_id: "profile-source-1",
description: null,
editable: true,
is_default: true,
diagnostics: [],
}],
sources: [{
profile_source_id: "profile-source-1",
display_path: "profiles/coder.dcdl",
kind: "profile",
content_type: "text/x-decodal",
content_digest: "sha256:source",
provenance: "project_profile_source_tree",
editable: false,
revision: "config-source:7",
size_bytes: 128,
diagnostics: [],
}],
diagnostics: [diagnostic],
};
}
Deno.test("profile settings requests use scoped API and strictly validate responses", async () => {
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
const requests: Array<{ input: string; init?: RequestInit }> = []; const requests: Array<{ url: string; init?: RequestInit }> = [];
globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { globalThis.fetch = (input: string | URL | Request, init?: RequestInit) => {
requests.push({ input: String(input), init }); requests.push({ url: String(input), init });
return Promise.resolve(Response.json({ return Promise.resolve(Response.json(profileSettingsFixture()));
workspace_id: "workspace-a", };
display_name: "Alpha",
revision: "revision-2",
}));
}) as typeof fetch;
try { try {
const api = createWorkspaceProfileApi(); const response = await fetchProfileSettings("workspace 1");
await api.getMetadata("workspace-a"); assertEquals(response.config_revision, 7);
await api.updateMetadata("workspace-a", "Alpha updated", "revision-1"); assertEquals(response.sources[0].provenance, "project_profile_source_tree");
assertEquals(requests.length, 1);
assertEquals(requests[0].url, "/api/w/workspace%201/settings/profiles");
assertEquals(requests[0].init, undefined);
} finally { } finally {
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
} }
});
if (requests.length !== 2) throw new Error("expected two metadata requests"); Deno.test("workspace metadata requests use generated DTO shapes", async () => {
if ( const originalFetch = globalThis.fetch;
requests.some((request) => request.input.includes("/settings/metadata")) const requests: Array<{ url: string; init?: RequestInit }> = [];
) { const workspace = {
throw new Error("obsolete metadata endpoint was used"); workspace_id: "workspace 1",
} display_name: "Workspace",
if ( created_at: "2026-01-01T00:00:00Z",
requests.some((request) => !request.input.endsWith("/settings/workspace")) revision: "sha256:metadata",
) { source: "workspace-config",
throw new Error("current Workspace settings endpoint was not used"); diagnostics: [diagnostic],
} };
const updateBody = JSON.parse(String(requests[1].init?.body)); globalThis.fetch = (input: string | URL | Request, init?: RequestInit) => {
if ( requests.push({ url: String(input), init });
updateBody.display_name !== "Alpha updated" || return Promise.resolve(
updateBody.revision !== "revision-1" || Response.json(
"expected_revision" in updateBody init?.method === "PUT" ? { workspace, diagnostics: [] } : workspace,
) { ),
throw new Error(`unexpected update payload: ${JSON.stringify(updateBody)}`); );
};
try {
assertEquals(
(await fetchWorkspaceMetadata("workspace 1")).revision,
"sha256:metadata",
);
assertEquals(
(await updateWorkspaceMetadata("workspace 1", {
display_name: "Renamed",
revision: "sha256:metadata",
})).workspace.workspace_id,
"workspace 1",
);
assertEquals(requests.map((request) => request.url), [
"/api/w/workspace%201/settings/workspace",
"/api/w/workspace%201/settings/workspace",
]);
assertEquals(requests[1].init?.method, "PUT");
assertEquals(
requests[1].init?.body,
JSON.stringify({ display_name: "Renamed", revision: "sha256:metadata" }),
);
} finally {
globalThis.fetch = originalFetch;
} }
}); });
Deno.test("profile settings parser rejects missing, mistyped, stale, and invalid provenance fields", () => {
const missing = profileSettingsFixture();
delete missing.profiles;
assertThrows(
() => parseProfileSettingsResponse(missing),
ProfileApiError,
);
const mistyped = profileSettingsFixture();
mistyped.config_revision = "7";
assertThrows(
() => parseProfileSettingsResponse(mistyped),
ProfileApiError,
);
const stale = profileSettingsFixture();
stale.legacy_profile_directory = ".yoi/profiles";
assertThrows(
() => parseProfileSettingsResponse(stale),
ProfileApiError,
);
const invalidProvenance = profileSettingsFixture();
const sources = invalidProvenance.sources as Array<Record<string, unknown>>;
sources[0].provenance = "filesystem";
assertThrows(
() => parseProfileSettingsResponse(invalidProvenance),
ProfileApiError,
);
});
Deno.test("workspace metadata parser rejects incomplete or stale response fields", () => {
assertThrows(
() =>
parseWorkspaceMetadataSettingsResponse({
workspace_id: "workspace-test",
display_name: "Workspace",
}),
ProfileApiError,
);
assertThrows(
() =>
parseWorkspaceMetadataSettingsResponse({
workspace_id: "workspace-test",
display_name: "Workspace",
created_at: "2026-01-01T00:00:00Z",
revision: "sha256:metadata",
source: "workspace-config",
diagnostics: [],
workspace_path: "/legacy/path",
}),
ProfileApiError,
);
});