mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
158 lines
5.6 KiB
TypeScript
158 lines
5.6 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import type { Registry, ToolContext } from "@argent/registry";
|
|
import { invokeSubTool } from "../src/utils/sub-invoke";
|
|
|
|
function mockRegistry(impl?: (id: string, args: unknown) => unknown): Registry {
|
|
return {
|
|
invokeTool: vi.fn(async (id: string, args: unknown) => impl?.(id, args)),
|
|
} as unknown as Registry;
|
|
}
|
|
|
|
describe("invokeSubTool", () => {
|
|
it("invokes directly (no third arg) when there is no telemetry context", async () => {
|
|
const registry = mockRegistry(() => ({ ok: true }));
|
|
|
|
const result = await invokeSubTool(registry, undefined, "gesture-tap", { x: 0.5, y: 0.3 });
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
// No options object — preserves the pre-fix call shape for direct invokes.
|
|
expect(registry.invokeTool).toHaveBeenCalledWith("gesture-tap", { x: 0.5, y: 0.3 });
|
|
});
|
|
|
|
it("invokes directly when the context carries no recorder", async () => {
|
|
const registry = mockRegistry();
|
|
const ctx = { artifacts: {} } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "gesture-tap", { x: 0.1 });
|
|
|
|
expect(registry.invokeTool).toHaveBeenCalledWith("gesture-tap", { x: 0.1 });
|
|
});
|
|
|
|
it("records a child invocation and forwards the id + recorder when attribution is present", async () => {
|
|
const registry = mockRegistry(() => ({ done: true }));
|
|
const release = vi.fn();
|
|
const recordChildInvocation = vi.fn((_id: string, _args?: unknown) => release);
|
|
const ctx = { artifacts: {}, recordChildInvocation } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "gesture-swipe", { fromX: 0.5 });
|
|
|
|
expect(recordChildInvocation).toHaveBeenCalledOnce();
|
|
const childId = recordChildInvocation.mock.calls[0]![0];
|
|
expect(childId).toEqual(expect.any(String));
|
|
|
|
// The child's own args are handed to the recorder so it can re-derive this
|
|
// sub-tool's platform instead of inheriting the orchestrator's.
|
|
expect(recordChildInvocation).toHaveBeenCalledWith(childId, { fromX: 0.5 });
|
|
|
|
// The sub-tool is invoked under the freshly-minted id, and the recorder is
|
|
// forwarded so propagation survives further nesting.
|
|
expect(registry.invokeTool).toHaveBeenCalledWith(
|
|
"gesture-swipe",
|
|
{ fromX: 0.5 },
|
|
{
|
|
toolInvocationId: childId,
|
|
recordChildInvocation,
|
|
}
|
|
);
|
|
expect(release).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("forwards inRepeatFlowScope on the attributed path when set via overrides", async () => {
|
|
const registry = mockRegistry();
|
|
const recordChildInvocation = vi.fn((_id: string, _args?: unknown) => vi.fn());
|
|
const ctx = { artifacts: {}, recordChildInvocation } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(
|
|
registry,
|
|
ctx,
|
|
"flow-execute",
|
|
{ name: "frag" },
|
|
{ inRepeatFlowScope: true }
|
|
);
|
|
|
|
expect(registry.invokeTool).toHaveBeenCalledWith(
|
|
"flow-execute",
|
|
{ name: "frag" },
|
|
expect.objectContaining({ inRepeatFlowScope: true, recordChildInvocation })
|
|
);
|
|
});
|
|
|
|
it("inherits inRepeatFlowScope from ctx on the attributed path, so an orchestrator in the middle propagates it", async () => {
|
|
const registry = mockRegistry();
|
|
const recordChildInvocation = vi.fn((_id: string, _args?: unknown) => vi.fn());
|
|
const ctx = {
|
|
artifacts: {},
|
|
recordChildInvocation,
|
|
inRepeatFlowScope: true,
|
|
} as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "flow-execute", { name: "frag" });
|
|
|
|
expect(registry.invokeTool).toHaveBeenCalledWith(
|
|
"flow-execute",
|
|
{ name: "frag" },
|
|
expect.objectContaining({ inRepeatFlowScope: true })
|
|
);
|
|
});
|
|
|
|
it("forwards inRepeatFlowScope on the unattributed path when set via overrides", async () => {
|
|
const registry = mockRegistry();
|
|
const ctx = { artifacts: {} } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(
|
|
registry,
|
|
ctx,
|
|
"flow-execute",
|
|
{ name: "frag" },
|
|
{ inRepeatFlowScope: true }
|
|
);
|
|
|
|
// Exact third arg: with no recorder and no signal the scope is the only option.
|
|
expect(registry.invokeTool).toHaveBeenCalledWith(
|
|
"flow-execute",
|
|
{ name: "frag" },
|
|
{ inRepeatFlowScope: true }
|
|
);
|
|
});
|
|
|
|
it("inherits inRepeatFlowScope from ctx on the unattributed path", async () => {
|
|
const registry = mockRegistry();
|
|
const ctx = { artifacts: {}, inRepeatFlowScope: true } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "flow-execute", { name: "frag" });
|
|
|
|
expect(registry.invokeTool).toHaveBeenCalledWith(
|
|
"flow-execute",
|
|
{ name: "frag" },
|
|
{ inRepeatFlowScope: true }
|
|
);
|
|
});
|
|
|
|
it("mints a distinct id per call", async () => {
|
|
const registry = mockRegistry();
|
|
const recordChildInvocation = vi.fn((_id: string) => vi.fn());
|
|
const ctx = { artifacts: {}, recordChildInvocation } as unknown as ToolContext;
|
|
|
|
await invokeSubTool(registry, ctx, "gesture-tap", {});
|
|
await invokeSubTool(registry, ctx, "gesture-tap", {});
|
|
|
|
const idA = recordChildInvocation.mock.calls[0]![0];
|
|
const idB = recordChildInvocation.mock.calls[1]![0];
|
|
expect(idA).not.toEqual(idB);
|
|
});
|
|
|
|
it("releases the recorded metadata even when the sub-tool throws", async () => {
|
|
const registry = {
|
|
invokeTool: vi.fn(async () => {
|
|
throw new Error("boom");
|
|
}),
|
|
} as unknown as Registry;
|
|
const release = vi.fn();
|
|
const recordChildInvocation = vi.fn((_id: string) => release);
|
|
const ctx = { artifacts: {}, recordChildInvocation } as unknown as ToolContext;
|
|
|
|
await expect(invokeSubTool(registry, ctx, "gesture-tap", {})).rejects.toThrow("boom");
|
|
expect(release).toHaveBeenCalledOnce();
|
|
});
|
|
});
|