fix(journal): surface variable eval errors
Collect and report failed declarations, failed tag modifier expressions, and circular dependencies instead of swallowing them. Diagnostics are deduped, logged with key/expression context, cleared on re-init, and exposed through useJournalCompletions and the VariableView warning banner. A bad declaration no longer aborts the rest of a cascade. Reject malformed numeric literals in the tokenizer so inputs like "1.2.3" throw instead of being truncated by parseFloat.
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
||||
getDeclExpr,
|
||||
extractDependencies,
|
||||
rebuildReactivityFromStore,
|
||||
getReactivityDiagnostics,
|
||||
} from "../../components/journal/var-reactivity";
|
||||
import { parseInput } from "../../components/journal/command-parser";
|
||||
import {
|
||||
@@ -396,6 +397,15 @@ describe("evaluateExpression", () => {
|
||||
).toThrow("Unexpected end");
|
||||
});
|
||||
|
||||
test("throws on malformed number literals", () => {
|
||||
expect(() =>
|
||||
evaluateExpression("1.2.3", { lookup: () => undefined }),
|
||||
).toThrow("Invalid number literal");
|
||||
expect(() =>
|
||||
evaluateExpression("1.", { lookup: () => undefined }),
|
||||
).toThrow("Invalid number literal");
|
||||
});
|
||||
|
||||
test("handles decimal numbers", () => {
|
||||
const result = evaluateExpression("3.5 + 2.5", { lookup: () => undefined });
|
||||
expect(result.value).toEqual({ kind: "number", value: 6 });
|
||||
@@ -574,6 +584,113 @@ describe("var-reactivity", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("diagnostics", () => {
|
||||
test("reports a declaration that fails to evaluate", () => {
|
||||
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
initReactivity({
|
||||
declarations: [
|
||||
{ key: "$hp", expression: "10 + 5" },
|
||||
{ key: "$bad", expression: "#warrior * 2" },
|
||||
],
|
||||
tagModifiers: [],
|
||||
});
|
||||
|
||||
const results = computeInitialValues(store());
|
||||
expect(results.find((r) => r.key === "$hp")?.value).toBe("15");
|
||||
expect(results.find((r) => r.key === "$bad")).toBeUndefined();
|
||||
|
||||
const diags = getReactivityDiagnostics();
|
||||
expect(diags).toHaveLength(1);
|
||||
expect(diags[0].kind).toBe("declaration");
|
||||
expect(diags[0].key).toBe("$bad");
|
||||
expect(diags[0].expression).toBe("#warrior * 2");
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
test("dedupes repeated failures of the same expression", () => {
|
||||
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
initReactivity({
|
||||
declarations: [{ key: "$bad", expression: "#warrior * 2" }],
|
||||
tagModifiers: [],
|
||||
});
|
||||
|
||||
computeInitialValues(store());
|
||||
computeInitialValues(store());
|
||||
expect(getReactivityDiagnostics()).toHaveLength(1);
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
test("clears diagnostics on re-init", () => {
|
||||
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
initReactivity({
|
||||
declarations: [{ key: "$bad", expression: "#warrior * 2" }],
|
||||
tagModifiers: [],
|
||||
});
|
||||
computeInitialValues(store());
|
||||
expect(getReactivityDiagnostics()).toHaveLength(1);
|
||||
|
||||
initReactivity({ declarations: [], tagModifiers: [] });
|
||||
expect(getReactivityDiagnostics()).toHaveLength(0);
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
test("records a diagnostic when declarations are circular", () => {
|
||||
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
expect(() =>
|
||||
initReactivity({
|
||||
declarations: [
|
||||
{ key: "$a", expression: "$b + 1" },
|
||||
{ key: "$b", expression: "$a + 1" },
|
||||
],
|
||||
tagModifiers: [],
|
||||
}),
|
||||
).toThrow();
|
||||
|
||||
const diags = getReactivityDiagnostics();
|
||||
expect(diags).toHaveLength(1);
|
||||
expect(diags[0].kind).toBe("init");
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
test("reports a tag modifier that fails to evaluate", () => {
|
||||
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
initReactivity({
|
||||
declarations: [],
|
||||
tagModifiers: [
|
||||
{ tag: "#warrior", target: "$hp", expression: "#x * 2", threshold: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
setBase("$class", "#warrior:1");
|
||||
computeCascade("$class", undefined, store({ $class: "#warrior:1" }));
|
||||
|
||||
const diags = getReactivityDiagnostics();
|
||||
expect(diags).toHaveLength(1);
|
||||
expect(diags[0].kind).toBe("modifier");
|
||||
expect(diags[0].key).toBe("$hp");
|
||||
expect(diags[0].tag).toBe("#warrior");
|
||||
expect(diags[0].source).toBe("$class");
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
test("keeps cascading other dependents when a declaration fails", () => {
|
||||
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
initReactivity({
|
||||
declarations: [
|
||||
{ key: "$bad", expression: "$con * #warrior" },
|
||||
{ key: "$good", expression: "$con + 1" },
|
||||
],
|
||||
tagModifiers: [],
|
||||
});
|
||||
|
||||
const cascade = computeCascade("$con", undefined, store({ $con: "5" }));
|
||||
expect(cascade.find((r) => r.key === "$good")?.value).toBe("6");
|
||||
expect(cascade.find((r) => r.key === "$bad")).toBeUndefined();
|
||||
expect(getReactivityDiagnostics().some((d) => d.key === "$bad")).toBe(true);
|
||||
warn.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCombined / setBase", () => {
|
||||
test("returns base value when no mods", () => {
|
||||
initReactivity({ declarations: [], tagModifiers: [] });
|
||||
|
||||
Reference in New Issue
Block a user