runa/vscode/test/debugVariables.test.ts

195 lines
8.1 KiB
TypeScript
Raw Permalink Normal View History

import assert from "node:assert/strict";
import { test } from "node:test";
import { ActorVariables } from "../src/debugVariables";
import { actor, REF } from "./fixtures";
test("actor variables preserve exact values, types, escaped strings and typed references", () => {
const variables = new ActorVariables();
variables.capture(actor());
const scope = variables.scope();
assert.match(scope.name, /committed; not frame locals/);
const roots = variables.variables(scope.variablesReference);
assert.deepEqual(
roots.map((variable) => variable.name),
["committed_state", "mailbox", "config", "metadata"],
);
const state = roots[0]!;
assert.equal(state.value, "Record(5)");
assert.equal(state.namedVariables, 5);
const fields = variables.variables(state.variablesReference);
assert.equal(fields.find((field) => field.name === "minimum")?.value, "-9223372036854775808");
assert.equal(fields.find((field) => field.name === "maximum")?.value, "9223372036854775807");
assert.equal(fields.find((field) => field.name === "reference")?.value, `@${REF}`);
assert.equal(fields.find((field) => field.name === "reference")?.type, "actor_ref");
assert.equal(fields.find((field) => field.name === "text")?.type, "string");
assert.equal(
fields.find((field) => field.name === "text")?.value,
JSON.stringify(`@${REF} "雪"`),
);
assert.ok(fields.every((field) => field.presentationHint.attributes[0] === "readOnly"));
const list = fields.find((field) => field.name === "list")!;
assert.equal(list.indexedVariables, 2);
assert.deepEqual(
variables.variables(list.variablesReference).map((child) => child.value),
["null", "true"],
);
const metadata = variables.variables(roots[3]!.variablesReference);
assert.equal(
metadata.find((field) => field.name === "mailbox_id")?.value,
"18446744073709551615",
);
const mailbox = variables.variables(roots[1]!.variablesReference);
const reserved = mailbox.find((field) => field.name === "reserved")!;
assert.equal(
reserved.indexedVariables,
1,
"a reserved null message must not be confused with no reservation",
);
assert.equal(variables.variables(reserved.variablesReference)[0]?.value, "null");
});
test("snapshot watches resolve JSON Pointer paths and expose expandable evaluate names", () => {
const snapshot = actor();
snapshot.state = {
type: "record",
data: Object.fromEntries([
[
"a/b",
{
type: "record",
data: {
"~key": { type: "list", data: [{ type: "integer", data: -9223372036854775808n }] },
},
},
],
["", { type: "null" }],
["__proto__", { type: "string", data: "ordinary own field" }],
["~1", { type: "bool", data: true }],
['"quoted"\n雪', { type: "integer", data: 7n }],
]),
};
const variables = new ActorVariables();
variables.capture(snapshot);
assert.equal(variables.evaluate("actor").value, "Record(4)");
const path = "actor/committed_state/a~1b/~0key/0";
assert.equal(variables.evaluate(path).value, "-9223372036854775808");
assert.equal(variables.evaluate("actor/committed_state/").value, "null");
assert.equal(variables.evaluate("actor/committed_state/__proto__").value, '"ordinary own field"');
assert.equal(variables.evaluate("actor/committed_state/~01").value, "true");
assert.equal(variables.evaluate('actor/committed_state/"quoted"\n雪').value, "7");
assert.throws(() => variables.evaluate("actor/committed_state/constructor"), /not found/);
const pending = [variables.scope().variablesReference];
while (pending.length) {
for (const variable of variables.variables(pending.pop()!)) {
assert.deepEqual(variables.evaluate(variable.evaluateName), {
...variable,
name: variable.evaluateName,
});
if (variable.variablesReference) pending.push(variable.variablesReference);
}
}
});
test("snapshot watches reject expressions, malformed pointers, scalar traversal and invalid indices", () => {
const variables = new ActorVariables();
variables.capture(actor());
for (const expression of [
"",
"/committed_state",
"state",
"actor.committed_state",
"actor()",
"actor = 1",
"actor/committed_state/text/0",
"actor/committed_state/list/-1",
"actor/committed_state/list/01",
"actor/committed_state/list/1.0",
"actor/committed_state/list/2",
"actor/committed_state/list/9007199254740992",
"actor/committed_state/list/constructor",
"actor/committed_state/list/-",
"actor/committed_state/list/ 0",
"actor/committed_state/list/0 + 1",
"actor/committed_state/~",
"actor/committed_state/~2",
"actor/committed_state/~00~",
`actor/${"x/".repeat(65)}`,
`actor/${"x".repeat(4096)}`,
])
assert.throws(() => variables.evaluate(expression), expression);
assert.equal(variables.evaluate("actor/committed_state/list/0").value, "null");
const reference = variables.evaluate("actor/committed_state").variablesReference;
variables.clear();
assert.throws(() => variables.evaluate("actor/committed_state"), /unavailable/);
assert.throws(() => variables.variables(reference), /Stale/);
});
test("snapshot watch path length accepts the exact documented boundary", () => {
const prefix = "actor/committed_state/";
const key = "x".repeat(4096 - prefix.length);
const snapshot = actor();
snapshot.state = { type: "record", data: { [key]: { type: "bool", data: true } } };
const variables = new ActorVariables();
variables.capture(snapshot);
assert.equal(variables.evaluate(prefix + key).value, "true");
assert.throws(() => variables.evaluate(prefix + key + "x"), /exceeds 4096/);
});
test("variable handles are stable per snapshot, stale after clear, and snapshots never alias inputs", () => {
const snapshot = actor();
const variables = new ActorVariables();
variables.capture(snapshot);
const scope = variables.scope();
const state = variables.variables(scope.variablesReference)[0]!;
assert.deepEqual(variables.variables(scope.variablesReference)[0], state);
snapshot.state = { type: "integer", data: 999n };
snapshot.queued.push({ type: "null" });
assert.equal(variables.variables(scope.variablesReference)[0]?.value, "Record(5)");
const mailbox = variables.variables(
variables.variables(scope.variablesReference)[1]!.variablesReference,
);
assert.equal(mailbox.find((field) => field.name === "queued")?.indexedVariables, 1);
variables.capture(snapshot);
assert.throws(() => variables.variables(scope.variablesReference), /Stale/);
const next = variables.scope();
assert.notEqual(scope.variablesReference, next.variablesReference);
assert.equal(variables.variables(next.variablesReference)[0]?.value, "999");
variables.clear();
assert.throws(() => variables.scope(), /unavailable/);
assert.throws(() => variables.variables(next.variablesReference), /Stale/);
});
test("variables support named/indexed paging and bounded previews without recursive serialization", () => {
const snapshot = actor();
snapshot.state = {
type: "list",
data: [
{ type: "string", data: "\n".repeat(300) },
{ type: "record", data: {} },
{ type: "list", data: [] },
{ type: "integer", data: 1n },
],
};
snapshot.reserved = null;
const variables = new ActorVariables();
variables.capture(snapshot);
const root = variables.scope().variablesReference;
const state = variables.variables(root)[0]!.variablesReference;
assert.deepEqual(variables.variables(state, "named"), []);
assert.deepEqual(variables.variables(root, "indexed"), []);
assert.deepEqual(
variables.variables(state, "indexed", 1, 2).map((field) => field.name),
["1", "2"],
);
assert.deepEqual(
variables.variables(state, undefined, 2, 0).map((field) => field.name),
["2", "3"],
);
assert.deepEqual(variables.variables(state, "indexed", 100), []);
const fields = variables.variables(state);
assert.equal(fields[0]?.value, `${JSON.stringify("\n".repeat(256))}... (300 UTF-16 code units)`);
assert.equal(fields[1]?.variablesReference, 0);
assert.equal(fields[2]?.variablesReference, 0);
assert.throws(() => variables.variables(root, "invalid"), /filter/);
});