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:
2026-10-08 18:34:44 +09:00
parent e939cd4e4b
commit 7919616db5
37 changed files with 2335 additions and 354 deletions
+20 -29
View File
@@ -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();