feat: replace revision counters with domain state contracts
Use content digests, key fingerprints, binding and trust identities, immutable mutation receipts, and observation tokens instead of state-update ordinals across API, storage, Runtime, and Web contracts. Preserve historical replay and secret material through frozen migrations; preflight uncertain removals before cutover, resume provisioning receipts, validate encrypted migration candidates, and align fresh/upgraded schemas. Include reviewed regression fixes, generated artifacts, and validation evidence.
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
commitConfigTree,
|
||||
ConfigSourceApiError,
|
||||
fetchConfigEntry,
|
||||
fetchConfigRevision,
|
||||
fetchConfigHistory,
|
||||
fetchConfigTree,
|
||||
parseWorkspaceConfigTreeResponse,
|
||||
} from "../../src/lib/workspace/config-source/api.ts";
|
||||
@@ -29,7 +29,6 @@ const entry = {
|
||||
content_digest: "sha256:entry",
|
||||
};
|
||||
const snapshot = {
|
||||
revision: 7,
|
||||
digest: "sha256:tree",
|
||||
entries: { "profiles/main.dcdl": entry },
|
||||
};
|
||||
@@ -57,15 +56,14 @@ Deno.test("config source API commits directly through the workspace scope", asyn
|
||||
const url = String(input);
|
||||
calls.push({ url, init });
|
||||
if (url.includes("/entries/")) return Promise.resolve(response(entry));
|
||||
if (url.includes("/revisions/")) return Promise.resolve(response(snapshot));
|
||||
if (url.includes("/history/")) return Promise.resolve(response(snapshot));
|
||||
return Promise.resolve(response(tree));
|
||||
}) as typeof fetch;
|
||||
|
||||
await fetchConfigTree("w/one", fetcher);
|
||||
await fetchConfigRevision("w/one", 7, fetcher);
|
||||
await fetchConfigHistory("w/one", "sha256:tree", fetcher);
|
||||
await fetchConfigEntry("w/one", "profiles/main.dcdl", fetcher);
|
||||
await commitConfigTree("w/one", {
|
||||
base_revision: 4,
|
||||
base_digest: "sha256:base",
|
||||
changes: [],
|
||||
entrypoints: [],
|
||||
@@ -73,7 +71,7 @@ Deno.test("config source API commits directly through the workspace scope", asyn
|
||||
|
||||
assertEquals<unknown>(calls.map((call) => call.url), [
|
||||
"/api/w/w%2Fone/config/source-tree",
|
||||
"/api/w/w%2Fone/config/source-tree/revisions/7",
|
||||
"/api/w/w%2Fone/config/source-tree/history/sha256%3Atree",
|
||||
"/api/w/w%2Fone/config/source-tree/entries/profiles%2Fmain.dcdl",
|
||||
"/api/w/w%2Fone/config/source-tree/commit",
|
||||
]);
|
||||
@@ -81,6 +79,9 @@ Deno.test("config source API commits directly through the workspace scope", asyn
|
||||
assert(
|
||||
String(calls[3].init?.body).includes('"base_digest":"sha256:base"'),
|
||||
);
|
||||
assertEquals(JSON.parse(String(calls[3].init?.body)), {
|
||||
base_digest: "sha256:base", changes: [], entrypoints: [],
|
||||
});
|
||||
});
|
||||
|
||||
function withSchema(source: unknown, contributionSource: unknown = "builtin") {
|
||||
@@ -113,7 +114,6 @@ for (const field of ["bundle", "contribution"] as const) {
|
||||
assertEquals<unknown>(await fetchConfigTree("w", fetcher), body);
|
||||
assertEquals<unknown>(
|
||||
await commitConfigTree("w", {
|
||||
base_revision: 7,
|
||||
base_digest: snapshot.digest,
|
||||
changes: [],
|
||||
entrypoints: ["main.dcdl"],
|
||||
@@ -197,12 +197,12 @@ Deno.test("config source API rejects mismatched entry map paths", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config source API rejects unsafe revisions and oversized entry content", () => {
|
||||
Deno.test("config source API rejects removed snapshot revision fields and oversized entry content", () => {
|
||||
assertThrows(
|
||||
() =>
|
||||
parseWorkspaceConfigTreeResponse({
|
||||
...tree,
|
||||
snapshot: { ...snapshot, revision: Number.MAX_SAFE_INTEGER + 1 },
|
||||
snapshot: { ...snapshot, revision: 7 },
|
||||
}),
|
||||
ConfigSourceApiError,
|
||||
"invalid response",
|
||||
@@ -251,7 +251,6 @@ Deno.test("config source API does not materialize imported builtins in the works
|
||||
Deno.test("config source API preserves unknown read-only builtin diagnostics without fallback requests", async () => {
|
||||
const diagnostic = {
|
||||
path: "main.dcdl",
|
||||
revision: 7,
|
||||
tree_digest: "sha256:tree",
|
||||
kind: "import",
|
||||
span: { start_byte: 8, end_byte: 40 },
|
||||
@@ -270,7 +269,6 @@ Deno.test("config source API preserves unknown read-only builtin diagnostics wit
|
||||
const error = await assertRejects(
|
||||
() =>
|
||||
commitConfigTree("w", {
|
||||
base_revision: 7,
|
||||
base_digest: "sha256:tree",
|
||||
changes: [],
|
||||
entrypoints: ["main.dcdl"],
|
||||
@@ -290,7 +288,6 @@ Deno.test("config source API surfaces bounded failed evaluation details", async
|
||||
let message = "";
|
||||
try {
|
||||
await commitConfigTree("w", {
|
||||
base_revision: 1,
|
||||
base_digest: "sha256:base",
|
||||
changes: [],
|
||||
entrypoints: [],
|
||||
|
||||
@@ -156,7 +156,7 @@ Deno.test("commit formats every working Decodal source without a preview roundtr
|
||||
assert(
|
||||
source.includes("await formatWorkingSources();") &&
|
||||
source.includes("entrypoints: entrypoints()") &&
|
||||
source.includes("Committed formatted revision"),
|
||||
source.includes("Committed formatted source tree"),
|
||||
"commit should format first and send the working changes directly to Backend authority",
|
||||
);
|
||||
assert(
|
||||
@@ -167,7 +167,7 @@ Deno.test("commit formats every working Decodal source without a preview roundtr
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config diagnostics analyze continuously with debounce and generation fencing", async () => {
|
||||
Deno.test("config diagnostics analyze continuously with debounce and request cancellation", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../src/lib/workspace/config-source/ConfigSourceEditor.svelte",
|
||||
@@ -179,7 +179,7 @@ Deno.test("config diagnostics analyze continuously with debounce and generation
|
||||
source.includes("setTimeout(() =>") &&
|
||||
source.includes("}, 250)") &&
|
||||
source.includes("analyzer.analyze(path, value)") &&
|
||||
source.includes("generation === analysisGeneration") &&
|
||||
source.includes("!request.signal.aborted") &&
|
||||
source.includes("analysisReady"),
|
||||
"source changes should trigger only the latest debounced analysis after snapshot initialization",
|
||||
);
|
||||
|
||||
@@ -16,7 +16,6 @@ function assert(condition: unknown, message: string): asserts condition {
|
||||
Deno.test("toolchain converts reactive-like proxies to plain Worker messages", async () => {
|
||||
const snapshot = new Proxy(
|
||||
{
|
||||
revision: 1,
|
||||
digest: "sha256:test",
|
||||
entries: {},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/// <reference lib="deno.ns" />
|
||||
|
||||
import { assertEquals } from "jsr:@std/assert";
|
||||
import { createHash } from "node:crypto";
|
||||
import { assertEquals, assertThrows } from "jsr:@std/assert";
|
||||
import init, {
|
||||
analyze_snapshot,
|
||||
apply_changes,
|
||||
changes_between,
|
||||
complete_current,
|
||||
compose_schema_bundle,
|
||||
evaluate_snapshot,
|
||||
@@ -32,6 +35,23 @@ async function digestText(text: string): Promise<string> {
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
function fixtureSnapshot(tree: Pick<ConfigTreeSnapshot, "entries">): ConfigTreeSnapshot {
|
||||
const hash = createHash("sha256").update("yoi-config-tree-v1\0");
|
||||
const entries: ConfigTreeSnapshot["entries"] = {};
|
||||
for (const path of Object.keys(tree.entries).toSorted()) {
|
||||
const entry = tree.entries[path];
|
||||
entries[path] = {
|
||||
...entry,
|
||||
content_digest: "sha256:" + createHash("sha256").update(entry.content).digest("hex"),
|
||||
};
|
||||
hash.update(path).update("\0")
|
||||
.update(entry.content_type === "decodal" ? "text/x-decodal" : "text/plain")
|
||||
.update("\0").update(entry.content).update("\0");
|
||||
}
|
||||
return { digest: "sha256:" + hash.digest("hex"), entries };
|
||||
}
|
||||
|
||||
async function toolchainFingerprint(
|
||||
entrypoints: string[],
|
||||
schemaBundle: WorkspaceConfigSchemaBundle,
|
||||
@@ -47,7 +67,6 @@ async function toolchainFingerprint(
|
||||
}
|
||||
|
||||
const snapshot: ConfigTreeSnapshot = {
|
||||
revision: 4,
|
||||
digest: "sha256:test-tree",
|
||||
entries: {
|
||||
"workspace.dcdl": {
|
||||
@@ -88,15 +107,40 @@ const mainEntrypointContract: ToolchainContract = {
|
||||
};
|
||||
|
||||
Deno.test("generated WASM evaluates the same virtual import contract", () => {
|
||||
const result = evaluate_snapshot(snapshot, contract) as {
|
||||
const result = evaluate_snapshot(fixtureSnapshot(snapshot), contract) as {
|
||||
projections: Array<{ data_json: { answer: number } }>;
|
||||
};
|
||||
assertEquals(result.projections[0].data_json, { answer: 42 });
|
||||
});
|
||||
|
||||
Deno.test("generated WASM keeps literal paths matching old cache identities as relative import bases", async () => {
|
||||
const source = "{}";
|
||||
const literalPath = `a.dcdl@text/x-decodal@${await digestText(source)}`;
|
||||
// Match the native fixture: even an unreferenced source must resolve its
|
||||
// relative import from the literal path rather than another entry's cache ID.
|
||||
const entries: ConfigTreeSnapshot["entries"] = {};
|
||||
for (const [path, content] of [
|
||||
["main.dcdl", "{}"],
|
||||
["a.dcdl", source],
|
||||
[literalPath, 'import "./shared.dcdl"'],
|
||||
["a.dcdl@text/shared.dcdl", "{ answer = 42; }"],
|
||||
]) {
|
||||
entries[path] = {
|
||||
path,
|
||||
content_type: "decodal",
|
||||
content,
|
||||
content_digest: await digestText(content),
|
||||
};
|
||||
}
|
||||
const result = evaluate_snapshot(
|
||||
fixtureSnapshot({ entries }),
|
||||
mainEntrypointContract,
|
||||
) as { projections: Array<{ data_json: unknown }> };
|
||||
assertEquals(result.projections[0].data_json, {});
|
||||
});
|
||||
|
||||
Deno.test("generated WASM accepts the mandatory main schema assertion", () => {
|
||||
const assertedSnapshot: ConfigTreeSnapshot = {
|
||||
revision: 1,
|
||||
digest: "sha256:asserted-main",
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
@@ -107,8 +151,7 @@ Deno.test("generated WASM accepts the mandatory main schema assertion", () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = evaluate_snapshot(
|
||||
assertedSnapshot,
|
||||
const result = evaluate_snapshot(fixtureSnapshot(assertedSnapshot),
|
||||
mainEntrypointContract,
|
||||
) as { projections: Array<{ data_json: { answer: number } }> };
|
||||
|
||||
@@ -116,19 +159,17 @@ Deno.test("generated WASM accepts the mandatory main schema assertion", () => {
|
||||
});
|
||||
|
||||
Deno.test("generated WASM diagnostics carry snapshot provenance", () => {
|
||||
const diagnostics = analyze_snapshot(
|
||||
snapshot,
|
||||
const diagnostics = analyze_snapshot(fixtureSnapshot(snapshot),
|
||||
"workspace.dcdl",
|
||||
"{ broken = ; }",
|
||||
) as Array<{
|
||||
path: string;
|
||||
revision: number;
|
||||
tree_digest: string;
|
||||
kind: string;
|
||||
}>;
|
||||
assertEquals(diagnostics[0].path, "workspace.dcdl");
|
||||
assertEquals(diagnostics[0].revision, 4);
|
||||
assertEquals(diagnostics[0].tree_digest, "sha256:test-tree");
|
||||
assertEquals(diagnostics[0].tree_digest, fixtureSnapshot(snapshot).digest);
|
||||
assertEquals("revision" in diagnostics[0], false);
|
||||
assertEquals(diagnostics[0].kind, "syntax");
|
||||
});
|
||||
|
||||
@@ -153,7 +194,6 @@ const schemaBundle = compose_schema_bundle([
|
||||
|
||||
function schemaSnapshot(source: string): ConfigTreeSnapshot {
|
||||
return {
|
||||
revision: 7,
|
||||
digest: "sha256:schema-tree",
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
@@ -177,7 +217,6 @@ const schemaContract: ToolchainContract = {
|
||||
};
|
||||
|
||||
const markdownSnapshot: ConfigTreeSnapshot = {
|
||||
revision: 8,
|
||||
digest: "sha256:markdown-tree",
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
@@ -203,7 +242,7 @@ const markdownContract: ToolchainContract = {
|
||||
};
|
||||
|
||||
Deno.test("generated WASM evaluates Markdown with the shared Skill projection", () => {
|
||||
const result = evaluate_snapshot(markdownSnapshot, markdownContract) as {
|
||||
const result = evaluate_snapshot(fixtureSnapshot(markdownSnapshot), markdownContract) as {
|
||||
projections: Array<{ data_json: Record<string, unknown> }>;
|
||||
};
|
||||
assertEquals(result.projections[0].data_json, {
|
||||
@@ -219,10 +258,9 @@ Deno.test("generated WASM evaluates Markdown with the shared Skill projection",
|
||||
});
|
||||
|
||||
Deno.test("generated WASM applies Decodal 0.4 typed maps and explicit object rest", () => {
|
||||
const result = evaluate_snapshot(
|
||||
schemaSnapshot(
|
||||
const result = evaluate_snapshot(fixtureSnapshot(schemaSnapshot(
|
||||
"{ features = { console = { enabled = true; }; }; web = { enabled = true; extension_value = 42; }; }",
|
||||
),
|
||||
)),
|
||||
schemaContract,
|
||||
) as { projections: Array<{ data_json: Record<string, unknown> }> };
|
||||
assertEquals(result.projections[0].data_json, {
|
||||
@@ -240,7 +278,7 @@ type ProjectedDiagnostic = {
|
||||
|
||||
function evaluateFailure(source: string): ProjectedDiagnostic {
|
||||
try {
|
||||
evaluate_snapshot(schemaSnapshot(source), schemaContract);
|
||||
evaluate_snapshot(fixtureSnapshot(schemaSnapshot(source)), schemaContract);
|
||||
} catch (error) {
|
||||
const diagnostics = error as ProjectedDiagnostic[];
|
||||
assertEquals(Array.isArray(diagnostics), true);
|
||||
@@ -274,7 +312,7 @@ Deno.test("generated WASM preserves native Decodal 0.4 diagnostic semantics", ()
|
||||
|
||||
Deno.test("generated WASM returns completion items for the editor adapter", () => {
|
||||
const source = 'import "./"';
|
||||
set_snapshot({
|
||||
set_snapshot(fixtureSnapshot({
|
||||
...snapshot,
|
||||
entries: {
|
||||
...snapshot.entries,
|
||||
@@ -283,7 +321,7 @@ Deno.test("generated WASM returns completion items for the editor adapter", () =
|
||||
content: source,
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
const result = complete_current(
|
||||
"workspace.dcdl",
|
||||
source,
|
||||
@@ -301,7 +339,7 @@ Deno.test("generated WASM returns completion items for the editor adapter", () =
|
||||
|
||||
Deno.test("generated WASM exposes read-only builtin completions without editable builtin entries", () => {
|
||||
const tree = schemaSnapshot("{}");
|
||||
set_snapshot(tree);
|
||||
set_snapshot(fixtureSnapshot(tree));
|
||||
set_schema_bundle(emptySchemaBundle);
|
||||
const source = 'let 名 = 1; import "$builtin/profiles/comp"';
|
||||
const result = complete_current(
|
||||
@@ -320,8 +358,7 @@ Deno.test("generated WASM exposes read-only builtin completions without editable
|
||||
assertEquals(companion?.kind, "file");
|
||||
assertEquals(companion?.detail, "read-only builtin Decodal source");
|
||||
assertEquals(Object.keys(tree.entries), ["main.dcdl"]);
|
||||
const evaluated = evaluate_snapshot(
|
||||
schemaSnapshot('import "$builtin/profiles/companion.dcdl"'),
|
||||
const evaluated = evaluate_snapshot(fixtureSnapshot(schemaSnapshot('import "$builtin/profiles/companion.dcdl"')),
|
||||
mainEntrypointContract,
|
||||
) as {
|
||||
projections: Array<{ data_json: { slug: string } }>;
|
||||
@@ -338,16 +375,14 @@ Deno.test("generated WASM rejects unknown builtin imports without same-suffix wo
|
||||
content_digest: "sha256:workspace-fallback",
|
||||
};
|
||||
set_schema_bundle(emptySchemaBundle);
|
||||
const diagnostics = analyze_snapshot(tree, "main.dcdl", undefined) as Array<{
|
||||
const diagnostics = analyze_snapshot(fixtureSnapshot(tree), "main.dcdl", undefined) as Array<{
|
||||
path: string;
|
||||
revision: number;
|
||||
tree_digest: string;
|
||||
message: string;
|
||||
}>;
|
||||
assertEquals(diagnostics.length > 0, true);
|
||||
assertEquals(diagnostics[0].path, "main.dcdl");
|
||||
assertEquals(diagnostics[0].revision, tree.revision);
|
||||
assertEquals(diagnostics[0].tree_digest, tree.digest);
|
||||
assertEquals(diagnostics[0].tree_digest, fixtureSnapshot(tree).digest);
|
||||
assertEquals(
|
||||
diagnostics[0].message.includes(
|
||||
"unknown or non-public read-only builtin source",
|
||||
@@ -360,7 +395,7 @@ Deno.test("generated WASM rejects unknown builtin imports without same-suffix wo
|
||||
);
|
||||
let failed = false;
|
||||
try {
|
||||
evaluate_snapshot(tree, mainEntrypointContract);
|
||||
evaluate_snapshot(fixtureSnapshot(tree), mainEntrypointContract);
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
assertEquals(
|
||||
@@ -372,26 +407,24 @@ Deno.test("generated WASM rejects unknown builtin imports without same-suffix wo
|
||||
}
|
||||
assertEquals(failed, true);
|
||||
assertEquals(
|
||||
analyze_snapshot(tree, "main.dcdl", 'import "./profiles/missing.dcdl"'),
|
||||
analyze_snapshot(fixtureSnapshot(tree), "main.dcdl", 'import "./profiles/missing.dcdl"'),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
function importFailure(tree: ConfigTreeSnapshot): ProjectedDiagnostic[] {
|
||||
try {
|
||||
evaluate_snapshot(tree, mainEntrypointContract);
|
||||
evaluate_snapshot(fixtureSnapshot(tree), mainEntrypointContract);
|
||||
} catch (error) {
|
||||
// A WASM trap/stack overflow must not count as a structured import failure.
|
||||
assertEquals(Array.isArray(error), true);
|
||||
const diagnostics = error as Array<
|
||||
ProjectedDiagnostic & {
|
||||
revision: number;
|
||||
tree_digest: string;
|
||||
}
|
||||
>;
|
||||
assertEquals(diagnostics.length > 0, true);
|
||||
assertEquals(diagnostics[0].revision, tree.revision);
|
||||
assertEquals(diagnostics[0].tree_digest, tree.digest);
|
||||
assertEquals(diagnostics[0].tree_digest, fixtureSnapshot(tree).digest);
|
||||
return diagnostics;
|
||||
}
|
||||
throw new Error("expected a structured import diagnostic");
|
||||
@@ -427,7 +460,7 @@ Deno.test("generated WASM enforces the import depth budget on a real 33-deep cha
|
||||
return tree;
|
||||
}
|
||||
// The synthetic evaluation-wrapper import consumes one of the 32 edges.
|
||||
const accepted = evaluate_snapshot(chain(31), mainEntrypointContract) as {
|
||||
const accepted = evaluate_snapshot(fixtureSnapshot(chain(31)), mainEntrypointContract) as {
|
||||
projections: Array<{ data_json: { answer: number } }>;
|
||||
};
|
||||
assertEquals(accepted.projections[0].data_json, { answer: 42 });
|
||||
@@ -447,7 +480,7 @@ Deno.test("generated WASM completes blank nested schema positions after Unicode"
|
||||
const source =
|
||||
'{ description = "日本語"; profile = { }\n} as WorkspaceConfigSchema';
|
||||
const cursor = source.indexOf("{ }") + 2;
|
||||
set_snapshot({
|
||||
set_snapshot(fixtureSnapshot({
|
||||
...snapshot,
|
||||
entries: {
|
||||
...snapshot.entries,
|
||||
@@ -456,7 +489,7 @@ Deno.test("generated WASM completes blank nested schema positions after Unicode"
|
||||
content: source,
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
set_schema_bundle({
|
||||
contributions: [],
|
||||
source: "{ profile = { default_profile = String; }; prompts = {}; }",
|
||||
@@ -486,7 +519,7 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () =>
|
||||
const bareSource = "{ pro }";
|
||||
const source = "{ pro } as WorkspaceConfigSchema";
|
||||
const cursor = source.indexOf("pro") + 3;
|
||||
set_snapshot({
|
||||
set_snapshot(fixtureSnapshot({
|
||||
...snapshot,
|
||||
entries: {
|
||||
...snapshot.entries,
|
||||
@@ -495,7 +528,7 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () =>
|
||||
content: source,
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
set_schema_bundle({
|
||||
contributions: [],
|
||||
source: "{ profile = { default_profile = String; }; prompts = {}; }",
|
||||
@@ -566,7 +599,6 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
const content =
|
||||
'{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }]; }; } as WorkspaceConfigSchema';
|
||||
const tree: ConfigTreeSnapshot = {
|
||||
revision: 8,
|
||||
digest: "sha256:value-profile-tree",
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
@@ -583,9 +615,9 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
schema_bundle: schema,
|
||||
fingerprint: await toolchainFingerprint(["main.dcdl"], schema),
|
||||
};
|
||||
set_snapshot(tree);
|
||||
set_snapshot(fixtureSnapshot(tree));
|
||||
set_schema_bundle(schema);
|
||||
const evaluated = evaluate_snapshot(tree, valueContract) as {
|
||||
const evaluated = evaluate_snapshot(fixtureSnapshot(tree), valueContract) as {
|
||||
projections: Array<
|
||||
{ data_json: { profile: { entries: Array<{ profile: unknown }> } } }
|
||||
>;
|
||||
@@ -648,13 +680,13 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
`profile = ${value};`,
|
||||
);
|
||||
assertEquals(
|
||||
analyze_snapshot(tree, "main.dcdl", partialContent),
|
||||
analyze_snapshot(fixtureSnapshot(tree), "main.dcdl", partialContent),
|
||||
[],
|
||||
value,
|
||||
);
|
||||
}
|
||||
const recipe = "{ worker = { mode = 42; }; }";
|
||||
const analysisTree = {
|
||||
const analysisTree: ConfigTreeSnapshot = {
|
||||
...tree,
|
||||
entries: {
|
||||
...tree.entries,
|
||||
@@ -685,13 +717,12 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
"profile = {};",
|
||||
`profile = ${value};`,
|
||||
);
|
||||
const diagnostics = analyze_snapshot(
|
||||
analysisTree,
|
||||
const diagnostics = analyze_snapshot(fixtureSnapshot(analysisTree),
|
||||
"main.dcdl",
|
||||
invalidContent,
|
||||
) as Array<{
|
||||
path: string;
|
||||
revision: number;
|
||||
tree_digest: string;
|
||||
kind: string;
|
||||
span: { start_byte: number; end_byte: number };
|
||||
message: string;
|
||||
@@ -702,7 +733,7 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
assertEquals(diagnostics.length > 0, true, value);
|
||||
const diagnostic = diagnostics[0];
|
||||
assertEquals(diagnostic.path, expectedPath);
|
||||
assertEquals(diagnostic.revision, tree.revision);
|
||||
assertEquals(diagnostic.tree_digest, fixtureSnapshot(analysisTree).digest);
|
||||
assertEquals(
|
||||
["constraint_violation", "type_mismatch"].includes(diagnostic.kind),
|
||||
true,
|
||||
@@ -783,7 +814,7 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
"profile = {};",
|
||||
`profile = ${form};`,
|
||||
);
|
||||
const scopedTree = {
|
||||
const scopedTree: ConfigTreeSnapshot = {
|
||||
...tree,
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
@@ -800,12 +831,11 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
},
|
||||
};
|
||||
assertEquals(
|
||||
analyze_snapshot(scopedTree, "main.dcdl", undefined),
|
||||
analyze_snapshot(fixtureSnapshot(scopedTree), "main.dcdl", undefined),
|
||||
[],
|
||||
form,
|
||||
);
|
||||
const result = evaluate_snapshot(
|
||||
scopedTree,
|
||||
const result = evaluate_snapshot(fixtureSnapshot(scopedTree),
|
||||
valueContract,
|
||||
) as typeof evaluated;
|
||||
assertEquals(
|
||||
@@ -839,7 +869,7 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
"profile = {};",
|
||||
`profile = ${form};`,
|
||||
);
|
||||
const fixtureTree = {
|
||||
const fixtureTree: ConfigTreeSnapshot = {
|
||||
...tree,
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
@@ -856,12 +886,11 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
},
|
||||
};
|
||||
assertEquals(
|
||||
analyze_snapshot(fixtureTree, "main.dcdl", undefined),
|
||||
analyze_snapshot(fixtureSnapshot(fixtureTree), "main.dcdl", undefined),
|
||||
[],
|
||||
`${fixture.name}: ${form}`,
|
||||
);
|
||||
const result = evaluate_snapshot(
|
||||
fixtureTree,
|
||||
const result = evaluate_snapshot(fixtureSnapshot(fixtureTree),
|
||||
valueContract,
|
||||
) as typeof evaluated;
|
||||
assertEquals(
|
||||
@@ -896,5 +925,65 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
|
||||
);
|
||||
}
|
||||
// Neither completion nor diagnostic shape checking changes the saved value.
|
||||
assertEquals(evaluate_snapshot(tree, valueContract), evaluated);
|
||||
assertEquals(evaluate_snapshot(fixtureSnapshot(tree), valueContract), evaluated);
|
||||
});
|
||||
|
||||
Deno.test("generated WASM returns to the same content digest and enforces entry digest CAS", () => {
|
||||
const base = fixtureSnapshot(schemaSnapshot("{ answer = 1; }"));
|
||||
set_schema_bundle(emptySchemaBundle);
|
||||
set_snapshot(base);
|
||||
const changed = apply_changes([{
|
||||
kind: "update", path: "main.dcdl",
|
||||
expected_digest: base.entries["main.dcdl"].content_digest,
|
||||
content: "{ broken = ; }",
|
||||
}]) as ConfigTreeSnapshot;
|
||||
assertEquals(changed.digest === base.digest, false);
|
||||
assertEquals("revision" in changed, false);
|
||||
const diagnostics = analyze_snapshot(changed, "main.dcdl", undefined) as Array<{ tree_digest: string }>;
|
||||
assertEquals(diagnostics.length > 0, true);
|
||||
assertEquals(diagnostics.every((diagnostic) => diagnostic.tree_digest === changed.digest), true);
|
||||
assertThrows(() => apply_changes([{
|
||||
kind: "update", path: "main.dcdl",
|
||||
expected_digest: base.entries["main.dcdl"].content_digest,
|
||||
content: "{ answer = 2; }",
|
||||
}]));
|
||||
const restored = apply_changes(changes_between(changed, base)) as ConfigTreeSnapshot;
|
||||
assertEquals(restored, base);
|
||||
assertEquals(analyze_snapshot(restored, "main.dcdl", undefined), []);
|
||||
});
|
||||
|
||||
Deno.test("generated WASM rejects cache URI paths in snapshot JSON despite valid content digests", async () => {
|
||||
const source = "{}";
|
||||
const cacheUri = `config-source://a.dcdl@text/x-decodal@${await digestText(source)}`;
|
||||
// Both entry and tree digests are correct, so rejection must come from the
|
||||
// untrusted VirtualPath boundary rather than a content-identity mismatch.
|
||||
const snapshot = fixtureSnapshot({
|
||||
entries: {
|
||||
"a.dcdl": {
|
||||
path: "a.dcdl",
|
||||
content_type: "decodal",
|
||||
content: source,
|
||||
content_digest: await digestText(source),
|
||||
},
|
||||
[cacheUri]: {
|
||||
path: cacheUri,
|
||||
content_type: "decodal",
|
||||
content: 'import "./shared.dcdl"',
|
||||
content_digest: await digestText('import "./shared.dcdl"'),
|
||||
},
|
||||
},
|
||||
});
|
||||
const json = JSON.stringify(snapshot);
|
||||
const rejection = assertThrows(() => set_snapshot(JSON.parse(json)));
|
||||
assertEquals(String(rejection).includes("config-source://"), true);
|
||||
});
|
||||
|
||||
Deno.test("generated WASM rejects forged content identity at the snapshot boundary", () => {
|
||||
const base = fixtureSnapshot(schemaSnapshot("{}"));
|
||||
for (const forged of [
|
||||
{ ...base, digest: "sha256:forged" },
|
||||
{ ...base, entries: { "main.dcdl": { ...base.entries["main.dcdl"], content: "{ changed = true; }" } } },
|
||||
]) {
|
||||
assertThrows(() => set_snapshot(forged));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,19 +34,19 @@ Start `tools/web-ux/drive-fixture/server.ts <port> <build-root>` with `--allow-n
|
||||
- `*denied*`: typed HTTP 403 root/read; `*offline*`/`home-error`: typed HTTP 503; `*empty*`: empty successful root list; `*paged*`: 240 additional nodes.
|
||||
- `POST /__fixture/fault` JSON `{workspace, mode}`: one-shot `conflict` or committed `unknown` mutation response; persistent `image_error` binary fetch failure.
|
||||
- `home-owner`/`home-member`/`home-long`: representative authoritative Worker catalogs; REST and subscription projections come from the same generated-shape records. `home-long` includes long display names and opaque Runtime/Worker IDs. `workers-error` intentionally fails catalog reads with HTTP 503.
|
||||
- `GET /__fixture/log`: observable API request IDs/methods/paths/node IDs/CAS revisions for identity and no-resend assertions.
|
||||
- `GET /__fixture/log`: observable API request IDs/methods/paths/node IDs/CAS observed committed requests for identity and no-resend assertions.
|
||||
- `POST /__fixture/reset`: fresh in-memory state.
|
||||
|
||||
Request delay/loss tests intercept the API boundary with Playwright and synchronize on gates, not guessed sleeps. They let the fixture commit before dropping/holding the response, then verify receipt query without blind upload retry.
|
||||
|
||||
## Visual evidence
|
||||
|
||||
Preserved production baseline: `.svelte-kit/t724/before-build/{site,output,build.log}`. Official bundles live below `.svelte-kit/t724/visual/`. Each contains screenshots, console/request/page diagnostics, accessibility snapshots, screenshot hashes, source revision/dirty context, and `review-context.json`.
|
||||
Preserved production baseline: `.svelte-kit/t724/before-build/{site,output,build.log}`. Official bundles live below `.svelte-kit/t724/visual/`. Each contains screenshots, console/request/page diagnostics, accessibility snapshots, screenshot hashes, source commit/dirty context, and `review-context.json`.
|
||||
|
||||
Home/sidebar and Workers are captured under identical owner/member/error/empty/long conditions, light/dark at 1440/768/390/320. After captures add Drive owner/member/empty/error/denied/paged/Markdown/image/image-error/truncated surfaces, pinned rename/replace revision forms, and owner grant selection/read-only/read-write/revoked states. Expected injected 503/403 diagnostics and superseded breadcrumb metadata aborts remain in official context; `diagnostic-disposition.json` explicitly separates unexpected diagnostics. CLI `completed-with-errors` is not relabeled a visual pass.
|
||||
Home/sidebar and Workers are captured under identical owner/member/error/empty/long conditions, light/dark at 1440/768/390/320. After captures add Drive owner/member/empty/error/denied/paged/Markdown/image/image-error/truncated surfaces, pinned rename/replace observed committed request forms, and owner grant selection/read-only/read-write/revoked states. Expected injected 503/403 diagnostics and superseded breadcrumb metadata aborts remain in official context; `diagnostic-disposition.json` explicitly separates unexpected diagnostics. CLI `completed-with-errors` is not relabeled a visual pass.
|
||||
|
||||
The production shell scrolls `main` internally, so `fullPage` does not expand that scroll panel. Capture points use ordinary keyboard focus to show the actual form or the top denial state; no CSS/DOM screenshot override is applied. The responsive audit separately records document/main/table-region geometry, visible keyboard focus, and page errors across the full theme/viewport matrix. Raster checks wait for actual decoding, assert natural dimensions 1200×800, preserve the 3:2 aspect ratio, and constrain the rendered box by both the available width and 60vh height at all four widths/both themes.
|
||||
|
||||
The Drive browser suite also exercises the file-browser redesign at 1440/768/390/320px in light/dark themes: menu placement, native-dialog focus and Escape restoration, folder creation, search, and destination-folder navigation. Screenshots are written to `.svelte-kit/drive-redesign/` for the list, New menu, creation dialog, Markdown preview, and move dialog. Component tests cover failed/paged destination reads, exclusion of files and the moving folder itself, and pinned mutation revisions. These checks use the same fixture boundary, not a live Backend.
|
||||
The Drive browser suite also exercises the file-browser redesign at 1440/768/390/320px in light/dark themes: menu placement, native-dialog focus and Escape restoration, folder creation, search, and destination-folder navigation. Screenshots are written to `.svelte-kit/drive-redesign/` for the list, New menu, creation dialog, Markdown preview, and move dialog. Component tests cover failed/paged destination reads, exclusion of files and the moving folder itself, and pinned mutation observed committed requests. These checks use the same fixture boundary, not a live Backend.
|
||||
|
||||
Parent and reviewer must open images and inspect context before asserting visual acceptance. In particular, a successful capture command is not sufficient and HTTP 200 does not excuse visible UI errors.
|
||||
|
||||
@@ -55,31 +55,31 @@ for (const scheme of ["light", "dark"]) {
|
||||
}
|
||||
if (stage === "after") {
|
||||
routes.push({
|
||||
id: "drive-form-revision",
|
||||
label: "Pinned rename and replace form revisions",
|
||||
id: "drive-form-observation",
|
||||
label: "Pinned rename and replace form observations",
|
||||
path: "/w/home-owner/drive/3",
|
||||
goal: "Show the form's locked read revision and explicit destination/file controls.",
|
||||
goal: "Show the form's locked read observation and explicit destination/file controls.",
|
||||
dataState:
|
||||
"Revision 1 read from fixture metadata; no mutation is sent by these capture interactions.",
|
||||
"Initial committed request read from fixture metadata; no mutation is sent by these capture interactions.",
|
||||
ready: { kind: "selector", selector: '.drive-page[data-drive-ready="true"]' },
|
||||
capturePoints: [
|
||||
{
|
||||
id: "rename",
|
||||
label: "Rename / move pinned revision",
|
||||
label: "Rename / move pinned observation",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{ action: "click", selector: '.drive-menu > summary[aria-label="More actions"]' },
|
||||
{ action: "click", selector: '.drive-toolbar button:has-text("Rename / move")' },
|
||||
{
|
||||
action: "wait",
|
||||
ready: { kind: "selector", selector: '.drive-form:has-text("Expected revision 1.")' },
|
||||
ready: { kind: "selector", selector: '.drive-form:has-text("Changes by another writer will not be overwritten.")' },
|
||||
},
|
||||
{ action: "press", selector: ".drive-form input:not([inputmode])", key: "Shift" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "replace",
|
||||
label: "Replace file pinned revision",
|
||||
label: "Replace file pinned observation",
|
||||
fullPage: true,
|
||||
interaction: [
|
||||
{ action: "click", selector: '.drive-form button:has-text("Cancel form")' },
|
||||
@@ -87,7 +87,7 @@ for (const scheme of ["light", "dark"]) {
|
||||
{ action: "click", selector: '.drive-toolbar button:has-text("Replace file")' },
|
||||
{
|
||||
action: "wait",
|
||||
ready: { kind: "selector", selector: '.drive-form:has-text("Expected revision 1.")' },
|
||||
ready: { kind: "selector", selector: '.drive-form:has-text("Changes by another writer will not be overwritten.")' },
|
||||
},
|
||||
{ action: "press", selector: ".drive-form input[type=file]", key: "Shift" },
|
||||
],
|
||||
@@ -367,7 +367,7 @@ for (const scheme of ["light", "dark"]) {
|
||||
if (
|
||||
[
|
||||
"drive-markdown",
|
||||
"drive-form-revision",
|
||||
"drive-form-observation",
|
||||
"drive-image",
|
||||
"drive-image-error",
|
||||
"drive-truncated",
|
||||
|
||||
@@ -10,7 +10,7 @@ const ready = (page: Page) =>
|
||||
const button = (page: Page, name: string) =>
|
||||
page.getByRole("button", { name, exact: true });
|
||||
const draft = (page: Page) =>
|
||||
page.getByRole("textbox", { name: /^Draft \(expected revision/ });
|
||||
page.getByRole("textbox", { name: /^Draft$/ });
|
||||
const preview = (page: Page) =>
|
||||
page.getByRole("article", { name: "Current saved content" });
|
||||
function gate() {
|
||||
@@ -135,7 +135,7 @@ async function safeFulfill(
|
||||
} catch { /* selection intentionally canceled this old request */ }
|
||||
}
|
||||
|
||||
Deno.test("production Drive two tabs retain losing CAS draft and never retry with a newer revision", () =>
|
||||
Deno.test("production Drive two tabs retain losing CAS draft and never retry with a newer observation", () =>
|
||||
harness(async (origin, browser) => {
|
||||
const context = await browser.newContext();
|
||||
const first = await context.newPage(), second = await context.newPage();
|
||||
@@ -151,7 +151,7 @@ Deno.test("production Drive two tabs retain losing CAS draft and never retry wit
|
||||
await first.getByText("Published — DB commit confirmed", { exact: false })
|
||||
.last().waitFor();
|
||||
await button(second, "Save").click();
|
||||
await second.getByText("Revision conflict — your draft is retained.", {
|
||||
await second.getByText("Content changed — your draft is retained.", {
|
||||
exact: true,
|
||||
})
|
||||
.waitFor();
|
||||
@@ -167,7 +167,7 @@ Deno.test("production Drive two tabs retain losing CAS draft and never retry wit
|
||||
"Second actor unsaved draft",
|
||||
);
|
||||
assert(
|
||||
(await second.getByText("Draft (expected revision 1)", { exact: false })
|
||||
(await second.getByText("Draft", { exact: false })
|
||||
.count()) > 0,
|
||||
);
|
||||
assert(
|
||||
@@ -187,7 +187,7 @@ Deno.test("production Drive two tabs retain losing CAS draft and never retry wit
|
||||
const casWrites = (await logs(origin)).filter((x) =>
|
||||
x.path === "/mutate"
|
||||
);
|
||||
assertEquals(casWrites.map((x) => [x.nodeId, x.expectedRevision]), [[
|
||||
assertEquals(casWrites.map((x) => [x.nodeId, x.expectedMutationId]), [[
|
||||
"3",
|
||||
"1",
|
||||
], ["3", "1"]]);
|
||||
@@ -229,7 +229,7 @@ Deno.test("production Drive delayed old read cannot replace another file or work
|
||||
.last().waitFor();
|
||||
const mutation = (await logs(origin)).filter((x) => x.path === "/mutate");
|
||||
assertEquals(
|
||||
mutation.map((x) => [x.workspace, x.nodeId, x.expectedRevision]),
|
||||
mutation.map((x) => [x.workspace, x.nodeId, x.expectedMutationId]),
|
||||
[[
|
||||
"home-owner",
|
||||
"4",
|
||||
@@ -330,12 +330,14 @@ Deno.test("production Drive delayed old save and status preserve destination dra
|
||||
const pinnedWrites = (await logs(origin)).filter((x) =>
|
||||
x.path === "/mutate"
|
||||
);
|
||||
assertEquals(pinnedWrites.map((x) => [x.nodeId, x.expectedRevision]), [
|
||||
assertEquals(pinnedWrites.map((x) => [x.nodeId, x.expectedMutationId]), [
|
||||
["3", "1"],
|
||||
["4", "1"],
|
||||
["3", "2"],
|
||||
["4", "2"],
|
||||
["3", pinnedWrites[0].requestId],
|
||||
["4", pinnedWrites[1].requestId],
|
||||
]);
|
||||
assert(pinnedWrites.every((x) => typeof x.requestId === "string" && x.requestId.length > 0));
|
||||
assertEquals(new Set(pinnedWrites.map((x) => x.requestId)).size, 4);
|
||||
} finally {
|
||||
release.release();
|
||||
await handled;
|
||||
|
||||
@@ -47,13 +47,13 @@ Deno.test("browser fixture preserves decimal identity, bounded reads, binary and
|
||||
assertEquals(escaped.text, "\u0000".repeat(65536));
|
||||
assertEquals(escaped.truncated, false);
|
||||
});
|
||||
Deno.test("browser fixture two revisions conflict without committing the losing request", async () => {
|
||||
Deno.test("browser fixture two last_mutation_ids conflict without committing the losing request", async () => {
|
||||
const { client } = setup();
|
||||
const entry = await client.metadata("home-owner", ref("home-owner", "3"));
|
||||
await client.mutate("home-owner", "tab-one", {
|
||||
operation: "update_text",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
expected_mutation_id: entry.last_mutation_id,
|
||||
text: "first actor",
|
||||
content_type: "text/markdown",
|
||||
});
|
||||
@@ -62,7 +62,7 @@ Deno.test("browser fixture two revisions conflict without committing the losing
|
||||
client.mutate("home-owner", "tab-two", {
|
||||
operation: "update_text",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
expected_mutation_id: entry.last_mutation_id,
|
||||
text: "second actor draft",
|
||||
content_type: "text/markdown",
|
||||
}),
|
||||
@@ -113,7 +113,7 @@ Deno.test("browser fixture rename and move retain identity while delete and recr
|
||||
const moved = await client.mutate("home-owner", "move", {
|
||||
operation: "relocate",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
expected_mutation_id: entry.last_mutation_id,
|
||||
parent: ref("home-owner", "2"),
|
||||
name: "renamed.md",
|
||||
});
|
||||
@@ -122,7 +122,7 @@ Deno.test("browser fixture rename and move retain identity while delete and recr
|
||||
await client.mutate("home-owner", "delete", {
|
||||
operation: "delete",
|
||||
id: entry.entry,
|
||||
expected_revision: entry.revision,
|
||||
expected_mutation_id: entry.last_mutation_id,
|
||||
});
|
||||
const replacement = await client.mutate("home-owner", "recreate", {
|
||||
operation: "create_text",
|
||||
|
||||
@@ -6,7 +6,7 @@ The ordinary Rust test exercises loopback HTTP signed Worker ingress: explicit r
|
||||
|
||||
The explicit process test starts a loopback Axum Server and launches Deno against it. `roundtrip.ts` imports the production `src/lib/workspace/drive/api.ts` adapter, not a copy or mock. Its typed `fetchFn` resolves relative paths only to the bound loopback origin and adds fixture bearer authorization. Tokens travel over stdin, not command-line arguments or logs; Rust failure diagnostics defensively redact both fixture bearer tokens. Deno has network permission only for the exact loopback port, uses no npm dependencies, and runs with `--cached-only`. Each listener is abort-on-drop, child processes are kill-on-drop, and the child has a 60-second upper timeout, without sleeps or readiness polling.
|
||||
|
||||
Coverage: Worker file list/read, Japanese folder/Markdown names, create/read/edit/download, empty files, actual PNG upload/authenticated image/download and binary update, publication receipt lookup, paged list, two authenticated accounts racing a common revision (one commit/one typed conflict, no hidden retry), stable latest URL through rename and move, deleted URL and same-name recreation, and unauthorized latest URLs. The exact Workers-page `drive-grants/api.ts` adapter also lists persisted grants, creates both access levels, revokes them and re-reads durable state, and rejects member administration. Its global browser fetch is temporarily origin/auth-bound in this isolated child; network responses are never mocked. This tests real authenticated HTTP with test bearer credentials, not a Passkey/cookie browser login or a full Runtime process. UI dirty-draft retention, delayed response handling, visual review and browser cancellation belong to the **separate browser fixture**, not this API test.
|
||||
Coverage: Worker file list/read, Japanese folder/Markdown names, create/read/edit/download, empty files, actual PNG upload/authenticated image/download and binary update, publication receipt lookup, paged list, two authenticated accounts racing a common observed committed request (one commit/one typed conflict, no hidden retry), stable latest URL through rename and move, deleted URL and same-name recreation, and unauthorized latest URLs. The exact Workers-page `drive-grants/api.ts` adapter also lists persisted grants, creates both access levels, revokes them and re-reads durable state, and rejects member administration. Its global browser fetch is temporarily origin/auth-bound in this isolated child; network responses are never mocked. This tests real authenticated HTTP with test bearer credentials, not a Passkey/cookie browser login or a full Runtime process. UI dirty-draft retention, delayed response handling, visual review and browser cancellation belong to the **separate browser fixture**, not this API test.
|
||||
|
||||
## Registration
|
||||
|
||||
|
||||
@@ -93,9 +93,9 @@ async function download(
|
||||
entry: DriveEntry,
|
||||
): Promise<Uint8Array> {
|
||||
const response = await fetchFn(
|
||||
driveDownloadUrl(ws, entry.entry, entry.revision),
|
||||
driveDownloadUrl(ws, entry.entry, entry.last_mutation_id),
|
||||
);
|
||||
assert(response.status === 200, "download must succeed at exact revision");
|
||||
assert(response.status === 200, "download must succeed at exact last_mutation_id");
|
||||
equal(
|
||||
response.headers.get("x-content-type-options"),
|
||||
"nosniff",
|
||||
@@ -203,7 +203,7 @@ equal(
|
||||
const updated = await saved(owner, ws, "web-edit", {
|
||||
operation: "update_text",
|
||||
id: markdown.entry,
|
||||
expected_revision: markdown.revision,
|
||||
expected_mutation_id: markdown.last_mutation_id,
|
||||
text: "# 更新\n",
|
||||
content_type: "text/markdown",
|
||||
});
|
||||
@@ -274,7 +274,7 @@ const binaryUpdated = await owner.upload(
|
||||
{
|
||||
operation: "update",
|
||||
id: empty.entry,
|
||||
expected_revision: empty.revision,
|
||||
expected_mutation_id: empty.last_mutation_id,
|
||||
content_type: "image/png",
|
||||
},
|
||||
);
|
||||
@@ -313,12 +313,12 @@ equal(
|
||||
"paged traversal must contain each file once",
|
||||
);
|
||||
|
||||
// Two authenticated accounts read the same revision, then race real HTTP writes.
|
||||
// Two authenticated accounts read the same last_mutation_id, then race real HTTP writes.
|
||||
const secondRead = await second.readText(ws, updated.entry);
|
||||
equal(
|
||||
secondRead.entry.revision,
|
||||
updated.revision,
|
||||
"second actor must observe same revision",
|
||||
secondRead.entry.last_mutation_id,
|
||||
updated.last_mutation_id,
|
||||
"second actor must observe same last_mutation_id",
|
||||
);
|
||||
const drafts = ["# actor A draft\n", "# actor B draft\n"];
|
||||
const race = await Promise.allSettled(
|
||||
@@ -326,7 +326,7 @@ const race = await Promise.allSettled(
|
||||
saved(client, ws, `web-race-${index}`, {
|
||||
operation: "update_text",
|
||||
id: updated.entry,
|
||||
expected_revision: updated.revision,
|
||||
expected_mutation_id: updated.last_mutation_id,
|
||||
text: drafts[index],
|
||||
content_type: "text/markdown",
|
||||
})
|
||||
@@ -335,7 +335,7 @@ const race = await Promise.allSettled(
|
||||
equal(
|
||||
race.filter((result) => result.status === "fulfilled").length,
|
||||
1,
|
||||
"same-revision two-actor CAS must have exactly one winner",
|
||||
"same-last_mutation_id two-actor CAS must have exactly one winner",
|
||||
);
|
||||
const loser = race.find((result) => result.status === "rejected");
|
||||
assert(
|
||||
@@ -358,14 +358,14 @@ const stableUrl = markdown.latest_url;
|
||||
const renamed = await saved(owner, ws, "web-rename", {
|
||||
operation: "relocate",
|
||||
id: latest.entry.entry,
|
||||
expected_revision: latest.entry.revision,
|
||||
expected_mutation_id: latest.entry.last_mutation_id,
|
||||
parent: folder.entry,
|
||||
name: "改名.md",
|
||||
});
|
||||
const moved = await saved(owner, ws, "web-move", {
|
||||
operation: "relocate",
|
||||
id: renamed.entry,
|
||||
expected_revision: renamed.revision,
|
||||
expected_mutation_id: renamed.last_mutation_id,
|
||||
parent: root.entry,
|
||||
name: renamed.name,
|
||||
});
|
||||
@@ -397,7 +397,7 @@ await rejected(
|
||||
const deleted = await owner.mutate(ws, "web-delete", {
|
||||
operation: "delete",
|
||||
id: moved.entry,
|
||||
expected_revision: moved.revision,
|
||||
expected_mutation_id: moved.last_mutation_id,
|
||||
});
|
||||
equal(deleted.entry, null, "delete must remove live metadata");
|
||||
await rejected(() => owner.readText(ws, moved.entry), "not_found");
|
||||
|
||||
@@ -40,7 +40,7 @@ function reviewRequested(
|
||||
event_id: eventId,
|
||||
sequence: 1,
|
||||
subject_ref: subjectRef,
|
||||
ticket_item_revision: "ticket-revision",
|
||||
ticket_content_digest: "ticket-digest",
|
||||
ticket_merge_request_subjects: [
|
||||
{ merge_request_id: "MR-1", subject_ref: subjectRef },
|
||||
],
|
||||
@@ -61,7 +61,7 @@ function review(
|
||||
sequence: 1,
|
||||
request_event_id: requestEventId,
|
||||
subject_ref: subjectRef,
|
||||
ticket_item_revision: "ticket-revision",
|
||||
ticket_content_digest: "ticket-digest",
|
||||
ticket_merge_request_subjects: [
|
||||
{ merge_request_id: "MR-1", subject_ref: subjectRef },
|
||||
],
|
||||
|
||||
@@ -37,7 +37,6 @@ const credential = {
|
||||
name: "Deploy key",
|
||||
public_key_algorithm: "ssh-ed25519",
|
||||
public_key_fingerprint: "SHA256:credential",
|
||||
current_revision: 2,
|
||||
status: "active",
|
||||
created_at: "2026-09-01T00:00:00Z",
|
||||
rotated_at: null,
|
||||
@@ -52,7 +51,6 @@ const hostTrust = {
|
||||
key_algorithm: "ssh-ed25519",
|
||||
host_key: "ssh-ed25519 AAAA",
|
||||
fingerprint: "SHA256:host",
|
||||
current_revision: 3,
|
||||
created_at: "2026-09-01T00:00:00Z",
|
||||
updated_at: "2026-09-02T00:00:00Z",
|
||||
referenced_repositories: ["main"],
|
||||
@@ -63,14 +61,12 @@ Deno.test("Repository Access parsers accept generated response contracts", () =>
|
||||
assertEquals(
|
||||
parseRepositorySshPublicKey({
|
||||
credential_id: "deploy-key",
|
||||
current_revision: 2,
|
||||
public_key_algorithm: "ssh-ed25519",
|
||||
public_key_fingerprint: "SHA256:credential",
|
||||
public_key: "ssh-ed25519 AAAA",
|
||||
}),
|
||||
{
|
||||
credential_id: "deploy-key",
|
||||
current_revision: 2,
|
||||
public_key_algorithm: "ssh-ed25519",
|
||||
public_key_fingerprint: "SHA256:credential",
|
||||
public_key: "ssh-ed25519 AAAA",
|
||||
@@ -80,7 +76,6 @@ Deno.test("Repository Access parsers accept generated response contracts", () =>
|
||||
assertEquals(
|
||||
parseRepositoryAccessProjection({
|
||||
workspace_id: "workspace-1",
|
||||
config_revision: 4,
|
||||
projection_digest: "sha256:projection",
|
||||
bindings: [{
|
||||
repository_key: "main",
|
||||
@@ -91,7 +86,6 @@ Deno.test("Repository Access parsers accept generated response contracts", () =>
|
||||
}),
|
||||
{
|
||||
workspace_id: "workspace-1",
|
||||
config_revision: 4,
|
||||
projection_digest: "sha256:projection",
|
||||
bindings: [{
|
||||
repository_key: "main",
|
||||
@@ -115,10 +109,11 @@ Deno.test("Repository Access parsers reject malformed list responses", () => {
|
||||
});
|
||||
|
||||
Deno.test("Repository Access parsers reject missing and wrong-typed fields", () => {
|
||||
const { current_revision: _revision, ...missingRevision } = credential;
|
||||
const { public_key_fingerprint: _fingerprint, ...missingFingerprint } =
|
||||
credential;
|
||||
assertSchemaError(
|
||||
() => parseRepositorySshCredentials([missingRevision]),
|
||||
"credentials[0].current_revision",
|
||||
() => parseRepositorySshCredentials([missingFingerprint]),
|
||||
"credentials[0].public_key_fingerprint",
|
||||
);
|
||||
assertSchemaError(
|
||||
() => parseRepositorySshHostTrusts([{ ...hostTrust, port: "22" }]),
|
||||
@@ -128,7 +123,6 @@ Deno.test("Repository Access parsers reject missing and wrong-typed fields", ()
|
||||
() =>
|
||||
parseRepositoryAccessProjection({
|
||||
workspace_id: "workspace-1",
|
||||
config_revision: 4,
|
||||
projection_digest: "sha256:projection",
|
||||
bindings: [{
|
||||
repository_key: "main",
|
||||
|
||||
@@ -21,7 +21,6 @@ type LoadedPage = {
|
||||
|
||||
const projectionFixture = {
|
||||
workspace_id: "workspace-1",
|
||||
config_revision: 1,
|
||||
projection_digest: "sha256:projection",
|
||||
bindings: [],
|
||||
};
|
||||
@@ -31,7 +30,6 @@ const credentialFixture = {
|
||||
name: "Workspace default",
|
||||
public_key_algorithm: "ssh-ed25519",
|
||||
public_key_fingerprint: "SHA256:credential",
|
||||
current_revision: 1,
|
||||
status: "active",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
rotated_at: null,
|
||||
@@ -39,7 +37,6 @@ const credentialFixture = {
|
||||
};
|
||||
const publicKeyFixture = {
|
||||
credential_id: "workspace-default",
|
||||
current_revision: 1,
|
||||
public_key_algorithm: "ssh-ed25519",
|
||||
public_key_fingerprint: "SHA256:credential",
|
||||
public_key: "ssh-ed25519 AAAA-workspace-default",
|
||||
@@ -52,7 +49,6 @@ const hostTrustFixture = {
|
||||
key_algorithm: "ssh-ed25519",
|
||||
host_key: "ssh-ed25519 AAAA-host",
|
||||
fingerprint: "SHA256:host",
|
||||
current_revision: 1,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
referenced_repositories: [],
|
||||
|
||||
@@ -90,9 +90,6 @@ test("Repository Access presents current state as tables with technical disclosu
|
||||
"Pinned SSH host keys",
|
||||
"repository-access-table-wrap",
|
||||
"Technical details",
|
||||
"Credential revisions",
|
||||
"Host key revisions",
|
||||
"accessProjection.config_revision",
|
||||
"accessProjection.projection_digest",
|
||||
"binding.repository_key",
|
||||
"binding.credential_id",
|
||||
|
||||
@@ -30,7 +30,7 @@ Deno.test("Repository SSH probe parser preserves the confirmation contract", ()
|
||||
port: 22,
|
||||
trust_state: "untrusted" as const,
|
||||
host_trust_id: "tofu-example.test-22",
|
||||
expected_host_trust_revision: null,
|
||||
expected_host_key_fingerprint: null,
|
||||
candidates: [
|
||||
{
|
||||
algorithm: "ssh-ed25519",
|
||||
@@ -136,7 +136,7 @@ Deno.test("Repository SSH connection test requires an explicit host-key confirma
|
||||
"selectedRuntimeId",
|
||||
"candidate.fingerprint",
|
||||
"Confirm and trust selected host key",
|
||||
"expected_host_trust_revision",
|
||||
"expected_host_key_fingerprint",
|
||||
"requestConnectionTest('POST'",
|
||||
"requestConnectionTest('PUT'",
|
||||
]
|
||||
|
||||
@@ -138,7 +138,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
|
||||
workspace_id: "team",
|
||||
runtime_id: worker.runtime_id,
|
||||
generated_at: "2026-01-01T00:00:00Z",
|
||||
revision: "7",
|
||||
|
||||
digest: "digest-7",
|
||||
workers: [{
|
||||
target_id: "worker-target",
|
||||
@@ -171,7 +171,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
|
||||
workspace_id: "team",
|
||||
runtime_id: worker.runtime_id,
|
||||
generated_at: "2026-01-01T00:00:01Z",
|
||||
revision: "8",
|
||||
|
||||
digest: "digest-8",
|
||||
workers: [],
|
||||
workdirs: [],
|
||||
@@ -189,7 +189,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
|
||||
]);
|
||||
assertEquals(requests[1]?.init?.method, "POST");
|
||||
assertEquals(JSON.parse(String(requests[1]?.init?.body)), {
|
||||
expected_plan_revision: "7",
|
||||
|
||||
expected_plan_digest: "digest-7",
|
||||
worker_target_ids: ["worker-target"],
|
||||
workdir_target_ids: [],
|
||||
@@ -204,7 +204,7 @@ Deno.test("sidebar Delete reports cleanup-plan blocking reasons", async () => {
|
||||
workspace_id: "team",
|
||||
runtime_id: worker.runtime_id,
|
||||
generated_at: "2026-01-01T00:00:00Z",
|
||||
revision: "8",
|
||||
|
||||
digest: "digest-8",
|
||||
workers: [{
|
||||
target_id: "worker-target",
|
||||
|
||||
@@ -16,7 +16,7 @@ Deno.test("ticket detail synchronizes reused route data", () => {
|
||||
const token of [
|
||||
"data.ticketId",
|
||||
"data.ticket.data",
|
||||
"incomingTicket.item_revision",
|
||||
"JSON.stringify([data.workspaceId, incomingTicketId, incomingTicket])",
|
||||
"routeGeneration += 1",
|
||||
"resetTicketView(incomingTicket)",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user