mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
a7775e4ba6
We want to display a nicely parsed and rendered log of events that happened over a span of an Argent session. Current log file is not designed to be machine-readable, so adding a JSONL-formatted log of events (gated by `tool-server-event-log`). I eventually want this 😋 https://x.com/j3nnings/status/2069072177584849192 When enabled, the tool server writes Bunyan-compatible records to `ARGENT_EVENT_LOG` or `~/.argent/tool-server-events.jsonl`. The log captures tool-server lifecycle events and registry lifecycle events, including tool invocations and others. To make messages nicer, added `interaction` object to tool definition. This allows tools to define a nicer message in log (print "Screenshot capture completed." instead of "Tool `screenshot` completed.") Adding these to the existing tools is a follow up PR matter I think.
225 lines
7.5 KiB
TypeScript
225 lines
7.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { Registry } from "../src/registry";
|
|
import { attachRegistryLogger } from "../src/logger";
|
|
import { createStaticBlueprint, createMockToolDef, staticUrn } from "./helpers";
|
|
|
|
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
logSpy.mockRestore();
|
|
errorSpy.mockRestore();
|
|
});
|
|
|
|
describe("attachRegistryLogger — formatError via serviceError", () => {
|
|
it("logs a single error with message and stack", async () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
const err = new Error("connection refused");
|
|
registry.events.emit("serviceError", "svc:1", err);
|
|
|
|
expect(errorSpy).toHaveBeenCalledOnce();
|
|
const output = errorSpy.mock.calls[0]![0] as string;
|
|
expect(output).toContain("[registry] serviceError svc:1:");
|
|
expect(output).toContain("connection refused");
|
|
// Stack trace lines should be present (V8/Node always attaches .stack)
|
|
expect(output).toContain("\n");
|
|
});
|
|
|
|
it("logs a 3-level cause chain joined by ' — caused by: '", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
const root = new Error("ECONNREFUSED");
|
|
const mid = new Error("WebSocket handshake failed", { cause: root });
|
|
const outer = new Error("Service init failed", { cause: mid });
|
|
|
|
registry.events.emit("serviceError", "svc:2", outer);
|
|
|
|
const output = errorSpy.mock.calls[0]![0] as string;
|
|
expect(output).toContain("Service init failed");
|
|
expect(output).toContain(" — caused by: ");
|
|
expect(output).toContain("WebSocket handshake failed");
|
|
expect(output).toContain("ECONNREFUSED");
|
|
});
|
|
|
|
it("deduplicates repeated messages in the cause chain", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
const inner = new Error("boom");
|
|
const outer = new Error("boom", { cause: inner });
|
|
|
|
registry.events.emit("serviceError", "svc:3", outer);
|
|
|
|
const output = errorSpy.mock.calls[0]![0] as string;
|
|
// "boom" should appear only once (no " — caused by: boom")
|
|
expect(output).not.toContain(" — caused by: ");
|
|
expect(output).toContain("boom");
|
|
});
|
|
|
|
it("falls back to message-only when stack is absent", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
const err = new Error("no stack");
|
|
// Simulate an error with no stack (e.g. from a non-V8 runtime)
|
|
err.stack = undefined;
|
|
|
|
registry.events.emit("serviceError", "svc:4", err);
|
|
|
|
const output = errorSpy.mock.calls[0]![0] as string;
|
|
expect(output).toContain("no stack");
|
|
// The output after the message should not have dangling newline + stack
|
|
expect(output).toBe("[registry] serviceError svc:4:\nno stack");
|
|
});
|
|
|
|
it("uses the deepest stack in the chain", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
const inner = new Error("root cause");
|
|
const outer = new Error("wrapper", { cause: inner });
|
|
// Delete the outer stack so only the inner stack is available
|
|
outer.stack = undefined;
|
|
|
|
registry.events.emit("serviceError", "svc:5", outer);
|
|
|
|
const output = errorSpy.mock.calls[0]![0] as string;
|
|
// Should contain the inner error's stack (which has "root cause" in its first line)
|
|
expect(output).toMatch(/at /);
|
|
});
|
|
});
|
|
|
|
describe("attachRegistryLogger — formatError via toolFailed", () => {
|
|
it("formats a cause chain for toolFailed the same way", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
const root = new Error("timeout");
|
|
const outer = new Error("evaluate failed", { cause: root });
|
|
|
|
registry.events.emit(
|
|
"toolFailed",
|
|
"my-tool",
|
|
"11111111-1111-4111-8111-111111111111",
|
|
outer,
|
|
undefined,
|
|
"Failed my-tool."
|
|
);
|
|
|
|
const output = errorSpy.mock.calls[0]![0] as string;
|
|
expect(output).toContain(
|
|
"[registry] toolFailed my-tool (11111111-1111-4111-8111-111111111111):"
|
|
);
|
|
expect(output).toContain("evaluate failed");
|
|
expect(output).toContain("timeout");
|
|
});
|
|
});
|
|
|
|
describe("attachRegistryLogger — happy-path events", () => {
|
|
it("logs serviceStateChange", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
registry.events.emit("serviceStateChange", "svc:x", "IDLE" as any, "STARTING" as any);
|
|
|
|
expect(logSpy).toHaveBeenCalledOnce();
|
|
expect(logSpy.mock.calls[0]![0]).toContain("serviceStateChange svc:x: IDLE → STARTING");
|
|
});
|
|
|
|
it("logs toolRegistered", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
registry.events.emit("toolRegistered", "my-tool");
|
|
|
|
expect(logSpy).toHaveBeenCalledOnce();
|
|
expect(logSpy.mock.calls[0]![0]).toContain("toolRegistered my-tool");
|
|
});
|
|
|
|
it("logs toolInvoked", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
registry.events.emit(
|
|
"toolInvoked",
|
|
"my-tool",
|
|
"11111111-1111-4111-8111-111111111111",
|
|
"Starting my-tool."
|
|
);
|
|
|
|
expect(logSpy).toHaveBeenCalledOnce();
|
|
expect(logSpy.mock.calls[0]![0]).toContain(
|
|
"toolInvoked my-tool (11111111-1111-4111-8111-111111111111)"
|
|
);
|
|
});
|
|
|
|
it("logs toolCompleted with duration", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
registry.events.emit(
|
|
"toolCompleted",
|
|
"my-tool",
|
|
"11111111-1111-4111-8111-111111111111",
|
|
123.456,
|
|
"Completed my-tool."
|
|
);
|
|
|
|
expect(logSpy).toHaveBeenCalledOnce();
|
|
expect(logSpy.mock.calls[0]![0]).toContain(
|
|
"toolCompleted my-tool (11111111-1111-4111-8111-111111111111, 123.46ms)"
|
|
);
|
|
});
|
|
|
|
it("logs serviceRegistered", () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
|
|
registry.events.emit("serviceRegistered", "svc:1");
|
|
|
|
expect(logSpy).toHaveBeenCalledOnce();
|
|
expect(logSpy.mock.calls[0]![0]).toContain("serviceRegistered svc:1");
|
|
});
|
|
});
|
|
|
|
describe("attachRegistryLogger — end-to-end with Registry", () => {
|
|
it("logs serviceError with cause when factory throws", async () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
const { blueprint } = createStaticBlueprint("Fail", { failOnInit: true });
|
|
registry.registerBlueprint(blueprint);
|
|
|
|
await expect(registry.resolveService(staticUrn("Fail"))).rejects.toThrow();
|
|
|
|
const errorCalls = (errorSpy.mock.calls as unknown[][]).map((c) => c[0] as string);
|
|
const serviceErrorLog = errorCalls.find((c) => c.includes("serviceError"));
|
|
expect(serviceErrorLog).toBeDefined();
|
|
expect(serviceErrorLog).toContain("Fail factory failure");
|
|
});
|
|
|
|
it("logs toolFailed with cause when tool execution errors", async () => {
|
|
const registry = new Registry();
|
|
attachRegistryLogger(registry);
|
|
const { blueprint } = createStaticBlueprint("S");
|
|
registry.registerBlueprint(blueprint);
|
|
registry.registerTool(
|
|
createMockToolDef("bad-tool", () => ({ S: staticUrn("S") }), { fail: true })
|
|
);
|
|
|
|
await expect(registry.invokeTool("bad-tool")).rejects.toThrow();
|
|
|
|
const errorCalls = (errorSpy.mock.calls as unknown[][]).map((c) => c[0] as string);
|
|
const toolFailedLog = errorCalls.find((c) => c.includes("toolFailed bad-tool"));
|
|
expect(toolFailedLog).toBeDefined();
|
|
expect(toolFailedLog).toContain("bad-tool execution failure");
|
|
});
|
|
});
|