fix: stabilize authoring graph selection
This commit is contained in:
@@ -43,6 +43,10 @@ describe("AuthoringGraph", () => {
|
||||
"data-active",
|
||||
"true",
|
||||
);
|
||||
expect(container.querySelector('[data-node-id="review"]')).toHaveAttribute(
|
||||
"aria-pressed",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
it("selects a connector by its source step and outcome", () => {
|
||||
@@ -51,7 +55,7 @@ describe("AuthoringGraph", () => {
|
||||
<AuthoringGraph draft={workspace.draft} selection={{ kind: "canvas" }} onSelectionChange={onSelectionChange} />,
|
||||
);
|
||||
|
||||
const edgeButton = container.querySelector('[data-edge-id="e-collect-review-0"]');
|
||||
const edgeButton = container.querySelector('[data-edge-id="e-7:collect2:ok6:review"]');
|
||||
expect(edgeButton).not.toBeNull();
|
||||
fireEvent.click(edgeButton!);
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { ReactNode } from "react";
|
||||
import type { DraftDiagnostic, DraftWorkspace } from "../domain/draft-workspace-models.js";
|
||||
import type { CapabilitySummary } from "../domain/capability-models.js";
|
||||
import { projectAuthoringGraph, type WorkbenchSelection } from "./authoring-graph.js";
|
||||
|
||||
const MAX_RAW_DRAFT_CHARS = 12_000;
|
||||
const TRUNCATION_MARKER = "... truncated ...";
|
||||
import { withDiagnosticKeys } from "./diagnostic-key.js";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
|
||||
type ContextInspectorProps = {
|
||||
readonly draft: DraftWorkspace;
|
||||
@@ -12,9 +11,6 @@ type ContextInspectorProps = {
|
||||
readonly selection: WorkbenchSelection;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const formatStatus = (status: DraftWorkspace["status"]): string =>
|
||||
status.charAt(0).toUpperCase() + status.slice(1);
|
||||
|
||||
@@ -24,100 +20,6 @@ const formatValue = (value: unknown): string => {
|
||||
return encoded ?? String(value);
|
||||
};
|
||||
|
||||
export const formatBoundedJson = (
|
||||
value: unknown,
|
||||
maxChars = MAX_RAW_DRAFT_CHARS,
|
||||
): string => {
|
||||
const truncationMarker = TRUNCATION_MARKER.slice(0, Math.max(0, maxChars));
|
||||
const contentLimit = Math.max(0, maxChars);
|
||||
let output = "";
|
||||
let truncated = false;
|
||||
const activeObjects = new WeakSet<object>();
|
||||
|
||||
const append = (chunk: string): void => {
|
||||
if (truncated) return;
|
||||
if (output.length + chunk.length > contentLimit) {
|
||||
output += chunk.slice(0, Math.max(0, contentLimit - output.length));
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
output += chunk;
|
||||
};
|
||||
|
||||
const appendJsonString = (text: string): void => {
|
||||
append('"');
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (truncated) return;
|
||||
const code = text.charCodeAt(index);
|
||||
if (code === 0x22) append('\\"');
|
||||
else if (code === 0x5c) append("\\\\");
|
||||
else if (code < 0x20) append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
else if (code >= 0xd800 && code <= 0xdbff) {
|
||||
const nextCode = text.charCodeAt(index + 1);
|
||||
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
||||
append(text.slice(index, index + 2));
|
||||
index++;
|
||||
} else append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
||||
append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
} else append(text.charAt(index));
|
||||
}
|
||||
if (!truncated) append('"');
|
||||
};
|
||||
|
||||
const visit = (current: unknown, depth: number): void => {
|
||||
if (truncated) return;
|
||||
if (current === null || typeof current !== "object") {
|
||||
if (typeof current === "string") appendJsonString(current);
|
||||
else if (typeof current === "number") append(Number.isFinite(current) ? String(current) : "null");
|
||||
else if (typeof current === "boolean") append(current ? "true" : "false");
|
||||
else append("null");
|
||||
return;
|
||||
}
|
||||
if (activeObjects.has(current)) {
|
||||
append('"[Circular]"');
|
||||
return;
|
||||
}
|
||||
activeObjects.add(current);
|
||||
const indent = " ".repeat(depth);
|
||||
const childIndent = " ".repeat(depth + 1);
|
||||
if (Array.isArray(current)) {
|
||||
append("[");
|
||||
let first = true;
|
||||
for (const item of current) {
|
||||
if (truncated) break;
|
||||
append(first ? `\n${childIndent}` : `,\n${childIndent}`);
|
||||
visit(item, depth + 1);
|
||||
first = false;
|
||||
}
|
||||
if (!truncated) append(first ? "]" : `\n${indent}]`);
|
||||
} else {
|
||||
if (!isRecord(current)) {
|
||||
activeObjects.delete(current);
|
||||
return;
|
||||
}
|
||||
const record = current;
|
||||
append("{");
|
||||
let first = true;
|
||||
for (const key in record) {
|
||||
if (!Object.prototype.hasOwnProperty.call(record, key) || truncated) continue;
|
||||
append(first ? `\n${childIndent}` : `,\n${childIndent}`);
|
||||
appendJsonString(key);
|
||||
append(": ");
|
||||
visit(record[key], depth + 1);
|
||||
first = false;
|
||||
}
|
||||
if (!truncated) append(first ? "}" : `\n${indent}}`);
|
||||
}
|
||||
activeObjects.delete(current);
|
||||
};
|
||||
|
||||
visit(value, 0);
|
||||
if (!truncated) return output;
|
||||
const markerStart = Math.max(0, contentLimit - truncationMarker.length);
|
||||
return `${output.slice(0, markerStart)}${truncationMarker}`;
|
||||
};
|
||||
|
||||
const Fact = ({ label, value }: { readonly label: string; readonly value: string }) => (
|
||||
<div>
|
||||
<dt>{label}</dt>
|
||||
@@ -159,8 +61,8 @@ const Diagnostics = ({ diagnostics }: { readonly diagnostics: ReadonlyArray<Draf
|
||||
<h2 id="draft-detail-diagnostics-heading">Diagnostics</h2>
|
||||
{diagnostics.length > 0 ? (
|
||||
<ol className="draft-detail__diagnostics">
|
||||
{diagnostics.map((diagnostic, index) => (
|
||||
<Diagnostic key={`${diagnostic.code}-${diagnostic.path}-${index}`} diagnostic={diagnostic} />
|
||||
{withDiagnosticKeys(diagnostics).map(({ diagnostic, key }) => (
|
||||
<Diagnostic key={key} diagnostic={diagnostic} />
|
||||
))}
|
||||
</ol>
|
||||
) : <p>No diagnostics reported.</p>}
|
||||
|
||||
@@ -13,9 +13,11 @@ type DraftWorkbenchProps = {
|
||||
readonly onSelectionChange?: (selection: WorkbenchSelection) => void;
|
||||
};
|
||||
|
||||
const EMPTY_CAPABILITIES: ReadonlyArray<CapabilitySummary> = [];
|
||||
|
||||
export const DraftWorkbench = ({
|
||||
draft,
|
||||
capabilities = [],
|
||||
capabilities = EMPTY_CAPABILITIES,
|
||||
initialSelection = { kind: "canvas" },
|
||||
onSelectionChange,
|
||||
}: DraftWorkbenchProps) => {
|
||||
|
||||
@@ -32,10 +32,10 @@ describe("projectAuthoringGraph", () => {
|
||||
["collect", "use"],
|
||||
["review", "interrupt"],
|
||||
]);
|
||||
expect(model.edges.map((edge) => [edge.id, edge.source, edge.label, edge.target])).toEqual([
|
||||
["e-collect-review-0", "collect", "ok", "review"],
|
||||
["e-review-__end__-1", "review", "approved", "__end__"],
|
||||
["e-review-collect-2", "review", "needs_changes", "collect"],
|
||||
expect(model.edges.map((edge) => [edge.source, edge.label, edge.target])).toEqual([
|
||||
["collect", "ok", "review"],
|
||||
["review", "approved", "__end__"],
|
||||
["review", "needs_changes", "collect"],
|
||||
]);
|
||||
expect(model.nodes.find((node) => node.id === "collect")?.data.nodeRef).toBe(
|
||||
"demo.collect",
|
||||
|
||||
@@ -25,7 +25,14 @@ const stringValue = (value: unknown): string | null =>
|
||||
typeof value === "string" && value.length > 0 ? value : null;
|
||||
|
||||
const stringList = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
||||
(() => {
|
||||
const values: string[] = [];
|
||||
if (!Array.isArray(value)) return values;
|
||||
for (const item of value) {
|
||||
if (typeof item === "string") values.push(item);
|
||||
}
|
||||
return values;
|
||||
})();
|
||||
|
||||
const stepKind = (step: JsonRecord): string => {
|
||||
for (const kind of [
|
||||
@@ -75,18 +82,44 @@ const nodeForStep = (id: string, step: JsonRecord): JsonRecord => {
|
||||
return node;
|
||||
};
|
||||
|
||||
const sortedRecords = (value: JsonRecord | null): Array<[string, JsonRecord]> =>
|
||||
value === null
|
||||
? []
|
||||
: Object.entries(value)
|
||||
.filter((entry): entry is [string, JsonRecord] => isRecord(entry[1]))
|
||||
.toSorted(([left], [right]) => left.localeCompare(right));
|
||||
const sortedRecords = (value: JsonRecord | null): Array<[string, JsonRecord]> => {
|
||||
if (value === null) return [];
|
||||
const records: Array<[string, JsonRecord]> = [];
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (isRecord(item)) records.push([key, item]);
|
||||
}
|
||||
return records.toSorted(([left], [right]) => left.localeCompare(right));
|
||||
};
|
||||
|
||||
const sortedEntries = (value: JsonRecord | null): Array<[string, unknown]> =>
|
||||
value === null
|
||||
? []
|
||||
: Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right));
|
||||
|
||||
const recordArray = (value: unknown): JsonRecord[] => {
|
||||
const records: JsonRecord[] = [];
|
||||
if (!Array.isArray(value)) return records;
|
||||
for (const item of value) {
|
||||
if (isRecord(item)) records.push(item);
|
||||
}
|
||||
return records;
|
||||
};
|
||||
|
||||
const copiedRecordArray = (value: unknown): Array<Record<string, unknown>> => {
|
||||
const records: Array<Record<string, unknown>> = [];
|
||||
for (const item of recordArray(value)) records.push({ ...item });
|
||||
return records;
|
||||
};
|
||||
|
||||
const nodeIdsFor = (nodes: ReadonlyArray<JsonRecord>): Set<string> => {
|
||||
const nodeIds = new Set<string>();
|
||||
for (const node of nodes) {
|
||||
const id = stringValue(node.id);
|
||||
if (id !== null) nodeIds.add(id);
|
||||
}
|
||||
return nodeIds;
|
||||
};
|
||||
|
||||
const routesForSteps = (routes: JsonRecord | null): Array<Record<string, unknown>> => {
|
||||
if (routes === null) return [];
|
||||
const edges: Array<Record<string, unknown>> = [];
|
||||
@@ -104,14 +137,12 @@ const compiledPlan = (draft: JsonRecord): {
|
||||
readonly nodes: Array<JsonRecord>;
|
||||
readonly edges: Array<Record<string, unknown>>;
|
||||
} => {
|
||||
const rawNodes = Array.isArray(draft.nodes)
|
||||
? draft.nodes.filter(isRecord)
|
||||
: [];
|
||||
const rawNodes = recordArray(draft.nodes);
|
||||
const rawEdges = Array.isArray(draft.edges)
|
||||
? draft.edges.filter(isRecord).map((edge) => ({ ...edge }))
|
||||
? copiedRecordArray(draft.edges)
|
||||
: routesForSteps(recordValue(draft.routes));
|
||||
const nodes = rawNodes.map((node) => ({ ...node }));
|
||||
const nodeIds = new Set(nodes.map((node) => stringValue(node.id)).filter((id): id is string => id !== null));
|
||||
const nodes = rawNodes;
|
||||
const nodeIds = nodeIdsFor(nodes);
|
||||
|
||||
for (const edge of rawEdges) {
|
||||
const target = stringValue(edge.to);
|
||||
@@ -130,9 +161,10 @@ const keyedPlan = (draft: JsonRecord): {
|
||||
} => {
|
||||
const steps = recordValue(draft.steps);
|
||||
if (steps === null) return { nodes: [], edges: [] };
|
||||
const nodes = sortedRecords(steps).map(([id, step]) => nodeForStep(id, step));
|
||||
const nodes: JsonRecord[] = [];
|
||||
for (const [id, step] of sortedRecords(steps)) nodes.push(nodeForStep(id, step));
|
||||
const edges = routesForSteps(recordValue(draft.routes));
|
||||
const nodeIds = new Set(nodes.map((node) => stringValue(node.id)).filter((id): id is string => id !== null));
|
||||
const nodeIds = nodeIdsFor(nodes);
|
||||
if (edges.some((edge) => edge.to === "__end__") && !nodeIds.has("__end__")) {
|
||||
nodes.push({ id: "__end__", type: "end", outcome: "ok" });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { DraftDiagnostic } from "../domain/draft-workspace-models.js";
|
||||
import { withDiagnosticKeys } from "./diagnostic-key.js";
|
||||
|
||||
const diagnostic = (overrides: Partial<DraftDiagnostic> = {}): DraftDiagnostic => ({
|
||||
code: "missing_route",
|
||||
path: "routes.review",
|
||||
message: "Review needs a route.",
|
||||
stepId: "review",
|
||||
repairHint: "Add a submitted route.",
|
||||
details: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("withDiagnosticKeys", () => {
|
||||
it("keeps unchanged diagnostic keys stable when another diagnostic is inserted", () => {
|
||||
const first = withDiagnosticKeys([diagnostic(), diagnostic({ code: "invalid_step" })]);
|
||||
const second = withDiagnosticKeys([
|
||||
diagnostic({ code: "missing_start" }),
|
||||
diagnostic(),
|
||||
diagnostic({ code: "invalid_step" }),
|
||||
]);
|
||||
|
||||
expect(second[1]?.key).toBe(first[0]?.key);
|
||||
expect(second[2]?.key).toBe(first[1]?.key);
|
||||
});
|
||||
|
||||
it("gives duplicate diagnostics distinct stable occurrence keys", () => {
|
||||
const entries = withDiagnosticKeys([diagnostic(), diagnostic()]);
|
||||
|
||||
expect(entries[0]?.key).not.toBe(entries[1]?.key);
|
||||
expect(entries[0]?.key).toBe(withDiagnosticKeys([diagnostic()])[0]?.key);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { DraftDiagnostic } from "../domain/draft-workspace-models.js";
|
||||
|
||||
export type DiagnosticEntry = {
|
||||
readonly diagnostic: DraftDiagnostic;
|
||||
readonly key: string;
|
||||
};
|
||||
|
||||
const diagnosticIdentity = (diagnostic: DraftDiagnostic): string =>
|
||||
JSON.stringify([
|
||||
diagnostic.code,
|
||||
diagnostic.path,
|
||||
diagnostic.message,
|
||||
diagnostic.stepId,
|
||||
diagnostic.repairHint,
|
||||
diagnostic.details,
|
||||
]);
|
||||
|
||||
export const withDiagnosticKeys = (
|
||||
diagnostics: ReadonlyArray<DraftDiagnostic>,
|
||||
): ReadonlyArray<DiagnosticEntry> => {
|
||||
const occurrenceByIdentity = new Map<string, number>();
|
||||
const entries: DiagnosticEntry[] = [];
|
||||
for (const diagnostic of diagnostics) {
|
||||
const identity = diagnosticIdentity(diagnostic);
|
||||
const occurrence = occurrenceByIdentity.get(identity) ?? 0;
|
||||
occurrenceByIdentity.set(identity, occurrence + 1);
|
||||
entries.push({
|
||||
diagnostic,
|
||||
key: `diagnostic-${identity}-${occurrence}`,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
|
||||
describe("formatBoundedJson", () => {
|
||||
it("bounds traversal without reading remote fields after the budget", () => {
|
||||
const draft = {
|
||||
first: "x".repeat(1_000),
|
||||
get later() {
|
||||
throw new Error("later field should not be read");
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => formatBoundedJson(draft, 80)).not.toThrow();
|
||||
expect(formatBoundedJson(draft, 80)).toHaveLength(80);
|
||||
expect(formatBoundedJson(draft, 80)).toContain("truncated");
|
||||
});
|
||||
|
||||
it("keeps an exact-fit JSON document complete and valid", () => {
|
||||
const draft = { step: "collect", count: 2 };
|
||||
const completeJson = JSON.stringify(draft, null, 2);
|
||||
|
||||
expect(formatBoundedJson(draft, completeJson.length)).toBe(completeJson);
|
||||
expect(JSON.parse(formatBoundedJson(draft, completeJson.length))).toEqual(draft);
|
||||
});
|
||||
|
||||
it("does not truncate a complete document just below the boundary", () => {
|
||||
const draft = { step: "collect", count: 2 };
|
||||
const completeJson = JSON.stringify(draft, null, 2);
|
||||
|
||||
expect(formatBoundedJson(draft, completeJson.length + 1)).toBe(completeJson);
|
||||
expect(formatBoundedJson(draft, completeJson.length - 1)).toContain("truncated");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
const MAX_RAW_DRAFT_CHARS = 12_000;
|
||||
const TRUNCATION_MARKER = "... truncated ...";
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
// Traverse until the display budget is exhausted so a large remote object is
|
||||
// never fully materialized just to produce a clipped escape-hatch preview.
|
||||
export const formatBoundedJson = (
|
||||
value: unknown,
|
||||
maxChars = MAX_RAW_DRAFT_CHARS,
|
||||
): string => {
|
||||
const truncationMarker = TRUNCATION_MARKER.slice(0, Math.max(0, maxChars));
|
||||
const contentLimit = Math.max(0, maxChars);
|
||||
let output = "";
|
||||
let truncated = false;
|
||||
const activeObjects = new WeakSet<object>();
|
||||
|
||||
const append = (chunk: string): void => {
|
||||
if (truncated) return;
|
||||
if (output.length + chunk.length > contentLimit) {
|
||||
output += chunk.slice(0, Math.max(0, contentLimit - output.length));
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
output += chunk;
|
||||
};
|
||||
|
||||
const appendJsonString = (text: string): void => {
|
||||
append('"');
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
if (truncated) return;
|
||||
const code = text.charCodeAt(index);
|
||||
if (code === 0x22) append('\\"');
|
||||
else if (code === 0x5c) append("\\\\");
|
||||
else if (code < 0x20) append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
else if (code >= 0xd800 && code <= 0xdbff) {
|
||||
const nextCode = text.charCodeAt(index + 1);
|
||||
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
||||
append(text.slice(index, index + 2));
|
||||
index++;
|
||||
} else append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
||||
append(`\\u${code.toString(16).padStart(4, "0")}`);
|
||||
} else append(text.charAt(index));
|
||||
}
|
||||
if (!truncated) append('"');
|
||||
};
|
||||
|
||||
const visit = (current: unknown, depth: number): void => {
|
||||
if (truncated) return;
|
||||
if (current === null || typeof current !== "object") {
|
||||
if (typeof current === "string") appendJsonString(current);
|
||||
else if (typeof current === "number") append(Number.isFinite(current) ? String(current) : "null");
|
||||
else if (typeof current === "boolean") append(current ? "true" : "false");
|
||||
else append("null");
|
||||
return;
|
||||
}
|
||||
if (activeObjects.has(current)) {
|
||||
append('"[Circular]"');
|
||||
return;
|
||||
}
|
||||
activeObjects.add(current);
|
||||
const indent = " ".repeat(depth);
|
||||
const childIndent = " ".repeat(depth + 1);
|
||||
if (Array.isArray(current)) {
|
||||
append("[");
|
||||
let first = true;
|
||||
for (const item of current) {
|
||||
if (truncated) break;
|
||||
append(first ? `\n${childIndent}` : `,\n${childIndent}`);
|
||||
visit(item, depth + 1);
|
||||
first = false;
|
||||
}
|
||||
if (!truncated) append(first ? "]" : `\n${indent}]`);
|
||||
} else {
|
||||
if (!isRecord(current)) {
|
||||
activeObjects.delete(current);
|
||||
return;
|
||||
}
|
||||
const record = current;
|
||||
append("{");
|
||||
let first = true;
|
||||
for (const key in record) {
|
||||
if (!Object.prototype.hasOwnProperty.call(record, key) || truncated) continue;
|
||||
append(first ? `\n${childIndent}` : `,\n${childIndent}`);
|
||||
appendJsonString(key);
|
||||
append(": ");
|
||||
visit(record[key], depth + 1);
|
||||
first = false;
|
||||
}
|
||||
if (!truncated) append(first ? "}" : `\n${indent}}`);
|
||||
}
|
||||
activeObjects.delete(current);
|
||||
};
|
||||
|
||||
visit(value, 0);
|
||||
if (!truncated) return output;
|
||||
const markerStart = Math.max(0, contentLimit - truncationMarker.length);
|
||||
return `${output.slice(0, markerStart)}${truncationMarker}`;
|
||||
};
|
||||
@@ -5,7 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DraftWorkspace } from "../domain/draft-workspace-models.js";
|
||||
import type { DraftWorkspaceController } from "./useDraftWorkspace.js";
|
||||
import { useDraftWorkspace } from "./useDraftWorkspace.js";
|
||||
import { DraftDetailRoute, formatBoundedJson } from "./DraftDetailRoute.js";
|
||||
import { DraftDetailRoute } from "./DraftDetailRoute.js";
|
||||
import { formatBoundedJson } from "../authoring/format-bounded-json.js";
|
||||
|
||||
const globalStyles = readFileSync(
|
||||
"src/styles/global.css",
|
||||
@@ -117,35 +118,6 @@ describe("DraftDetailRoute", () => {
|
||||
expect(rawJson).toHaveAttribute("tabindex", "0");
|
||||
});
|
||||
|
||||
it("bounds JSON traversal without reading remote fields after the budget", () => {
|
||||
const draft = {
|
||||
first: "x".repeat(1_000),
|
||||
get later() {
|
||||
throw new Error("later field should not be read");
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => formatBoundedJson(draft, 80)).not.toThrow();
|
||||
expect(formatBoundedJson(draft, 80)).toHaveLength(80);
|
||||
expect(formatBoundedJson(draft, 80)).toContain("truncated");
|
||||
});
|
||||
|
||||
it("keeps an exact-fit JSON document complete and valid", () => {
|
||||
const draft = { step: "collect", count: 2 };
|
||||
const completeJson = JSON.stringify(draft, null, 2);
|
||||
|
||||
expect(formatBoundedJson(draft, completeJson.length)).toBe(completeJson);
|
||||
expect(JSON.parse(formatBoundedJson(draft, completeJson.length))).toEqual(draft);
|
||||
});
|
||||
|
||||
it("does not truncate a complete document just below the boundary", () => {
|
||||
const draft = { step: "collect", count: 2 };
|
||||
const completeJson = JSON.stringify(draft, null, 2);
|
||||
|
||||
expect(formatBoundedJson(draft, completeJson.length + 1)).toBe(completeJson);
|
||||
expect(formatBoundedJson(draft, completeJson.length - 1)).toContain("truncated");
|
||||
});
|
||||
|
||||
it("does not render a selected workspace whose identity differs from the URL", () => {
|
||||
mockedUseDraftWorkspace.mockReturnValue(
|
||||
controller({ selected: workspace({ workspaceId: "draft-old" }) }),
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
DraftWorkspace,
|
||||
} from "../domain/draft-workspace-models.js";
|
||||
import { DraftWorkbench } from "../authoring/DraftWorkbench.js";
|
||||
export { formatBoundedJson } from "../authoring/ContextInspector.js";
|
||||
import { useDraftWorkspace } from "./useDraftWorkspace.js";
|
||||
|
||||
const titleFor = (workspace: DraftWorkspace): string =>
|
||||
|
||||
Reference in New Issue
Block a user