feat: validate shared workdir REST contracts in web
This commit is contained in:
@@ -113,6 +113,7 @@ export async function loadJson<T>(
|
|||||||
fetchFn: typeof fetch,
|
fetchFn: typeof fetch,
|
||||||
path: string,
|
path: string,
|
||||||
init?: RequestInit,
|
init?: RequestInit,
|
||||||
|
parse: (value: unknown) => T = (value) => value as T,
|
||||||
): Promise<ApiResult<T>> {
|
): Promise<ApiResult<T>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetchFn(path, init);
|
const response = await fetchFn(path, init);
|
||||||
@@ -123,7 +124,8 @@ export async function loadJson<T>(
|
|||||||
error: text || `${path} request failed (${response.status})`,
|
error: text || `${path} request failed (${response.status})`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { data: (await response.json()) as T, error: null };
|
const payload: unknown = await response.json();
|
||||||
|
return { data: parse(payload), error: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import type {
|
||||||
|
Diagnostic,
|
||||||
|
WorkingDirectoryCleanupTarget,
|
||||||
|
WorkingDirectoryCreateRequest,
|
||||||
|
WorkingDirectoryCreateResponse,
|
||||||
|
WorkingDirectoryDetailResponse,
|
||||||
|
WorkingDirectoryListResponse,
|
||||||
|
WorkingDirectoryOccupancy,
|
||||||
|
WorkingDirectorySummary,
|
||||||
|
} from "../../generated/workdir-api";
|
||||||
|
|
||||||
|
const SUMMARY_KEYS = new Set([
|
||||||
|
"working_directory_id",
|
||||||
|
"repository_id",
|
||||||
|
"creation_selector",
|
||||||
|
"creation_ref",
|
||||||
|
"creation_tree",
|
||||||
|
"current_selector",
|
||||||
|
"current_ref",
|
||||||
|
"current_tree",
|
||||||
|
"observed_at_epoch_seconds",
|
||||||
|
"materializer_kind",
|
||||||
|
"cleanup_target",
|
||||||
|
"status",
|
||||||
|
"cleanliness",
|
||||||
|
"primary_worker_id",
|
||||||
|
"occupied_by",
|
||||||
|
]);
|
||||||
|
const CREATE_REQUEST_KEYS = new Set([
|
||||||
|
"runtime_id",
|
||||||
|
"repository_id",
|
||||||
|
"selector",
|
||||||
|
"operation_id",
|
||||||
|
]);
|
||||||
|
const DIAGNOSTIC_KEYS = new Set(["code", "severity", "message"]);
|
||||||
|
const CLEANUP_TARGET_KEYS = new Set([
|
||||||
|
"kind",
|
||||||
|
"working_directory_id",
|
||||||
|
"repository_id",
|
||||||
|
]);
|
||||||
|
const OCCUPANCY_KEYS = new Set([
|
||||||
|
"runtime_id",
|
||||||
|
"worker_id",
|
||||||
|
"display_name",
|
||||||
|
"linked_at",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function parseWorkingDirectoryListResponse(
|
||||||
|
value: unknown,
|
||||||
|
): WorkingDirectoryListResponse {
|
||||||
|
const record = exactRecord(
|
||||||
|
value,
|
||||||
|
new Set(["workspace_id", "items", "diagnostics"]),
|
||||||
|
"Workdir list response",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: stringField(record, "workspace_id"),
|
||||||
|
items: arrayField(record, "items").map(parseSummary),
|
||||||
|
diagnostics: arrayField(record, "diagnostics").map(parseDiagnostic),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWorkingDirectoryDetailResponse(
|
||||||
|
value: unknown,
|
||||||
|
): WorkingDirectoryDetailResponse {
|
||||||
|
return parseDetailLike(value, "Workdir detail response");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWorkingDirectoryCreateResponse(
|
||||||
|
value: unknown,
|
||||||
|
): WorkingDirectoryCreateResponse {
|
||||||
|
return parseDetailLike(value, "Workdir create response");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateWorkingDirectoryCreateRequest(
|
||||||
|
value: unknown,
|
||||||
|
): WorkingDirectoryCreateRequest {
|
||||||
|
const record = exactRecord(
|
||||||
|
value,
|
||||||
|
CREATE_REQUEST_KEYS,
|
||||||
|
"Workdir create request",
|
||||||
|
);
|
||||||
|
const request: WorkingDirectoryCreateRequest = {
|
||||||
|
repository_id: stringField(record, "repository_id"),
|
||||||
|
};
|
||||||
|
assignOptionalString(request, record, "runtime_id");
|
||||||
|
assignOptionalString(request, record, "selector");
|
||||||
|
assignOptionalString(request, record, "operation_id");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDetailLike(
|
||||||
|
value: unknown,
|
||||||
|
label: string,
|
||||||
|
): WorkingDirectoryDetailResponse {
|
||||||
|
const record = exactRecord(
|
||||||
|
value,
|
||||||
|
new Set(["workspace_id", "runtime_id", "item", "diagnostics"]),
|
||||||
|
label,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: stringField(record, "workspace_id"),
|
||||||
|
runtime_id: stringField(record, "runtime_id"),
|
||||||
|
item: parseSummary(record.item),
|
||||||
|
diagnostics: arrayField(record, "diagnostics").map(parseDiagnostic),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSummary(value: unknown): WorkingDirectorySummary {
|
||||||
|
const record = exactRecord(value, SUMMARY_KEYS, "Workdir summary");
|
||||||
|
const summary: WorkingDirectorySummary = {
|
||||||
|
working_directory_id: stringField(record, "working_directory_id"),
|
||||||
|
repository_id: stringField(record, "repository_id"),
|
||||||
|
materializer_kind: enumField(record, "materializer_kind", [
|
||||||
|
"runtime_git_cache",
|
||||||
|
"local_git_worktree",
|
||||||
|
]),
|
||||||
|
status: enumField(record, "status", [
|
||||||
|
"active",
|
||||||
|
"cleanup_pending",
|
||||||
|
"corrupted",
|
||||||
|
"not_found",
|
||||||
|
"unknown",
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
assignOptionalString(summary, record, "creation_selector");
|
||||||
|
assignOptionalString(summary, record, "creation_ref");
|
||||||
|
assignOptionalString(summary, record, "creation_tree");
|
||||||
|
assignOptionalString(summary, record, "current_selector");
|
||||||
|
assignOptionalString(summary, record, "current_ref");
|
||||||
|
assignOptionalString(summary, record, "current_tree");
|
||||||
|
assignOptionalString(summary, record, "cleanliness");
|
||||||
|
assignOptionalString(summary, record, "primary_worker_id");
|
||||||
|
if (record.observed_at_epoch_seconds !== undefined) {
|
||||||
|
const observedAt = record.observed_at_epoch_seconds;
|
||||||
|
if (observedAt === null) {
|
||||||
|
summary.observed_at_epoch_seconds = null;
|
||||||
|
} else {
|
||||||
|
if (!Number.isSafeInteger(observedAt) || Number(observedAt) < 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Workdir summary.observed_at_epoch_seconds must be a non-negative safe integer or null",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
summary.observed_at_epoch_seconds = Number(observedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (record.cleanup_target !== undefined) {
|
||||||
|
summary.cleanup_target = record.cleanup_target === null
|
||||||
|
? null
|
||||||
|
: parseCleanupTarget(record.cleanup_target);
|
||||||
|
}
|
||||||
|
if (record.occupied_by !== undefined) {
|
||||||
|
summary.occupied_by = record.occupied_by === null
|
||||||
|
? null
|
||||||
|
: parseOccupancy(record.occupied_by);
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCleanupTarget(value: unknown): WorkingDirectoryCleanupTarget {
|
||||||
|
const record = exactRecord(
|
||||||
|
value,
|
||||||
|
CLEANUP_TARGET_KEYS,
|
||||||
|
"Workdir cleanup target",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
kind: stringField(record, "kind"),
|
||||||
|
working_directory_id: stringField(record, "working_directory_id"),
|
||||||
|
repository_id: stringField(record, "repository_id"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOccupancy(value: unknown): WorkingDirectoryOccupancy {
|
||||||
|
const record = exactRecord(value, OCCUPANCY_KEYS, "Workdir occupancy");
|
||||||
|
return {
|
||||||
|
runtime_id: stringField(record, "runtime_id"),
|
||||||
|
worker_id: stringField(record, "worker_id"),
|
||||||
|
display_name: stringField(record, "display_name"),
|
||||||
|
linked_at: stringField(record, "linked_at"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDiagnostic(value: unknown): Diagnostic {
|
||||||
|
const record = exactRecord(value, DIAGNOSTIC_KEYS, "Workdir diagnostic");
|
||||||
|
return {
|
||||||
|
code: stringField(record, "code"),
|
||||||
|
severity: enumField(record, "severity", ["info", "warning", "error"]),
|
||||||
|
message: stringField(record, "message"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function exactRecord(
|
||||||
|
value: unknown,
|
||||||
|
keys: ReadonlySet<string>,
|
||||||
|
label: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
throw new Error(`${label} must be an object`);
|
||||||
|
}
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
for (const key of Object.keys(record)) {
|
||||||
|
if (!keys.has(key)) {
|
||||||
|
throw new Error(`${label} contains unknown field ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringField(record: Record<string, unknown>, key: string): string {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value !== "string" || value.length === 0) {
|
||||||
|
throw new Error(`${key} must be a non-empty string`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayField(record: Record<string, unknown>, key: string): unknown[] {
|
||||||
|
const value = record[key];
|
||||||
|
if (!Array.isArray(value)) throw new Error(`${key} must be an array`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumField<T extends string>(
|
||||||
|
record: Record<string, unknown>,
|
||||||
|
key: string,
|
||||||
|
values: readonly T[],
|
||||||
|
): T {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value !== "string" || !values.includes(value as T)) {
|
||||||
|
throw new Error(`${key} has an unsupported value`);
|
||||||
|
}
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignOptionalString<T extends object>(
|
||||||
|
target: T,
|
||||||
|
source: Record<string, unknown>,
|
||||||
|
key: string,
|
||||||
|
): void {
|
||||||
|
const value = source[key];
|
||||||
|
if (value === undefined) return;
|
||||||
|
if (value === null) {
|
||||||
|
(target as Record<string, unknown>)[key] = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof value !== "string" || value.length === 0) {
|
||||||
|
throw new Error(`${key} must be a non-empty string or null`);
|
||||||
|
}
|
||||||
|
(target as Record<string, unknown>)[key] = value;
|
||||||
|
}
|
||||||
@@ -1,10 +1,28 @@
|
|||||||
|
import type {
|
||||||
|
WorkingDirectoryCreateRequest,
|
||||||
|
WorkingDirectoryCreateResponse,
|
||||||
|
WorkingDirectoryDetailResponse,
|
||||||
|
WorkingDirectoryListResponse,
|
||||||
|
WorkingDirectoryOccupancy,
|
||||||
|
WorkingDirectorySummary,
|
||||||
|
} from "$lib/generated/workdir-api";
|
||||||
import type {
|
import type {
|
||||||
Event as PodProtocolEvent,
|
Event as PodProtocolEvent,
|
||||||
Method as PodProtocolMethod,
|
Method as PodProtocolMethod,
|
||||||
Segment as PodProtocolSegment,
|
Segment as PodProtocolSegment,
|
||||||
} from "$lib/generated/protocol";
|
} from "$lib/generated/protocol";
|
||||||
|
|
||||||
export type { PodProtocolEvent, PodProtocolMethod, PodProtocolSegment };
|
export type {
|
||||||
|
PodProtocolEvent,
|
||||||
|
PodProtocolMethod,
|
||||||
|
PodProtocolSegment,
|
||||||
|
WorkingDirectoryCreateRequest,
|
||||||
|
WorkingDirectoryCreateResponse,
|
||||||
|
WorkingDirectoryDetailResponse,
|
||||||
|
WorkingDirectoryListResponse,
|
||||||
|
WorkingDirectoryOccupancy,
|
||||||
|
WorkingDirectorySummary,
|
||||||
|
};
|
||||||
|
|
||||||
export type ExtensionPoint = {
|
export type ExtensionPoint = {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -111,44 +129,6 @@ export type WorkingDirectoryRepositoryOption = {
|
|||||||
default_selector?: string | null;
|
default_selector?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkingDirectoryOccupancy = {
|
|
||||||
runtime_id: string;
|
|
||||||
worker_id: string;
|
|
||||||
display_name: string;
|
|
||||||
linked_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkingDirectorySummary = {
|
|
||||||
working_directory_id: string;
|
|
||||||
repository_id: string;
|
|
||||||
creation_selector?: string | null;
|
|
||||||
creation_ref?: string | null;
|
|
||||||
current_selector?: string | null;
|
|
||||||
current_ref?: string | null;
|
|
||||||
materializer_kind: string;
|
|
||||||
status: string;
|
|
||||||
cleanliness?: string | null;
|
|
||||||
primary_worker_id?: string | null;
|
|
||||||
occupied_by?: WorkingDirectoryOccupancy | null;
|
|
||||||
cleanup_target: {
|
|
||||||
kind: string;
|
|
||||||
working_directory_id: string;
|
|
||||||
repository_id: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BrowserWorkingDirectoryCreateResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
item: WorkingDirectorySummary;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BrowserWorkingDirectoryListResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
items: WorkingDirectorySummary[];
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CleanupTargetKind =
|
export type CleanupTargetKind =
|
||||||
| "worker_delete"
|
| "worker_delete"
|
||||||
| "workdir_clean_cleanup"
|
| "workdir_clean_cleanup"
|
||||||
@@ -217,12 +197,6 @@ export type BrowserWorkerWorkingDirectorySelection = {
|
|||||||
relative_cwd?: string | null;
|
relative_cwd?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BrowserWorkingDirectoryCreateRequest = {
|
|
||||||
runtime_id: string;
|
|
||||||
repository_id: string;
|
|
||||||
selector?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkerLaunchOptionsResponse = {
|
export type WorkerLaunchOptionsResponse = {
|
||||||
workspace_id: string;
|
workspace_id: string;
|
||||||
runtimes: WorkerLaunchRuntimeOption[];
|
runtimes: WorkerLaunchRuntimeOption[];
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
|
import { parseWorkingDirectoryListResponse } from "$lib/workspace/api/workdirs";
|
||||||
import type {
|
import type {
|
||||||
BrowserWorkingDirectoryListResponse,
|
|
||||||
ListResponse,
|
ListResponse,
|
||||||
Runtime,
|
Runtime,
|
||||||
RuntimeCleanupPlanResponse,
|
RuntimeCleanupPlanResponse,
|
||||||
@@ -14,12 +14,14 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
|||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(params.workspaceId, "/runtimes"),
|
workspaceApiPath(params.workspaceId, "/runtimes"),
|
||||||
),
|
),
|
||||||
loadJson<BrowserWorkingDirectoryListResponse>(
|
loadJson(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(
|
workspaceApiPath(
|
||||||
params.workspaceId,
|
params.workspaceId,
|
||||||
`/runtimes/${encodeURIComponent(runtimeId)}/working-directories`,
|
`/runtimes/${encodeURIComponent(runtimeId)}/working-directories`,
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
|
parseWorkingDirectoryListResponse,
|
||||||
),
|
),
|
||||||
loadJson<RuntimeCleanupPlanResponse>(
|
loadJson<RuntimeCleanupPlanResponse>(
|
||||||
fetch,
|
fetch,
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||||
|
import {
|
||||||
|
parseWorkingDirectoryCreateResponse,
|
||||||
|
validateWorkingDirectoryCreateRequest,
|
||||||
|
} from '$lib/workspace/api/workdirs';
|
||||||
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||||
import { buildCreateWorkspaceWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace/sidebar/worker-launch';
|
import { buildCreateWorkspaceWorkerRequest, defaultWorkerLaunchForm } from '$lib/workspace/sidebar/worker-launch';
|
||||||
import type {
|
import type {
|
||||||
BrowserCreateWorkerResponse,
|
BrowserCreateWorkerResponse,
|
||||||
BrowserWorkingDirectoryCreateResponse,
|
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
WorkerLaunchOptionsResponse,
|
WorkerLaunchOptionsResponse,
|
||||||
WorkingDirectorySummary,
|
WorkingDirectorySummary,
|
||||||
@@ -160,22 +163,23 @@
|
|||||||
creatingWorkingDirectory = true;
|
creatingWorkingDirectory = true;
|
||||||
submitError = null;
|
submitError = null;
|
||||||
try {
|
try {
|
||||||
|
const request = validateWorkingDirectoryCreateRequest({
|
||||||
|
runtime_id: runtimeId,
|
||||||
|
repository_id: workingDirectoryRepositoryId,
|
||||||
|
...(workingDirectorySelector ? { selector: workingDirectorySelector } : {}),
|
||||||
|
});
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
workerApiPath(`/runtimes/${encodeURIComponent(runtimeId)}/working-directories`), {
|
workerApiPath(`/runtimes/${encodeURIComponent(runtimeId)}/working-directories`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(request),
|
||||||
runtime_id: runtimeId,
|
|
||||||
repository_id: workingDirectoryRepositoryId,
|
|
||||||
selector: workingDirectorySelector || null,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
submitError = await responseDisplayError(response, 'workdir create failed');
|
submitError = await responseDisplayError(response, 'workdir create failed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = (await response.json()) as BrowserWorkingDirectoryCreateResponse;
|
const payload = parseWorkingDirectoryCreateResponse(await response.json());
|
||||||
const items = options?.working_directories ?? [];
|
const items = options?.working_directories ?? [];
|
||||||
options = options
|
options = options
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
declare const Deno: {
|
||||||
|
test(name: string, fn: () => void | Promise<void>): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseWorkingDirectoryCreateResponse,
|
||||||
|
parseWorkingDirectoryListResponse,
|
||||||
|
validateWorkingDirectoryCreateRequest,
|
||||||
|
} from "../src/lib/workspace/api/workdirs.ts";
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
working_directory_id: "workdir-1",
|
||||||
|
repository_id: "main",
|
||||||
|
materializer_kind: "runtime_git_cache",
|
||||||
|
status: "active",
|
||||||
|
occupied_by: {
|
||||||
|
runtime_id: "arcadia",
|
||||||
|
worker_id: "worker-1",
|
||||||
|
display_name: "Coder",
|
||||||
|
linked_at: "2026-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Deno.test("Workdir REST validation accepts the generated list and create contracts", () => {
|
||||||
|
const list = parseWorkingDirectoryListResponse({
|
||||||
|
workspace_id: "workspace-a",
|
||||||
|
items: [summary],
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
if (list.items[0]?.occupied_by?.runtime_id !== "arcadia") {
|
||||||
|
throw new Error("occupancy subject was not preserved");
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = parseWorkingDirectoryCreateResponse({
|
||||||
|
workspace_id: "workspace-a",
|
||||||
|
runtime_id: "arcadia",
|
||||||
|
item: summary,
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
if (created.runtime_id !== "arcadia") {
|
||||||
|
throw new Error("create Runtime was not preserved");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Workdir REST validation rejects stale response JSON", () => {
|
||||||
|
let rejected = false;
|
||||||
|
try {
|
||||||
|
parseWorkingDirectoryListResponse({
|
||||||
|
workspace_id: "workspace-a",
|
||||||
|
items: [summary],
|
||||||
|
diagnostics: [],
|
||||||
|
source: "legacy-runtime",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
rejected = true;
|
||||||
|
}
|
||||||
|
if (!rejected) throw new Error("stale response field was accepted");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Workdir REST validation enforces create operation fields", () => {
|
||||||
|
const request = validateWorkingDirectoryCreateRequest({
|
||||||
|
runtime_id: "arcadia",
|
||||||
|
repository_id: "main",
|
||||||
|
selector: "develop",
|
||||||
|
operation_id: "operation-1",
|
||||||
|
});
|
||||||
|
if (request.operation_id !== "operation-1") {
|
||||||
|
throw new Error("operation id was not preserved");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const invalid of [
|
||||||
|
{ runtime_id: "arcadia", operation_id: "operation-1" },
|
||||||
|
{ repository_id: "main", operation_key: "operation-1" },
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
let rejected = false;
|
||||||
|
try {
|
||||||
|
validateWorkingDirectoryCreateRequest(invalid);
|
||||||
|
} catch {
|
||||||
|
rejected = true;
|
||||||
|
}
|
||||||
|
if (!rejected) {
|
||||||
|
throw new Error(
|
||||||
|
`invalid request was accepted: ${JSON.stringify(invalid)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user