config: commit without preview
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
import {
|
||||
commitConfigTree,
|
||||
fetchConfigTree,
|
||||
previewConfigTree,
|
||||
} from "./api.ts";
|
||||
import { ConfigSourceToolchain } from "./toolchain.ts";
|
||||
import type {
|
||||
@@ -23,15 +22,15 @@
|
||||
let diagnostics = $state<ConfigDiagnostic[]>([]);
|
||||
let status = $state("Loading source tree…");
|
||||
let busy = $state(false);
|
||||
let draftChanges = $state<ConfigTreeChange[]>([]);
|
||||
let workingChanges = $state<ConfigTreeChange[]>([]);
|
||||
let baseRevision = $state(0);
|
||||
let baseDigest = $state("");
|
||||
let renamePath = $state("");
|
||||
let baseSnapshot = $state.raw<WorkspaceConfigTreeResponse["snapshot"] | null>(null);
|
||||
let preflightDigest = $state("");
|
||||
let conflict = $state(false);
|
||||
let candidateContract = $state<WorkspaceConfigTreeResponse["contract"] | null>(null);
|
||||
let toolchain: ConfigSourceToolchain | null = null;
|
||||
let toolchain = $state.raw<ConfigSourceToolchain | null>(null);
|
||||
let analysisReady = $state(false);
|
||||
let analysisGeneration = 0;
|
||||
|
||||
const paths = $derived(
|
||||
treeState ? Object.keys(treeState.snapshot.entries).toSorted() : [],
|
||||
@@ -40,8 +39,7 @@
|
||||
treeState && selectedPath ? treeState.snapshot.entries[selectedPath] : undefined,
|
||||
);
|
||||
const mainSelected = $derived(selectedPath === MAIN_ENTRYPOINT);
|
||||
const dirty = $derived(draftChanges.length > 0 || (selected ? source !== selected.content : source.length > 0));
|
||||
const commitReady = $derived(dirty && preflightDigest === treeState?.snapshot.digest);
|
||||
const dirty = $derived(workingChanges.length > 0 || (selected ? source !== selected.content : source.length > 0));
|
||||
|
||||
onMount(() => {
|
||||
toolchain = new ConfigSourceToolchain();
|
||||
@@ -49,7 +47,30 @@
|
||||
return () => toolchain?.close();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const analyzer = toolchain;
|
||||
const path = selectedPath;
|
||||
const value = source;
|
||||
const ready = analysisReady;
|
||||
const generation = ++analysisGeneration;
|
||||
diagnostics = [];
|
||||
if (!analyzer || !path || !ready) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
void analyzer.analyze(path, value).then((result) => {
|
||||
if (generation === analysisGeneration) diagnostics = result;
|
||||
}).catch((error) => {
|
||||
if (generation === analysisGeneration) status = `Analyze failed: ${String(error)}`;
|
||||
});
|
||||
}, 250);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
if (generation === analysisGeneration) analysisGeneration += 1;
|
||||
};
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
analysisReady = false;
|
||||
try {
|
||||
treeState = await fetchConfigTree(workspaceId);
|
||||
if (!selectedPath || !treeState.snapshot.entries[selectedPath]) {
|
||||
@@ -60,11 +81,11 @@
|
||||
baseRevision = treeState.snapshot.revision;
|
||||
baseDigest = treeState.snapshot.digest;
|
||||
await toolchain?.setSnapshot(treeState.snapshot, treeState.contract.schema_bundle);
|
||||
draftChanges = [];
|
||||
analysisReady = true;
|
||||
workingChanges = [];
|
||||
renamePath = selectedPath;
|
||||
diagnostics = [];
|
||||
conflict = false;
|
||||
candidateContract = null;
|
||||
status = `Revision ${treeState.snapshot.revision} · ${treeState.snapshot.digest.slice(0, 20)}…`;
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
@@ -76,9 +97,7 @@
|
||||
if (!change || !toolchain) return;
|
||||
const candidate = await toolchain.applyChanges([change]);
|
||||
if (treeState) treeState = { ...treeState, snapshot: candidate };
|
||||
if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
preflightDigest = "";
|
||||
candidateContract = null;
|
||||
if (baseSnapshot) workingChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
conflict = false;
|
||||
}
|
||||
|
||||
@@ -114,27 +133,21 @@
|
||||
return [MAIN_ENTRYPOINT];
|
||||
}
|
||||
|
||||
async function analyze() {
|
||||
if (!toolchain || !treeState || !selectedPath) return;
|
||||
diagnostics = await toolchain.analyze(selectedPath, source);
|
||||
status = diagnostics.length === 0 ? "No diagnostics." : `${diagnostics.length} diagnostic(s).`;
|
||||
}
|
||||
|
||||
async function format() {
|
||||
if (!toolchain) return;
|
||||
try {
|
||||
source = await toolchain.format(source);
|
||||
await analyze();
|
||||
status = "Formatted source. Changes remain local until Commit succeeds.";
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function formatDraftSources() {
|
||||
async function formatWorkingSources() {
|
||||
if (!toolchain || !treeState || !baseSnapshot) return;
|
||||
await stageCurrent();
|
||||
const paths = new Set<string>();
|
||||
for (const change of draftChanges) {
|
||||
for (const change of workingChanges) {
|
||||
if (change.kind === "create" || change.kind === "update") paths.add(change.path);
|
||||
if (change.kind === "rename") paths.add(change.to);
|
||||
}
|
||||
@@ -157,74 +170,33 @@
|
||||
if (!formattedAny) return;
|
||||
|
||||
treeState = { ...treeState, snapshot: candidate };
|
||||
draftChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
workingChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
source = candidate.entries[selectedPath]?.content ?? source;
|
||||
preflightDigest = "";
|
||||
candidateContract = null;
|
||||
conflict = false;
|
||||
}
|
||||
|
||||
async function requestCandidatePreview() {
|
||||
if (!toolchain) throw new Error("config source toolchain is unavailable");
|
||||
const candidate = await previewConfigTree(workspaceId, {
|
||||
changes: draftChanges,
|
||||
entrypoints: entrypoints(),
|
||||
});
|
||||
await toolchain.evaluate(candidate.contract);
|
||||
diagnostics = [];
|
||||
candidateContract = candidate.contract;
|
||||
preflightDigest = candidate.snapshot.digest;
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function recordCandidateError(error: unknown) {
|
||||
function recordCommitError(error: unknown) {
|
||||
const message = String(error);
|
||||
conflict = message.includes("conflict") || message.includes("base revision/digest mismatch");
|
||||
status = conflict ? `${message} Reload the authoritative tree before editing again.` : message;
|
||||
}
|
||||
|
||||
async function preview() {
|
||||
if (!treeState) return;
|
||||
busy = true;
|
||||
try {
|
||||
await formatDraftSources();
|
||||
if (draftChanges.length === 0) {
|
||||
status = "No draft changes to preview.";
|
||||
return;
|
||||
}
|
||||
const candidate = await requestCandidatePreview();
|
||||
status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`;
|
||||
} catch (error) {
|
||||
recordCandidateError(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function commit() {
|
||||
if (!treeState) return;
|
||||
busy = true;
|
||||
try {
|
||||
await formatDraftSources();
|
||||
if (draftChanges.length === 0) {
|
||||
status = "No draft changes to commit.";
|
||||
await formatWorkingSources();
|
||||
if (workingChanges.length === 0) {
|
||||
status = "No working changes to commit.";
|
||||
return;
|
||||
}
|
||||
if (preflightDigest !== treeState.snapshot.digest || !candidateContract) {
|
||||
await requestCandidatePreview();
|
||||
}
|
||||
const contract = candidateContract;
|
||||
if (!contract) throw new Error("candidate preview did not return a toolchain contract");
|
||||
treeState = await commitConfigTree(workspaceId, {
|
||||
base_revision: baseRevision,
|
||||
base_digest: baseDigest,
|
||||
changes: draftChanges,
|
||||
entrypoints: contract.entrypoints,
|
||||
toolchain_fingerprint: contract.fingerprint,
|
||||
changes: workingChanges,
|
||||
entrypoints: entrypoints(),
|
||||
});
|
||||
draftChanges = [];
|
||||
preflightDigest = "";
|
||||
candidateContract = null;
|
||||
workingChanges = [];
|
||||
conflict = false;
|
||||
baseSnapshot = $state.snapshot(treeState.snapshot);
|
||||
baseRevision = treeState.snapshot.revision;
|
||||
@@ -234,21 +206,21 @@
|
||||
diagnostics = [];
|
||||
status = `Committed formatted revision ${treeState.snapshot.revision}.`;
|
||||
} catch (error) {
|
||||
recordCandidateError(error);
|
||||
recordCommitError(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function discardAndReload() {
|
||||
draftChanges = [];
|
||||
workingChanges = [];
|
||||
source = "";
|
||||
await reload();
|
||||
}
|
||||
|
||||
async function reloadAndReapply() {
|
||||
if (!toolchain || !treeState) return;
|
||||
const localChanges = [...draftChanges];
|
||||
const localChanges = [...workingChanges];
|
||||
const remote = await fetchConfigTree(workspaceId);
|
||||
baseSnapshot = structuredClone(remote.snapshot);
|
||||
baseRevision = remote.snapshot.revision;
|
||||
@@ -256,14 +228,12 @@
|
||||
await toolchain.setSnapshot(remote.snapshot, remote.contract.schema_bundle);
|
||||
try {
|
||||
const candidate = await toolchain.applyChanges(localChanges);
|
||||
draftChanges = localChanges;
|
||||
workingChanges = localChanges;
|
||||
treeState = { ...remote, snapshot: candidate };
|
||||
selectedPath = candidate.entries[selectedPath] ? selectedPath : Object.keys(candidate.entries).toSorted()[0] ?? "";
|
||||
source = selectedPath ? candidate.entries[selectedPath].content : "";
|
||||
conflict = false;
|
||||
preflightDigest = "";
|
||||
candidateContract = null;
|
||||
status = "Local changes reapplied to the latest revision. Preview again before Commit.";
|
||||
status = "Local changes reapplied to the latest revision. Commit to persist them.";
|
||||
} catch (error) {
|
||||
conflict = true;
|
||||
status = `Local changes conflict with the latest revision: ${String(error)}. Discard local changes or resolve against a fresh reload.`;
|
||||
@@ -276,7 +246,7 @@
|
||||
selectedPath = path;
|
||||
source = "{}\n";
|
||||
diagnostics = [];
|
||||
status = `Drafting new source ${path}. It is not persisted until Commit succeeds.`;
|
||||
status = `Creating local source ${path}. It is not persisted until Commit succeeds.`;
|
||||
}
|
||||
|
||||
async function deleteEntry() {
|
||||
@@ -288,14 +258,12 @@
|
||||
};
|
||||
const candidate = await toolchain.applyChanges([change]);
|
||||
treeState = { ...treeState, snapshot: candidate };
|
||||
if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
preflightDigest = "";
|
||||
candidateContract = null;
|
||||
if (baseSnapshot) workingChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
conflict = false;
|
||||
selectedPath = Object.keys(candidate.entries).toSorted()[0] ?? "";
|
||||
source = selectedPath ? candidate.entries[selectedPath].content : "";
|
||||
renamePath = selectedPath;
|
||||
status = "Delete staged. Preview and Commit to persist the candidate tree.";
|
||||
status = "Delete staged locally. Commit to persist the working tree.";
|
||||
}
|
||||
|
||||
async function renameEntry() {
|
||||
@@ -311,13 +279,11 @@
|
||||
};
|
||||
const candidate = await toolchain.applyChanges([change]);
|
||||
treeState = { ...treeState, snapshot: candidate };
|
||||
if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
preflightDigest = "";
|
||||
candidateContract = null;
|
||||
if (baseSnapshot) workingChanges = await toolchain.changesBetween(baseSnapshot, candidate);
|
||||
conflict = false;
|
||||
selectedPath = to;
|
||||
source = candidate.entries[to]?.content ?? "";
|
||||
status = `Rename to ${to} staged. Preview and Commit to persist.`;
|
||||
status = `Rename to ${to} staged locally. Commit to persist it.`;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -342,7 +308,7 @@
|
||||
<form class="config-source-create" onsubmit={(event) => { event.preventDefault(); createEntry(); }}>
|
||||
<label for="new-config-path">New path</label>
|
||||
<input id="new-config-path" bind:value={newPath} placeholder="module.dcdl" />
|
||||
<button type="submit">Create draft</button>
|
||||
<button type="submit">Create local source</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
@@ -356,9 +322,7 @@
|
||||
<input aria-label="Rename path" bind:value={renamePath} disabled={!selected || mainSelected || busy} />
|
||||
<button type="button" onclick={renameEntry} disabled={!selected || mainSelected || renamePath === selectedPath || busy}>Rename</button>
|
||||
<button type="button" onclick={format} disabled={!selectedPath || busy}>Format</button>
|
||||
<button type="button" onclick={analyze} disabled={!selectedPath || busy}>Analyze</button>
|
||||
<button type="button" onclick={preview} disabled={!dirty || busy}>Preview</button>
|
||||
<button class="primary" type="button" onclick={commit} disabled={!commitReady || busy}>Commit</button>
|
||||
<button class="primary" type="button" onclick={commit} disabled={!dirty || busy}>Commit</button>
|
||||
<button class="danger" type="button" onclick={deleteEntry} disabled={!selected || mainSelected || busy}>Delete</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type {
|
||||
ConfigCommitRequest,
|
||||
ConfigEntry,
|
||||
ConfigPreviewRequest,
|
||||
ConfigTreeSnapshot,
|
||||
EvaluatedConfigCandidate,
|
||||
WorkspaceConfigTreeResponse,
|
||||
} from "./types.ts";
|
||||
|
||||
@@ -55,20 +53,6 @@ export async function fetchConfigRevision(
|
||||
);
|
||||
}
|
||||
|
||||
export async function previewConfigTree(
|
||||
workspaceId: string,
|
||||
request: ConfigPreviewRequest,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<EvaluatedConfigCandidate> {
|
||||
return await readJson(
|
||||
await fetcher(`${sourceTreeUrl(workspaceId)}/preview`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function commitConfigTree(
|
||||
workspaceId: string,
|
||||
request: ConfigCommitRequest,
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
import type { ConfigTreeChange } from "./ConfigTreeChange";
|
||||
import type { VirtualPath } from "./VirtualPath";
|
||||
|
||||
export type ConfigCommitRequest = { base_revision: number, base_digest: string, changes: Array<ConfigTreeChange>, entrypoints: Array<VirtualPath>, toolchain_fingerprint: string, };
|
||||
export type ConfigCommitRequest = { base_revision: number, base_digest: string, changes: Array<ConfigTreeChange>, entrypoints: Array<VirtualPath>, };
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConfigTreeChange } from "./ConfigTreeChange";
|
||||
import type { VirtualPath } from "./VirtualPath";
|
||||
|
||||
export type ConfigPreviewRequest = { changes: Array<ConfigTreeChange>, entrypoints: Array<VirtualPath>, };
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConfigTreeSnapshot } from "./ConfigTreeSnapshot";
|
||||
import type { EvaluationResult } from "./EvaluationResult";
|
||||
import type { ToolchainContract } from "./ToolchainContract";
|
||||
|
||||
export type EvaluatedConfigCandidate = { base_revision: number, base_digest: string, snapshot: ConfigTreeSnapshot, contract: ToolchainContract, evaluation: EvaluationResult, };
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
ConfigDiagnostic,
|
||||
ConfigTreeChange,
|
||||
ConfigTreeSnapshot,
|
||||
ToolchainContract,
|
||||
WorkspaceConfigSchemaBundle,
|
||||
} from "./types.ts";
|
||||
import { jsonWorkerMessage } from "./toolchain-message.ts";
|
||||
@@ -20,7 +19,6 @@ type Command =
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "apply_changes" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "changes_between" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "analyze" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "evaluate" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "complete" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "format" }>, "id">;
|
||||
|
||||
@@ -68,9 +66,6 @@ export class ConfigSourceToolchain {
|
||||
analyze(path: string, source?: string): Promise<ConfigDiagnostic[]> {
|
||||
return this.#request({ kind: "analyze", path, source });
|
||||
}
|
||||
evaluate(contract: ToolchainContract) {
|
||||
return this.#request({ kind: "evaluate", contract });
|
||||
}
|
||||
async complete(
|
||||
path: string,
|
||||
source: string,
|
||||
|
||||
@@ -3,7 +3,6 @@ import init, {
|
||||
apply_changes,
|
||||
changes_between,
|
||||
complete_current,
|
||||
evaluate_current,
|
||||
format_source,
|
||||
set_schema_bundle,
|
||||
set_snapshot,
|
||||
@@ -20,7 +19,6 @@ export type ConfigSourceWorkerRequest =
|
||||
| { id: number; kind: "apply_changes"; changes: ConfigTreeChange[] }
|
||||
| { id: number; kind: "changes_between"; base: unknown; candidate: unknown }
|
||||
| { id: number; kind: "analyze"; path: string; source?: string }
|
||||
| { id: number; kind: "evaluate"; contract: unknown }
|
||||
| {
|
||||
id: number;
|
||||
kind: "complete";
|
||||
@@ -65,9 +63,6 @@ self.onmessage = async (
|
||||
}
|
||||
result = analyze_snapshot(snapshot, request.path, request.source);
|
||||
break;
|
||||
case "evaluate":
|
||||
result = evaluate_current(request.contract);
|
||||
break;
|
||||
case "complete":
|
||||
result = complete_current(
|
||||
request.path,
|
||||
|
||||
@@ -13,6 +13,4 @@ export type { EvaluationResult } from "./generated/types/EvaluationResult.ts";
|
||||
export type { ToolchainContract } from "./generated/types/ToolchainContract.ts";
|
||||
export type { VirtualPath } from "./generated/types/VirtualPath.ts";
|
||||
export type { ConfigCommitRequest } from "./generated/types/ConfigCommitRequest.ts";
|
||||
export type { ConfigPreviewRequest } from "./generated/types/ConfigPreviewRequest.ts";
|
||||
export type { EvaluatedConfigCandidate } from "./generated/types/EvaluatedConfigCandidate.ts";
|
||||
export type { WorkspaceConfigState as WorkspaceConfigTreeResponse } from "./generated/types/WorkspaceConfigState.ts";
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
fetchConfigEntry,
|
||||
fetchConfigRevision,
|
||||
fetchConfigTree,
|
||||
previewConfigTree,
|
||||
} from "../../src/lib/workspace/config-source/api.ts";
|
||||
|
||||
function response(body: unknown, status = 200): Response {
|
||||
@@ -16,7 +15,7 @@ function response(body: unknown, status = 200): Response {
|
||||
});
|
||||
}
|
||||
|
||||
Deno.test("config source API stays workspace-scoped and separates preview from commit", async () => {
|
||||
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) => {
|
||||
calls.push({ url: String(input), init });
|
||||
@@ -26,37 +25,38 @@ Deno.test("config source API stays workspace-scoped and separates preview from c
|
||||
await fetchConfigTree("w/one", fetcher);
|
||||
await fetchConfigRevision("w/one", 7, fetcher);
|
||||
await fetchConfigEntry("w/one", "profiles/main.dcdl", fetcher);
|
||||
await previewConfigTree("w/one", { changes: [], entrypoints: [] }, fetcher);
|
||||
await commitConfigTree("w/one", {
|
||||
base_revision: 4,
|
||||
base_digest: "sha256:base",
|
||||
changes: [],
|
||||
entrypoints: [],
|
||||
toolchain_fingerprint: "sha256:toolchain",
|
||||
}, fetcher);
|
||||
|
||||
assertEquals(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/entries/profiles%2Fmain.dcdl",
|
||||
"/api/w/w%2Fone/config/source-tree/preview",
|
||||
"/api/w/w%2Fone/config/source-tree/commit",
|
||||
]);
|
||||
assertEquals(calls[3].init?.method, "POST");
|
||||
assertEquals(calls[4].init?.method, "POST");
|
||||
assert(
|
||||
String(calls[4].init?.body).includes('"base_digest":"sha256:base"'),
|
||||
String(calls[3].init?.body).includes('"base_digest":"sha256:base"'),
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config source API surfaces failed evaluation instead of treating it as a draft write", async () => {
|
||||
Deno.test("config source API surfaces failed evaluation instead of treating it as a successful commit", async () => {
|
||||
const fetcher = (() =>
|
||||
Promise.resolve(
|
||||
new Response("structured diagnostics", { status: 422 }),
|
||||
)) as typeof fetch;
|
||||
let message = "";
|
||||
try {
|
||||
await previewConfigTree("w", { changes: [], entrypoints: [] }, fetcher);
|
||||
await commitConfigTree("w", {
|
||||
base_revision: 1,
|
||||
base_digest: "sha256:base",
|
||||
changes: [],
|
||||
entrypoints: [],
|
||||
}, fetcher);
|
||||
} catch (error) {
|
||||
message = String(error);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ Deno.test("main entrypoint always enables the fixed schema wrapper", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("preview and commit format every draft Decodal source", async () => {
|
||||
Deno.test("commit formats every working Decodal source without a preview roundtrip", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../src/lib/workspace/config-source/ConfigSourceEditor.svelte",
|
||||
@@ -108,7 +108,7 @@ Deno.test("preview and commit format every draft Decodal source", async () => {
|
||||
);
|
||||
|
||||
assert(
|
||||
source.includes("async function formatDraftSources()") &&
|
||||
source.includes("async function formatWorkingSources()") &&
|
||||
source.includes('change.kind === "create" || change.kind === "update"') &&
|
||||
source.includes('change.kind === "rename"') &&
|
||||
source.includes('entry.content_type !== "decodal"') &&
|
||||
@@ -116,15 +116,39 @@ Deno.test("preview and commit format every draft Decodal source", async () => {
|
||||
"all changed Decodal entries should be formatted rather than only the selected source",
|
||||
);
|
||||
assert(
|
||||
source.includes("await formatDraftSources();") &&
|
||||
source.includes("await requestCandidatePreview();") &&
|
||||
source.includes("await formatWorkingSources();") &&
|
||||
source.includes("entrypoints: entrypoints()") &&
|
||||
source.includes("Committed formatted revision"),
|
||||
"preview and commit should format first and refresh stale preflight before persistence",
|
||||
"commit should format first and send the working changes directly to Backend authority",
|
||||
);
|
||||
assert(
|
||||
!source.includes(
|
||||
"Preview the complete candidate successfully before Commit.",
|
||||
!source.includes("previewConfigTree") &&
|
||||
!source.includes("requestCandidatePreview") &&
|
||||
!source.includes("toolchain_fingerprint"),
|
||||
"normal commit must not depend on a separate preview or client-echoed toolchain fingerprint",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config diagnostics analyze continuously with debounce and generation fencing", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../../src/lib/workspace/config-source/ConfigSourceEditor.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
"format-driven candidate changes should trigger automatic preflight instead of a dead-end error",
|
||||
);
|
||||
|
||||
assert(
|
||||
source.includes("setTimeout(() =>") &&
|
||||
source.includes("}, 250)") &&
|
||||
source.includes("analyzer.analyze(path, value)") &&
|
||||
source.includes("generation === analysisGeneration") &&
|
||||
source.includes("analysisReady"),
|
||||
"source changes should trigger only the latest debounced analysis after snapshot initialization",
|
||||
);
|
||||
assert(
|
||||
!source.includes("onclick={analyze}") &&
|
||||
!source.includes(">Analyze</button>") &&
|
||||
!source.includes(">Preview</button>"),
|
||||
"manual Analyze and Preview controls should be removed",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user