feat(workspace): allow creation without an initial repository
Keep bootstrap, signing identity, config and resource-key allocation atomic with optional Repository intent. Preserve repository-present fingerprints for saved receipts; replay reads current authority at the initial key and never recreates missing assets. Provide explicit repository-free CLI creation, optional TUI/Web creation inputs, nullable generated contracts, and valid empty catalog/Worker paths. Retain existing authenticated non-owner viewing policy pending the T-717 authorization decision. Validation: root cargo check; server tests 639 lib + 11 bin; server-api 70 default / 77 typescript; client 52 lib + 2 integration; tui 337; yoi 11 lib + 120 bin. OpenAPI/TS freshness, cargo fmt and diff checks pass. Web check/build, 18 model tests, 268 component tests and Chromium 8 width/theme combinations pass; before/after visual evidence is documented in web/workspace/test/visual-review. Full Web suite has one independently reproduced baseline Ticket source-policy failure.
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void | Promise<void>): void;
|
||||
readTextFile(path: URL): Promise<string>;
|
||||
};
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(
|
||||
@@ -35,6 +30,26 @@ import {
|
||||
loadWorkspaceCatalog,
|
||||
WorkspaceCatalogError,
|
||||
} from "../src/lib/workspace/api/workspace-catalog.ts";
|
||||
import { creationResponse } from "../src/lib/workspace/api/workspace-catalog.test-fixtures.ts";
|
||||
|
||||
Deno.test("workspace creation accepts omitted and null repository requests without fabricating a repository", async () => {
|
||||
for (const repository of [undefined, null]) {
|
||||
const request = {
|
||||
operation_key: "empty-create",
|
||||
display_name: "New Workspace",
|
||||
...(repository === null ? { repository } : {}),
|
||||
};
|
||||
const fetcher = async (url: string | URL | Request, init?: RequestInit) => {
|
||||
assertEquals(url, "/api/workspaces");
|
||||
assertEquals(init?.method, "POST");
|
||||
assertEquals(JSON.parse(String(init?.body)), request);
|
||||
return Response.json(creationResponse());
|
||||
};
|
||||
const response = await createWorkspace(fetcher as typeof fetch, request);
|
||||
assertEquals(response.repository, null);
|
||||
assertEquals(response.workspace.workspace_id, "home-empty");
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("workspace catalog enriches each visible workspace without dropping siblings", async () => {
|
||||
const fetcher = (input: string | URL | Request) => {
|
||||
@@ -118,27 +133,3 @@ Deno.test("workspace creation preserves caller-owned operation key across retry"
|
||||
);
|
||||
assertEquals(bodies, [request, request]);
|
||||
});
|
||||
|
||||
Deno.test("workspace creation form starts with an explicit required Repository key", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL("../src/routes/+page.svelte", import.meta.url),
|
||||
);
|
||||
|
||||
assert(
|
||||
source.includes('let repositoryKey = $state("");'),
|
||||
"the form must start without an implicit Repository key",
|
||||
);
|
||||
assert(
|
||||
source.includes("<input bind:value={repositoryKey} required"),
|
||||
"the Browser must require creator input for the Repository key",
|
||||
);
|
||||
assert(
|
||||
source.includes("repository_key: normalized.repositoryKey"),
|
||||
"the Browser must submit the creator-provided Repository key",
|
||||
);
|
||||
assert(
|
||||
!source.includes('repositoryKey || "main"') &&
|
||||
!source.includes("repositoryKey || 'main'"),
|
||||
"the Browser must not fall back to main",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,12 +8,57 @@ import {
|
||||
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_revision: -1,
|
||||
}]
|
||||
) {
|
||||
assertThrows(
|
||||
() =>
|
||||
parseWorkspaceCreateResponse({
|
||||
...creationResponse(),
|
||||
repository: value,
|
||||
}),
|
||||
"workspace create response.repository",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function assertThrows(operation: () => unknown, expected: string): void {
|
||||
try {
|
||||
operation();
|
||||
|
||||
Reference in New Issue
Block a user