chore: merge develop into hare/develop
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"dev": "deno run -A npm:vite@8.3.2 dev",
|
||||
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
|
||||
"check": "deno run -A npm:@sveltejs/kit@3.0.0 sync && deno run -A npm:svelte-check@4.7.6 --tsconfig ./tsconfig.json",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts",
|
||||
"test:component": "deno run -A npm:vitest@5.0.3 run --config vitest.config.ts",
|
||||
"build": "deno run -A npm:vite@8.3.2 build",
|
||||
"preview": "deno run -A npm:vite@8.3.2 preview",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da)
|
||||
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
@@ -237,6 +237,10 @@ export type ConfigSchemaContribution = {
|
||||
version: string;
|
||||
source: string;
|
||||
projection_validator?: ConfigProjectionValidator;
|
||||
/**
|
||||
* Completion-only shape for partial values; never materialized as defaults.
|
||||
*/
|
||||
authoring_source?: string;
|
||||
source_digest: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da)
|
||||
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da)
|
||||
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da)
|
||||
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:fd06f382c6d33a5c3cc98ef9e264545923c53fa95a9be555a85680f98c6065da)
|
||||
// Input contract: 0.1.0 (sha256:a6a7147c9a2f465a5da044e9ba21947cb3d86eacbfe3905696483917e794d5cf)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
@@ -216,6 +216,51 @@ test("unmount during schema setup closes the toolchain", async () => {
|
||||
expect(toolchain.close).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("builtin diagnostics show read-only paths without adding editable workspace files", async () => {
|
||||
toolchain.analyze.mockResolvedValue([{
|
||||
path: "$builtin/profiles/companion.dcdl",
|
||||
revision: 7,
|
||||
tree_digest: "sha256:tree",
|
||||
kind: "constraint_violation",
|
||||
span: { start_byte: 1, end_byte: 4 },
|
||||
message: "Imported value does not match the requested schema",
|
||||
labels: [],
|
||||
notes: [],
|
||||
}]);
|
||||
const view = renderEditor();
|
||||
await expectReady(view);
|
||||
await waitFor(() => {
|
||||
const diagnostic = view.container.querySelector(".config-source-diagnostics");
|
||||
expect(diagnostic?.textContent).toContain("$builtin/profiles/companion.dcdl (read-only builtin source)");
|
||||
expect(diagnostic?.textContent).toContain("bytes 1–4");
|
||||
});
|
||||
const paths = view.getByRole("navigation", { name: "Virtual configuration paths" });
|
||||
expect(paths.textContent).not.toContain("$builtin/");
|
||||
expect(paths.querySelectorAll("button")).toHaveLength(1);
|
||||
expect(view.container.querySelector('.cm-content[contenteditable="true"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test("unknown builtin diagnostics retain the workspace importer path", async () => {
|
||||
toolchain.analyze.mockResolvedValue([{
|
||||
path: "main.dcdl",
|
||||
revision: 7,
|
||||
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 view = renderEditor();
|
||||
await expectReady(view);
|
||||
await waitFor(() => {
|
||||
const diagnostic = view.container.querySelector(".config-source-diagnostics");
|
||||
expect(diagnostic?.textContent).toContain("main.dcdl · bytes 8–40");
|
||||
expect(diagnostic?.textContent).toContain("unknown or non-public read-only builtin source");
|
||||
});
|
||||
expect(view.getByRole("navigation", { name: "Virtual configuration paths" }).textContent).not.toContain("$builtin/");
|
||||
});
|
||||
|
||||
test("unmount during fetch does not start a toolchain", async () => {
|
||||
let finishFetch!: (response: Response) => void;
|
||||
fetcher.mockReturnValueOnce(
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
<li>
|
||||
<strong>{diagnostic.kind}</strong>
|
||||
<span>{diagnostic.message}</span>
|
||||
<small>bytes {diagnostic.span.start_byte}–{diagnostic.span.end_byte}</small>
|
||||
<small>{diagnostic.path}{diagnostic.path.startsWith("$builtin/") ? " (read-only builtin source)" : ""} · bytes {diagnostic.span.start_byte}–{diagnostic.span.end_byte}</small>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
@@ -187,7 +187,7 @@ function parseContribution(value: unknown): ConfigSchemaContribution {
|
||||
const item = record(
|
||||
value,
|
||||
["provider_id", "namespace", "version", "source", "source_digest"],
|
||||
["projection_validator"],
|
||||
["projection_validator", "authoring_source"],
|
||||
);
|
||||
const projection = item.projection_validator === undefined
|
||||
? undefined
|
||||
@@ -197,6 +197,12 @@ function parseContribution(value: unknown): ConfigSchemaContribution {
|
||||
namespace: boundedString(item.namespace),
|
||||
version: boundedString(item.version),
|
||||
source: boundedString(item.source, MAX_SCHEMA_SOURCE_BYTES),
|
||||
...(item.authoring_source === undefined ? {} : {
|
||||
authoring_source: boundedString(
|
||||
item.authoring_source,
|
||||
MAX_SCHEMA_SOURCE_BYTES,
|
||||
),
|
||||
}),
|
||||
...(projection === undefined ? {} : { projection_validator: projection }),
|
||||
source_digest: boundedString(item.source_digest, MAX_DIGEST_BYTES),
|
||||
};
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -1,4 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConfigProjectionValidator } from "./ConfigProjectionValidator";
|
||||
|
||||
export type ConfigSchemaContribution = { provider_id: string, namespace: string, version: string, source: string, projection_validator?: ConfigProjectionValidator | null, source_digest: string, };
|
||||
export type ConfigSchemaContribution = { provider_id: string, namespace: string, version: string, source: string, authoring_source?: string | null, projection_validator?: ConfigProjectionValidator | null, source_digest: string, };
|
||||
|
||||
@@ -226,6 +226,62 @@ Deno.test("config source API rejects unsafe revisions and oversized entry conten
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
revision: 7,
|
||||
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_revision: 7,
|
||||
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(
|
||||
@@ -244,3 +300,41 @@ Deno.test("config source API surfaces bounded failed evaluation details", async
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,6 +89,31 @@ Deno.test("completion starts only after non-whitespace typing", () => {
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("toolchain preserves read-only builtin import completion detail", () => {
|
||||
const result = toCodeMirrorCompletion({
|
||||
from: 8,
|
||||
items: [{
|
||||
label: "$builtin/profiles/companion.dcdl",
|
||||
kind: "file",
|
||||
detail: "read-only builtin Decodal source",
|
||||
priority: 0,
|
||||
}],
|
||||
});
|
||||
assert(result !== null, "builtin completion should be available");
|
||||
assert(
|
||||
result.options[0].label === "$builtin/profiles/companion.dcdl",
|
||||
"builtin namespace must not become a workspace path",
|
||||
);
|
||||
assert(
|
||||
result.options[0].detail === "read-only builtin Decodal source",
|
||||
"CodeMirror must display the read-only completion detail",
|
||||
);
|
||||
assert(
|
||||
result.options[0].type === "file",
|
||||
"builtin imports remain file completions",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("toolchain preserves WASM UTF-16 completion ranges for CodeMirror", () => {
|
||||
const result = toCodeMirrorCompletion({
|
||||
from: "let 名 = ".length,
|
||||
|
||||
@@ -299,6 +299,150 @@ Deno.test("generated WASM returns completion items for the editor adapter", () =
|
||||
assertEquals(result.items[0].kind, "file");
|
||||
});
|
||||
|
||||
Deno.test("generated WASM exposes read-only builtin completions without editable builtin entries", () => {
|
||||
const tree = schemaSnapshot("{}");
|
||||
set_snapshot(tree);
|
||||
set_schema_bundle(emptySchemaBundle);
|
||||
const source = 'let 名 = 1; import "$builtin/profiles/comp"';
|
||||
const result = complete_current(
|
||||
"main.dcdl",
|
||||
source,
|
||||
source.length - 1,
|
||||
true,
|
||||
) as {
|
||||
from: number;
|
||||
items: Array<{ label: string; kind: string; detail: string }>;
|
||||
};
|
||||
const companion = result.items.find((item) =>
|
||||
item.label === "$builtin/profiles/companion.dcdl"
|
||||
);
|
||||
assertEquals(result.from, 'let 名 = 1; import "'.length);
|
||||
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"'),
|
||||
mainEntrypointContract,
|
||||
) as {
|
||||
projections: Array<{ data_json: { slug: string } }>;
|
||||
};
|
||||
assertEquals(evaluated.projections[0].data_json.slug, "companion");
|
||||
});
|
||||
|
||||
Deno.test("generated WASM rejects unknown builtin imports without same-suffix workspace fallback", () => {
|
||||
const tree = schemaSnapshot('import "$builtin/profiles/missing.dcdl"');
|
||||
tree.entries["profiles/missing.dcdl"] = {
|
||||
path: "profiles/missing.dcdl",
|
||||
content_type: "decodal",
|
||||
content: "{}",
|
||||
content_digest: "sha256:workspace-fallback",
|
||||
};
|
||||
set_schema_bundle(emptySchemaBundle);
|
||||
const diagnostics = analyze_snapshot(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].message.includes(
|
||||
"unknown or non-public read-only builtin source",
|
||||
),
|
||||
true,
|
||||
);
|
||||
assertEquals(
|
||||
diagnostics[0].message.includes("$builtin/profiles/missing.dcdl"),
|
||||
true,
|
||||
);
|
||||
let failed = false;
|
||||
try {
|
||||
evaluate_snapshot(tree, mainEntrypointContract);
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
assertEquals(
|
||||
(error as Array<{ message: string }>)[0].message.includes(
|
||||
"unknown or non-public read-only builtin source",
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
assertEquals(failed, true);
|
||||
assertEquals(
|
||||
analyze_snapshot(tree, "main.dcdl", 'import "./profiles/missing.dcdl"'),
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
function importFailure(tree: ConfigTreeSnapshot): ProjectedDiagnostic[] {
|
||||
try {
|
||||
evaluate_snapshot(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);
|
||||
return diagnostics;
|
||||
}
|
||||
throw new Error("expected a structured import diagnostic");
|
||||
}
|
||||
|
||||
Deno.test("generated WASM rejects nested lazy-object import cycles without a stack overflow", () => {
|
||||
const tree = schemaSnapshot('{ nested = import "./b.dcdl"; }');
|
||||
tree.entries["b.dcdl"] = {
|
||||
path: "b.dcdl",
|
||||
content_type: "decodal",
|
||||
content: '{ nested = import "./main.dcdl"; }',
|
||||
content_digest: "sha256:nested-cycle-b",
|
||||
};
|
||||
const diagnostics = importFailure(tree);
|
||||
assertEquals(diagnostics[0].kind, "cycle");
|
||||
assertEquals(diagnostics[0].message.includes("import cycle"), true);
|
||||
});
|
||||
|
||||
Deno.test("generated WASM enforces the import depth budget on a real 33-deep chain", () => {
|
||||
function chain(depth: number): ConfigTreeSnapshot {
|
||||
const tree = schemaSnapshot('import "./chain/1.dcdl"');
|
||||
for (let index = 1; index <= depth; index++) {
|
||||
const path = `chain/${index}.dcdl`;
|
||||
tree.entries[path] = {
|
||||
path,
|
||||
content_type: "decodal",
|
||||
content: index === depth
|
||||
? "{ answer = 42; }"
|
||||
: `import "./${index + 1}.dcdl"`,
|
||||
content_digest: `sha256:chain-${depth}-${index}`,
|
||||
};
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
// The synthetic evaluation-wrapper import consumes one of the 32 edges.
|
||||
const accepted = evaluate_snapshot(chain(31), mainEntrypointContract) as {
|
||||
projections: Array<{ data_json: { answer: number } }>;
|
||||
};
|
||||
assertEquals(accepted.projections[0].data_json, { answer: 42 });
|
||||
for (const depth of [32, 33]) {
|
||||
const diagnostics = importFailure(chain(depth));
|
||||
assertEquals(diagnostics[0].kind, "import");
|
||||
assertEquals(
|
||||
diagnostics.some((diagnostic) =>
|
||||
diagnostic.message.includes("import depth")
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("generated WASM completes blank nested schema positions after Unicode", () => {
|
||||
const source =
|
||||
'{ description = "日本語"; profile = { }\n} as WorkspaceConfigSchema';
|
||||
@@ -381,3 +525,376 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () =>
|
||||
assertEquals(result.from, 2);
|
||||
assertEquals(result.items.some((item) => item.label === "profile"), true);
|
||||
});
|
||||
|
||||
function fixtureDecodal(value: unknown): string {
|
||||
if (Array.isArray(value)) return `[${value.map(fixtureDecodal).join(", ")}]`;
|
||||
if (value !== null && typeof value === "object") {
|
||||
return `{ ${
|
||||
Object.entries(value).map(([key, value]) =>
|
||||
`${key} = ${fixtureDecodal(value)};`
|
||||
).join(" ")
|
||||
} }`;
|
||||
}
|
||||
if (
|
||||
typeof value === "string" || typeof value === "number" ||
|
||||
typeof value === "boolean"
|
||||
) return JSON.stringify(value);
|
||||
throw new Error("fixture omission must not be represented as null/undefined");
|
||||
}
|
||||
|
||||
Deno.test("generated WASM authors value-based Profiles using the Backend schema without materializing authoring fields", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../../../resources/config-schema/profile.dcdl",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const authoring_source = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../../../resources/config-schema/profile-authoring.dcdl",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const schema = compose_schema_bundle([{
|
||||
provider_id: "builtin:profile",
|
||||
namespace: "profile",
|
||||
version: "2",
|
||||
source,
|
||||
source_digest: await digestText(source),
|
||||
authoring_source,
|
||||
}]) as WorkspaceConfigSchemaBundle;
|
||||
const content =
|
||||
'{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }]; }; } as WorkspaceConfigSchema';
|
||||
const tree: ConfigTreeSnapshot = {
|
||||
revision: 8,
|
||||
digest: "sha256:value-profile-tree",
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
path: "main.dcdl",
|
||||
content_type: "decodal",
|
||||
content,
|
||||
content_digest: await digestText(content),
|
||||
},
|
||||
},
|
||||
};
|
||||
const valueContract = {
|
||||
...contract,
|
||||
entrypoints: ["main.dcdl"],
|
||||
schema_bundle: schema,
|
||||
fingerprint: await toolchainFingerprint(["main.dcdl"], schema),
|
||||
};
|
||||
set_snapshot(tree);
|
||||
set_schema_bundle(schema);
|
||||
const evaluated = evaluate_snapshot(tree, valueContract) as {
|
||||
projections: Array<
|
||||
{ data_json: { profile: { entries: Array<{ profile: unknown }> } } }
|
||||
>;
|
||||
};
|
||||
assertEquals(
|
||||
evaluated.projections[0].data_json.profile.entries[0].profile,
|
||||
{},
|
||||
);
|
||||
for (
|
||||
const [body, token, label] of [
|
||||
[
|
||||
"{ profile = { entries = [{ sel }] } } as WorkspaceConfigSchema",
|
||||
"sel",
|
||||
"selector",
|
||||
],
|
||||
[
|
||||
"{ profile = { entries = [{ pro }] } } as WorkspaceConfigSchema",
|
||||
"pro",
|
||||
"profile",
|
||||
],
|
||||
[
|
||||
"{ profile = { entries = [{ profile = { wor } }] } } as WorkspaceConfigSchema",
|
||||
"wor",
|
||||
"worker",
|
||||
],
|
||||
[
|
||||
"{ profile = { entries = [{ profile = { worker = { mo } } }] } } as WorkspaceConfigSchema",
|
||||
"mo",
|
||||
"mode",
|
||||
],
|
||||
[
|
||||
"{ profile = { entries = [{ profile = { feature = { ta } } }] } } as WorkspaceConfigSchema",
|
||||
"ta",
|
||||
"task",
|
||||
],
|
||||
]
|
||||
) {
|
||||
const cursor = body.lastIndexOf(token) + token.length;
|
||||
const result = complete_current("main.dcdl", body, cursor, true) as {
|
||||
from: number;
|
||||
items: Array<{ label: string }>;
|
||||
};
|
||||
assertEquals(
|
||||
result.items.some((item) => item.label === label),
|
||||
true,
|
||||
label,
|
||||
);
|
||||
assertEquals(result.from, cursor - token.length);
|
||||
}
|
||||
for (
|
||||
const value of [
|
||||
"{}",
|
||||
"{ worker = {}; }",
|
||||
'{ worker = { mode = "wip"; }; }',
|
||||
"{ feature = { task = {}; }; }",
|
||||
]
|
||||
) {
|
||||
const partialContent = content.replace(
|
||||
"profile = {};",
|
||||
`profile = ${value};`,
|
||||
);
|
||||
assertEquals(
|
||||
analyze_snapshot(tree, "main.dcdl", partialContent),
|
||||
[],
|
||||
value,
|
||||
);
|
||||
}
|
||||
const recipe = "{ worker = { mode = 42; }; }";
|
||||
const analysisTree = {
|
||||
...tree,
|
||||
entries: {
|
||||
...tree.entries,
|
||||
"recipe.dcdl": {
|
||||
path: "recipe.dcdl",
|
||||
content_type: "decodal",
|
||||
content: recipe,
|
||||
content_digest: await digestText(recipe),
|
||||
},
|
||||
},
|
||||
};
|
||||
for (
|
||||
const [value, field, expectedPath] of [
|
||||
["42", "profile", "main.dcdl"],
|
||||
["{ worker = 42; }", "worker", "main.dcdl"],
|
||||
["{ worker = { mode = 42; }; }", "mode", "main.dcdl"],
|
||||
["{ feature = { task = { enabled = 42; }; }; }", "enabled", "main.dcdl"],
|
||||
['{ worker = { typo = "wip"; }; }', "typo", "main.dcdl"],
|
||||
['import "./recipe.dcdl"', "mode", "recipe.dcdl"],
|
||||
[
|
||||
'(import "./recipe.dcdl") // { worker = { mode = 42; }; }',
|
||||
"mode",
|
||||
"main.dcdl",
|
||||
],
|
||||
]
|
||||
) {
|
||||
const invalidContent = content.replace(
|
||||
"profile = {};",
|
||||
`profile = ${value};`,
|
||||
);
|
||||
const diagnostics = analyze_snapshot(
|
||||
analysisTree,
|
||||
"main.dcdl",
|
||||
invalidContent,
|
||||
) as Array<{
|
||||
path: string;
|
||||
revision: number;
|
||||
kind: string;
|
||||
span: { start_byte: number; end_byte: number };
|
||||
message: string;
|
||||
labels: Array<
|
||||
{ span: { start_byte: number; end_byte: number }; message: string }
|
||||
>;
|
||||
}>;
|
||||
assertEquals(diagnostics.length > 0, true, value);
|
||||
const diagnostic = diagnostics[0];
|
||||
assertEquals(diagnostic.path, expectedPath);
|
||||
assertEquals(diagnostic.revision, tree.revision);
|
||||
assertEquals(
|
||||
["constraint_violation", "type_mismatch"].includes(diagnostic.kind),
|
||||
true,
|
||||
);
|
||||
assertEquals(
|
||||
diagnostic.message.includes(field) ||
|
||||
diagnostic.labels.some((label) => label.message.includes(field)),
|
||||
true,
|
||||
);
|
||||
// New authoring failures must be anchored in the supplied value, not the
|
||||
// much longer schema source, including relative imported recipes.
|
||||
if (value !== "42") {
|
||||
const length = new TextEncoder().encode(
|
||||
expectedPath === "main.dcdl" ? invalidContent : recipe,
|
||||
).length;
|
||||
assertEquals(
|
||||
diagnostic.span.end_byte > diagnostic.span.start_byte &&
|
||||
diagnostic.span.end_byte <= length,
|
||||
true,
|
||||
);
|
||||
assertEquals(
|
||||
diagnostic.labels.every((label) => label.span.end_byte <= length),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const field of ["scope", "delegation_scope"]) {
|
||||
for (
|
||||
const [prefix, label] of [["int", "intent"], ["deny", "deny_write"], [
|
||||
"sym",
|
||||
"symlink_policy",
|
||||
]]
|
||||
) {
|
||||
const body =
|
||||
`{ profile.entries = [{ profile.${field} = { ${prefix} } }]; } as WorkspaceConfigSchema`;
|
||||
const cursor = body.lastIndexOf(prefix) + prefix.length;
|
||||
const result = complete_current("main.dcdl", body, cursor, true) as {
|
||||
items: Array<{ label: string }>;
|
||||
};
|
||||
assertEquals(
|
||||
result.items.some((item) => item.label === label),
|
||||
true,
|
||||
`${field}.${label}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (
|
||||
const [recipe, expected] of [
|
||||
['{ scope = "workspace_read"; delegation_scope = "workspace_write"; }', {
|
||||
scope: "workspace_read",
|
||||
delegation_scope: "workspace_write",
|
||||
}],
|
||||
['{ scope = "workspace_write"; delegation_scope = "workspace_read"; }', {
|
||||
scope: "workspace_write",
|
||||
delegation_scope: "workspace_read",
|
||||
}],
|
||||
[
|
||||
'{ scope = { intent = "workspace_read"; }; delegation_scope = { intent = "workspace_write"; deny_write = ["private"]; symlink_policy = "resolved"; }; }',
|
||||
{
|
||||
scope: { intent: "workspace_read" },
|
||||
delegation_scope: {
|
||||
intent: "workspace_write",
|
||||
deny_write: ["private"],
|
||||
symlink_policy: "resolved",
|
||||
},
|
||||
},
|
||||
],
|
||||
] as const
|
||||
) {
|
||||
for (
|
||||
const form of [
|
||||
recipe,
|
||||
'import "./scoped.dcdl"',
|
||||
'(import "./scoped.dcdl") // { description = "patched"; }',
|
||||
]
|
||||
) {
|
||||
const scopedContent = content.replace(
|
||||
"profile = {};",
|
||||
`profile = ${form};`,
|
||||
);
|
||||
const scopedTree = {
|
||||
...tree,
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
...tree.entries["main.dcdl"],
|
||||
content: scopedContent,
|
||||
content_digest: await digestText(scopedContent),
|
||||
},
|
||||
"scoped.dcdl": {
|
||||
path: "scoped.dcdl",
|
||||
content_type: "decodal",
|
||||
content: recipe,
|
||||
content_digest: await digestText(recipe),
|
||||
},
|
||||
},
|
||||
};
|
||||
assertEquals(
|
||||
analyze_snapshot(scopedTree, "main.dcdl", undefined),
|
||||
[],
|
||||
form,
|
||||
);
|
||||
const result = evaluate_snapshot(
|
||||
scopedTree,
|
||||
valueContract,
|
||||
) as typeof evaluated;
|
||||
assertEquals(
|
||||
result.projections[0].data_json.profile.entries[0].profile,
|
||||
form.includes("patched")
|
||||
? { ...expected, description: "patched" }
|
||||
: expected,
|
||||
);
|
||||
}
|
||||
}
|
||||
const cases = JSON.parse(
|
||||
await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../../../resources/config-schema/profile-authoring-values.json",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
) as Array<{ name: string; profile: Record<string, unknown> }>;
|
||||
for (const fixture of cases) {
|
||||
const recipe = fixtureDecodal(fixture.profile);
|
||||
for (
|
||||
const [form, patched] of [[recipe, false], [
|
||||
'import "./recipe.dcdl"',
|
||||
false,
|
||||
], [
|
||||
'(import "./recipe.dcdl") // { description = "patched"; }',
|
||||
true,
|
||||
]] as const
|
||||
) {
|
||||
const fixtureContent = content.replace(
|
||||
"profile = {};",
|
||||
`profile = ${form};`,
|
||||
);
|
||||
const fixtureTree = {
|
||||
...tree,
|
||||
entries: {
|
||||
"main.dcdl": {
|
||||
...tree.entries["main.dcdl"],
|
||||
content: fixtureContent,
|
||||
content_digest: await digestText(fixtureContent),
|
||||
},
|
||||
"recipe.dcdl": {
|
||||
path: "recipe.dcdl",
|
||||
content_type: "decodal",
|
||||
content: recipe,
|
||||
content_digest: await digestText(recipe),
|
||||
},
|
||||
},
|
||||
};
|
||||
assertEquals(
|
||||
analyze_snapshot(fixtureTree, "main.dcdl", undefined),
|
||||
[],
|
||||
`${fixture.name}: ${form}`,
|
||||
);
|
||||
const result = evaluate_snapshot(
|
||||
fixtureTree,
|
||||
valueContract,
|
||||
) as typeof evaluated;
|
||||
assertEquals(
|
||||
result.projections[0].data_json.profile.entries[0].profile,
|
||||
patched
|
||||
? { ...fixture.profile, description: "patched" }
|
||||
: fixture.profile,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (
|
||||
const [path, prefix, label] of [
|
||||
["compaction", "reta", "retained_tokens"],
|
||||
["compaction", "prune_min", "prune_min_savings"],
|
||||
["compaction", "req", "request"],
|
||||
["compaction", "wor", "worker"],
|
||||
["compaction", "compact_ret", "compact_retained_tokens"],
|
||||
["feature.memory.extraction", "reas", "reasoning"],
|
||||
["feature.subjektiv.extraction", "reas", "reasoning"],
|
||||
]
|
||||
) {
|
||||
const body =
|
||||
`{ profile.entries = [{ profile.${path} = { ${prefix} } }]; } as WorkspaceConfigSchema`;
|
||||
const cursor = body.lastIndexOf(prefix) + prefix.length;
|
||||
const result = complete_current("main.dcdl", body, cursor, true) as {
|
||||
items: Array<{ label: string }>;
|
||||
};
|
||||
assertEquals(
|
||||
result.items.some((item) => item.label === label),
|
||||
true,
|
||||
`${path}.${label}`,
|
||||
);
|
||||
}
|
||||
// Neither completion nor diagnostic shape checking changes the saved value.
|
||||
assertEquals(evaluate_snapshot(tree, valueContract), evaluated);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user