Files

338 lines
9.5 KiB
TypeScript

/// <reference lib="deno.ns" />
import {
assert,
assertEquals,
assertRejects,
assertThrows,
} from "jsr:@std/assert";
import {
commitConfigTree,
ConfigSourceApiError,
fetchConfigEntry,
fetchConfigHistory,
fetchConfigTree,
parseWorkspaceConfigTreeResponse,
} from "../../src/lib/workspace/config-source/api.ts";
function response(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
const entry = {
path: "profiles/main.dcdl",
content_type: "decodal",
content: "profile = {}",
content_digest: "sha256:entry",
};
const snapshot = {
digest: "sha256:tree",
entries: { "profiles/main.dcdl": entry },
};
const tree = {
snapshot,
contract: {
contract_version: 1,
decodal_version: "0.4.0",
schema_version: 1,
entrypoints: ["main.dcdl"],
import_policy_version: 1,
schema_bundle: {
contributions: [],
source: "builtin",
fingerprint: "sha256:schema",
},
fingerprint: "sha256:toolchain",
},
projection_digest: "sha256:projection",
};
Deno.test("config source API commits directly through the workspace scope", async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const fetcher = ((input: string | URL | Request, init?: RequestInit) => {
const url = String(input);
calls.push({ url, init });
if (url.includes("/entries/")) return Promise.resolve(response(entry));
if (url.includes("/history/")) return Promise.resolve(response(snapshot));
return Promise.resolve(response(tree));
}) as typeof fetch;
await fetchConfigTree("w/one", fetcher);
await fetchConfigHistory("w/one", "sha256:tree", fetcher);
await fetchConfigEntry("w/one", "profiles/main.dcdl", fetcher);
await commitConfigTree("w/one", {
base_digest: "sha256:base",
changes: [],
entrypoints: [],
}, fetcher);
assertEquals<unknown>(calls.map((call) => call.url), [
"/api/w/w%2Fone/config/source-tree",
"/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",
]);
assertEquals<unknown>(calls[3].init?.method, "POST");
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") {
return {
...tree,
contract: {
...tree.contract,
schema_bundle: {
...tree.contract.schema_bundle,
source,
contributions: [{
provider_id: "builtin",
namespace: "workspace",
version: "1",
source: contributionSource,
source_digest: "sha256:contribution",
}],
},
},
};
}
for (const field of ["bundle", "contribution"] as const) {
Deno.test(`config source API accepts ${field} schema bodies larger than metadata`, async () => {
const source = "schema body\n".repeat(6000);
const body = field === "bundle"
? withSchema(source)
: withSchema("builtin", source);
const fetcher = (() => Promise.resolve(response(body))) as typeof fetch;
assertEquals<unknown>(await fetchConfigTree("w", fetcher), body);
assertEquals<unknown>(
await commitConfigTree("w", {
base_digest: snapshot.digest,
changes: [],
entrypoints: ["main.dcdl"],
}, fetcher),
body,
);
});
Deno.test(`config source API bounds ${field} schema bodies in UTF-8 bytes and checks types`, () => {
const body = (source: unknown) =>
field === "bundle" ? withSchema(source) : withSchema("builtin", source);
const limit = 8 * 1024 * 1024;
const atLimit = "é".repeat(limit / 2);
assertEquals<unknown>(
parseWorkspaceConfigTreeResponse(body(atLimit)),
body(atLimit),
);
for (const invalid of [atLimit + "x", null, 42, {}]) {
assertThrows(
() => parseWorkspaceConfigTreeResponse(body(invalid)),
ConfigSourceApiError,
);
}
});
}
Deno.test("config source API retains metadata and total response limits", async () => {
const invalid = withSchema("builtin");
invalid.contract.schema_bundle.contributions[0].provider_id = "x".repeat(
4097,
);
assertThrows(
() => parseWorkspaceConfigTreeResponse(invalid),
ConfigSourceApiError,
);
for (
const headers of [
new Headers(),
new Headers({ "content-length": String(8 * 1024 * 1024 + 1) }),
]
) {
const fetcher = (() =>
Promise.resolve(
new Response(
JSON.stringify(withSchema("x".repeat(8 * 1024 * 1024))),
{ headers },
),
)) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"too large",
);
}
});
Deno.test("config source API rejects unknown response fields", async () => {
const fetcher =
(() =>
Promise.resolve(response({ ...tree, unexpected: true }))) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API rejects mismatched entry map paths", async () => {
const invalid = {
...tree,
snapshot: {
...snapshot,
entries: { "profiles/other.dcdl": entry },
},
};
const fetcher = (() => Promise.resolve(response(invalid))) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API rejects unknown snapshot fields and oversized entry content", () => {
assertThrows(
() =>
parseWorkspaceConfigTreeResponse({
...tree,
snapshot: { ...snapshot, unexpected_field: true },
}),
ConfigSourceApiError,
"invalid response",
);
assertThrows(
() =>
parseWorkspaceConfigTreeResponse({
...tree,
snapshot: {
...snapshot,
entries: {
"profiles/main.dcdl": {
...entry,
content: "x".repeat(256 * 1024 + 1),
},
},
},
}),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API does not materialize imported builtins in the workspace file list", async () => {
const body = {
...tree,
snapshot: {
...snapshot,
entries: {
"profiles/main.dcdl": {
...entry,
content: 'import "$builtin/profiles/companion.dcdl"',
},
},
},
};
const fetcher = (() => Promise.resolve(response(body))) as typeof fetch;
const result = await fetchConfigTree("w", fetcher);
assertEquals(Object.keys(result.snapshot.entries), ["profiles/main.dcdl"]);
assertEquals(
result.snapshot.entries["profiles/main.dcdl"].content,
body.snapshot.entries["profiles/main.dcdl"].content,
);
});
Deno.test("config source API preserves unknown read-only builtin diagnostics without fallback requests", async () => {
const diagnostic = {
path: "main.dcdl",
tree_digest: "sha256:tree",
kind: "import",
span: { start_byte: 8, end_byte: 40 },
message:
"unknown or non-public read-only builtin source: $builtin/profiles/missing.dcdl",
labels: [],
notes: [],
};
const calls: string[] = [];
const fetcher = ((input: string | URL | Request) => {
calls.push(String(input));
return Promise.resolve(
response({ error: JSON.stringify([diagnostic]) }, 400),
);
}) as typeof fetch;
const error = await assertRejects(
() =>
commitConfigTree("w", {
base_digest: "sha256:tree",
changes: [],
entrypoints: ["main.dcdl"],
}, fetcher),
ConfigSourceApiError,
diagnostic.message,
);
assert(String(error).includes("main.dcdl"));
assertEquals(calls, ["/api/w/w/config/source-tree/commit"]);
});
Deno.test("config source API surfaces bounded failed evaluation details", async () => {
const fetcher = (() =>
Promise.resolve(
new Response("structured diagnostics", { status: 422 }),
)) as typeof fetch;
let message = "";
try {
await commitConfigTree("w", {
base_digest: "sha256:base",
changes: [],
entrypoints: [],
}, fetcher);
} catch (error) {
message = String(error);
}
assert(message.includes("structured diagnostics"));
});
Deno.test("config source API carries bounded optional authoring schemas without changing validation source", () => {
const body = withSchema("validation");
const authoringSource = "shape\n".repeat(6000);
const contribution = {
...body.contract.schema_bundle.contributions[0],
authoring_source: authoringSource,
};
const withAuthoring = {
...body,
contract: {
...body.contract,
schema_bundle: {
...body.contract.schema_bundle,
contributions: [contribution],
},
},
};
const parsed = parseWorkspaceConfigTreeResponse(withAuthoring);
assertEquals(
parsed.contract.schema_bundle.contributions[0].authoring_source,
authoringSource,
);
assertEquals(parsed.contract.schema_bundle.source, "validation");
for (const invalid of [null, 42, {}, "é".repeat(4 * 1024 * 1024) + "x"]) {
assertThrows(() =>
parseWorkspaceConfigTreeResponse({
...withAuthoring,
contract: {
...body.contract,
schema_bundle: {
...body.contract.schema_bundle,
contributions: [{ ...contribution, authoring_source: invalid }],
},
},
}), ConfigSourceApiError);
}
});