diff --git a/crates/config-source/src/lib.rs b/crates/config-source/src/lib.rs index c4977fae..4a10f183 100644 --- a/crates/config-source/src/lib.rs +++ b/crates/config-source/src/lib.rs @@ -1360,6 +1360,82 @@ mod tests { ); } + #[test] + fn workspace_typed_associative_values_remain_closed_and_typed() { + let schema = || { + WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new( + "builtin:features", + "features", + "1", + "{ features = {...{ enabled = Bool; }}; }", + ) + .unwrap()]) + .unwrap() + }; + for (source, expected_kind) in [ + ( + "{ features = { web = { enabled = \"yes\"; }; }; }", + "constraintviolation", + ), + ("{ features = { web = {}; }; }", "materialize"), + ( + "{ features = { web = { enabled = true; typo = 1; }; }; }", + "constraintviolation", + ), + ] { + let snapshot = + ConfigTreeSnapshot::from_entries(1, [entry("main.dcdl", source)]).unwrap(); + let diagnostics = SnapshotEnvironment::new(snapshot) + .evaluate_contract(&ToolchainContract::with_schema_bundle( + 1, + vec![path("main.dcdl")], + 1, + schema(), + )) + .unwrap_err(); + assert_eq!(diagnostics[0].path, path("main.dcdl")); + assert_eq!(diagnostics[0].kind, expected_kind); + assert!(diagnostics[0].span.end_byte > diagnostics[0].span.start_byte); + } + } + + #[test] + fn language_service_and_formatter_accept_decodal_0_4_schema_syntax() { + let source = + "{} as { features = {...{ enabled = Bool; }}; web = { enabled = Bool; ...Unknown }; }"; + let snapshot = ConfigTreeSnapshot::from_entries(1, [entry("schema.dcdl", source)]).unwrap(); + let environment = SnapshotEnvironment::new(snapshot); + let diagnostics = environment.analyze(&path("schema.dcdl"), None); + assert!( + diagnostics + .iter() + .all(|diagnostic| diagnostic.kind != "syntax"), + "{diagnostics:#?}" + ); + + let completion_source = "{} as { web = { enabled = Unk } }"; + let completion = environment + .complete( + &path("schema.dcdl"), + completion_source, + completion_source.find("Unk").unwrap() + "Unk".len(), + true, + ) + .unwrap() + .expect("explicit completion is available"); + assert!(format!("{completion:?}").contains("Unknown")); + + let formatted = environment.format(source).unwrap(); + assert!(formatted.contains(" as ")); + assert!(formatted.contains("...Unknown")); + assert!( + environment + .analyze(&path("schema.dcdl"), Some(&formatted)) + .iter() + .all(|diagnostic| diagnostic.kind != "syntax") + ); + } + #[test] fn workspace_schema_preserves_fields_only_where_rest_is_explicit() { let snapshot = ConfigTreeSnapshot::from_entries( diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 7840ead7..c8e27717 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@7.2.7 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@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src --allow-env=VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts src/lib/workspace/tickets/ticket-panel.test.ts", + "test": "deno test --allow-read=src,test --allow-env=LOG,VSCODE_TEXTMATE_DEBUG src/lib/workspace/auth/model.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/sidebar/repository-nav.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/config-source/decodal-grammar.test.ts test/config-source/wasm-parity.test.ts", "build": "deno run -A npm:vite@7.2.7 build", "preview": "deno run -A npm:vite@7.2.7 preview" }, diff --git a/web/workspace/test/config-source/decodal-grammar.test.ts b/web/workspace/test/config-source/decodal-grammar.test.ts index 56f6a288..852be041 100644 --- a/web/workspace/test/config-source/decodal-grammar.test.ts +++ b/web/workspace/test/config-source/decodal-grammar.test.ts @@ -18,6 +18,8 @@ import "main.dcdl" as WorkspaceConfigSchema }, }); if (errors.length > 0) { - throw new Error(`Decodal 0.4 grammar produced parse errors at ${errors.join(", ")}`); + throw new Error( + `Decodal 0.4 grammar produced parse errors at ${errors.join(", ")}`, + ); } }); diff --git a/web/workspace/test/config-source/wasm-parity.test.ts b/web/workspace/test/config-source/wasm-parity.test.ts index 111d6d3e..34e5ea5c 100644 --- a/web/workspace/test/config-source/wasm-parity.test.ts +++ b/web/workspace/test/config-source/wasm-parity.test.ts @@ -13,14 +13,20 @@ import type { } from "../../src/lib/workspace/config-source/types.ts"; const bytes = await Deno.readFile( - new URL("../../src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm", import.meta.url), + new URL( + "../../src/lib/workspace/config-source/generated/config_source_wasm_bg.wasm", + import.meta.url, + ), ); await init({ module_or_path: bytes }); async function digestText(text: string): Promise { const bytes = new TextEncoder().encode(text); const digest = await crypto.subtle.digest("SHA-256", bytes); - return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`; + return `sha256:${ + Array.from(new Uint8Array(digest), (byte) => + byte.toString(16).padStart(2, "0")).join("") + }`; } async function toolchainFingerprint( @@ -56,7 +62,9 @@ const snapshot: ConfigTreeSnapshot = { }, }; -const emptySchemaBundle = compose_schema_bundle([]) as WorkspaceConfigSchemaBundle; +const emptySchemaBundle = compose_schema_bundle( + [], +) as WorkspaceConfigSchemaBundle; const contract: ToolchainContract = { contract_version: 2, decodal_version: "0.4.0", @@ -64,7 +72,10 @@ const contract: ToolchainContract = { entrypoints: ["workspace.dcdl"], import_policy_version: 1, schema_bundle: emptySchemaBundle, - fingerprint: await toolchainFingerprint(["workspace.dcdl"], emptySchemaBundle), + fingerprint: await toolchainFingerprint( + ["workspace.dcdl"], + emptySchemaBundle, + ), }; Deno.test("generated WASM evaluates the same virtual import contract", () => { @@ -148,15 +159,43 @@ Deno.test("generated WASM applies Decodal 0.4 typed maps and explicit object res }); }); -Deno.test("generated WASM rejects unknown root fields with source provenance", () => { - let thrown: unknown; +type ProjectedDiagnostic = { + path: string; + kind: string; + message: string; + span: { start_byte: number; end_byte: number }; +}; + +function evaluateFailure(source: string): ProjectedDiagnostic { try { - evaluate_snapshot(schemaSnapshot("{ features = {}; custom = 42; }"), schemaContract); + evaluate_snapshot(schemaSnapshot(source), schemaContract); } catch (error) { - thrown = error; + const diagnostics = error as ProjectedDiagnostic[]; + assertEquals(Array.isArray(diagnostics), true); + return diagnostics[0]; + } + throw new Error("expected Decodal evaluation to fail"); +} + +Deno.test("generated WASM preserves native Decodal 0.4 diagnostic semantics", () => { + for ( + const [source, expectedKind] of [ + ["{ features = {}; custom = 42; }", "constraintviolation"], + [ + '{ features = { web = { enabled = "yes"; }; }; }', + "constraintviolation", + ], + ["{ features = { web = {}; }; }", "materialize"], + [ + "{ features = { web = { enabled = true; typo = 1; }; }; }", + "constraintviolation", + ], + ] as const + ) { + const diagnostic = evaluateFailure(source); + assertEquals(diagnostic.path, "main.dcdl"); + assertEquals(diagnostic.kind, expectedKind); + assertEquals(diagnostic.message.length > 0, true); + assertEquals(diagnostic.span.end_byte > diagnostic.span.start_byte, true); } - const diagnostics = thrown as Array<{ path: string; kind: string }>; - assertEquals(Array.isArray(diagnostics), true); - assertEquals(diagnostics[0].path, "main.dcdl"); - assertEquals(diagnostics[0].kind, "constraintviolation"); });