Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
approvalEffectKey,
|
|
isToolEffectIdempotencyKey,
|
|
legacyScopedToolEffectIdempotencyKey,
|
|
stableJsonValue,
|
|
toolEffectIdempotencyKey,
|
|
} from "./approval-effect-key.js";
|
|
|
|
describe("stableJsonValue", () => {
|
|
it("sorts object keys", () => {
|
|
expect(stableJsonValue({ b: 1, a: 2 })).toBe('{"a":2,"b":1}');
|
|
});
|
|
|
|
it("rejects values that cannot be represented uniquely as JSON", () => {
|
|
const sparse = Array(1);
|
|
|
|
for (const value of [undefined, [undefined], sparse, { value: undefined }, Number.NaN]) {
|
|
expect(() => stableJsonValue(value)).toThrow("only JSON values");
|
|
}
|
|
expect(stableJsonValue([])).toBe("[]");
|
|
expect(stableJsonValue([null])).toBe("[null]");
|
|
});
|
|
|
|
it("rejects cyclic and non-plain objects", () => {
|
|
const cyclic: { self?: unknown } = {};
|
|
cyclic.self = cyclic;
|
|
|
|
expect(() => stableJsonValue(cyclic)).toThrow("only JSON values");
|
|
expect(() => stableJsonValue(new Date(0))).toThrow("only JSON values");
|
|
});
|
|
});
|
|
|
|
describe("approvalEffectKey", () => {
|
|
it("includes run and tool with an opaque digest of canonical args", () => {
|
|
const key = approvalEffectKey("run-1", "destination.write", { body: "private draft" });
|
|
|
|
expect(key).toMatch(/^run-1:destination\.write:[a-f0-9]{64}$/);
|
|
expect(key).not.toContain("private draft");
|
|
});
|
|
});
|
|
|
|
describe("toolEffectIdempotencyKey", () => {
|
|
it("scopes effects to run, tool, and args without the model tool-call id", () => {
|
|
const write = { path: "a.txt", content: "one" };
|
|
expect(toolEffectIdempotencyKey("run-1", "write_file", write)).toMatch(
|
|
/^run-1:write_file:[a-f0-9]{64}$/,
|
|
);
|
|
expect(toolEffectIdempotencyKey("run-1", "write_file", write)).toBe(
|
|
approvalEffectKey("run-1", "write_file", write),
|
|
);
|
|
expect(toolEffectIdempotencyKey("run-1", "write_file", write)).not.toBe(
|
|
toolEffectIdempotencyKey("run-2", "write_file", write),
|
|
);
|
|
expect(toolEffectIdempotencyKey("run-1", "write_file", write)).not.toBe(
|
|
toolEffectIdempotencyKey("run-1", "shell", write),
|
|
);
|
|
expect(toolEffectIdempotencyKey("run-1", "write_file", write)).not.toBe(
|
|
toolEffectIdempotencyKey("run-1", "write_file", {
|
|
path: "a.txt",
|
|
content: "two",
|
|
}),
|
|
);
|
|
expect(toolEffectIdempotencyKey("run-1", "write_file", write)).not.toContain("call_0");
|
|
});
|
|
|
|
it("is stable across replay when the model assigns a new tool-call id", () => {
|
|
const args = { path: "MEMORY.md", content: "fact" };
|
|
expect(toolEffectIdempotencyKey("run-1", "remember", args)).toBe(
|
|
toolEffectIdempotencyKey("run-1", "remember", args),
|
|
);
|
|
expect(legacyScopedToolEffectIdempotencyKey("run-1", "remember", "call_0", args)).not.toBe(
|
|
toolEffectIdempotencyKey("run-1", "remember", args),
|
|
);
|
|
});
|
|
|
|
it("distinguishes identical-args calls in one run via occurrence", () => {
|
|
const args = { actions: [{ kind: "click", x: 12, y: 40 }] };
|
|
const first = toolEffectIdempotencyKey("run-1", "computer_act", args);
|
|
const second = toolEffectIdempotencyKey("run-1", "computer_act", args, 1);
|
|
expect(first).toBe(approvalEffectKey("run-1", "computer_act", args));
|
|
expect(second).not.toBe(first);
|
|
expect(second).toBe(`${first}:1`);
|
|
expect(isToolEffectIdempotencyKey(first, "run-1", "computer_act")).toBe(true);
|
|
expect(isToolEffectIdempotencyKey(second, "run-1", "computer_act")).toBe(true);
|
|
expect(
|
|
isToolEffectIdempotencyKey(
|
|
legacyScopedToolEffectIdempotencyKey("run-1", "computer_act", "call_0", args),
|
|
"run-1",
|
|
"computer_act",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|