mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 02:10:43 +00:00
222 lines
7.1 KiB
TypeScript
222 lines
7.1 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
TRAJECTORY_RUNTIME_EVENT_MAX_BYTES,
|
|
createTrajectoryRuntimeRecorder,
|
|
resolveTrajectoryPointerOpenFlags,
|
|
resolveTrajectoryPointerFilePath,
|
|
resolveTrajectoryFilePath,
|
|
toTrajectoryToolDefinitions,
|
|
} from "./runtime.js";
|
|
|
|
type TrajectoryRuntimeRecorder = NonNullable<ReturnType<typeof createTrajectoryRuntimeRecorder>>;
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeTempDir(): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-trajectory-runtime-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function expectTrajectoryRuntimeRecorder(
|
|
recorder: ReturnType<typeof createTrajectoryRuntimeRecorder>,
|
|
): TrajectoryRuntimeRecorder {
|
|
expect(recorder).toEqual(expect.objectContaining({ recordEvent: expect.any(Function) }));
|
|
if (recorder === null) {
|
|
throw new Error("Expected trajectory runtime recorder");
|
|
}
|
|
return recorder;
|
|
}
|
|
|
|
describe("trajectory runtime", () => {
|
|
it("resolves a session-adjacent trajectory file by default", () => {
|
|
expect(
|
|
resolveTrajectoryFilePath({
|
|
sessionFile: "/tmp/session.jsonl",
|
|
sessionId: "session-1",
|
|
}),
|
|
).toBe("/tmp/session.trajectory.jsonl");
|
|
});
|
|
|
|
it("sanitizes session ids when resolving an override directory", () => {
|
|
expect(
|
|
resolveTrajectoryFilePath({
|
|
env: { OPENCLAW_TRAJECTORY_DIR: "/tmp/traces" },
|
|
sessionId: "../evil/session",
|
|
}),
|
|
).toBe("/tmp/traces/___evil_session.jsonl");
|
|
});
|
|
|
|
it("records sanitized runtime events by default", () => {
|
|
const writes: string[] = [];
|
|
const recorder = createTrajectoryRuntimeRecorder({
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:main:session-1",
|
|
sessionFile: "/tmp/session.jsonl",
|
|
provider: "openai",
|
|
modelId: "gpt-5.4",
|
|
modelApi: "responses",
|
|
workspaceDir: "/tmp/workspace",
|
|
writer: {
|
|
filePath: "/tmp/session.trajectory.jsonl",
|
|
write: (line) => {
|
|
writes.push(line);
|
|
},
|
|
flush: async () => undefined,
|
|
},
|
|
});
|
|
|
|
const runtimeRecorder = expectTrajectoryRuntimeRecorder(recorder);
|
|
runtimeRecorder.recordEvent("context.compiled", {
|
|
systemPrompt: "system prompt",
|
|
headers: [{ name: "Authorization", value: "Bearer sk-test-secret-token" }],
|
|
command: "curl -H 'Authorization: Bearer sk-other-secret-token'",
|
|
tools: toTrajectoryToolDefinitions([
|
|
{ name: "z-tool", parameters: { z: 1 } },
|
|
{ name: "a-tool", description: "alpha", parameters: { a: 1 } },
|
|
{ name: " ", description: "ignored" },
|
|
]),
|
|
});
|
|
|
|
expect(writes).toHaveLength(1);
|
|
const parsed = JSON.parse(writes[0]);
|
|
expect(parsed.type).toBe("context.compiled");
|
|
expect(parsed.source).toBe("runtime");
|
|
expect(parsed.sessionId).toBe("session-1");
|
|
expect(parsed.data.tools).toEqual([
|
|
{ name: "a-tool", description: "alpha", parameters: { a: 1 } },
|
|
{ name: "z-tool", parameters: { z: 1 } },
|
|
]);
|
|
expect(JSON.stringify(parsed.data)).not.toContain("sk-test-secret-token");
|
|
expect(JSON.stringify(parsed.data)).not.toContain("sk-other-secret-token");
|
|
});
|
|
|
|
it("bounds large runtime event fields before serialization", () => {
|
|
const writes: string[] = [];
|
|
const recorder = createTrajectoryRuntimeRecorder({
|
|
sessionId: "session-1",
|
|
sessionFile: "/tmp/session.jsonl",
|
|
writer: {
|
|
filePath: "/tmp/session.trajectory.jsonl",
|
|
write: (line) => {
|
|
writes.push(line);
|
|
},
|
|
flush: async () => undefined,
|
|
},
|
|
});
|
|
|
|
const runtimeRecorder = expectTrajectoryRuntimeRecorder(recorder);
|
|
runtimeRecorder.recordEvent("context.compiled", {
|
|
prompt: "x".repeat(TRAJECTORY_RUNTIME_EVENT_MAX_BYTES + 1),
|
|
});
|
|
|
|
expect(writes).toHaveLength(1);
|
|
const parsed = JSON.parse(writes[0]);
|
|
expect(parsed.data.prompt).toMatchObject({
|
|
truncated: true,
|
|
reason: "trajectory-field-size-limit",
|
|
});
|
|
expect(Buffer.byteLength(writes[0], "utf8")).toBeLessThanOrEqual(
|
|
TRAJECTORY_RUNTIME_EVENT_MAX_BYTES + 1,
|
|
);
|
|
});
|
|
|
|
it("stops runtime capture at the file budget and records a truncation event", async () => {
|
|
const writes: string[] = [];
|
|
const recorder = createTrajectoryRuntimeRecorder({
|
|
sessionId: "session-1",
|
|
sessionFile: "/tmp/session.jsonl",
|
|
maxRuntimeFileBytes: 900,
|
|
writer: {
|
|
filePath: "/tmp/session.trajectory.jsonl",
|
|
write: (line) => {
|
|
writes.push(line);
|
|
},
|
|
flush: async () => undefined,
|
|
},
|
|
});
|
|
|
|
const runtimeRecorder = expectTrajectoryRuntimeRecorder(recorder);
|
|
runtimeRecorder.recordEvent("context.compiled", {
|
|
prompt: "x".repeat(180),
|
|
});
|
|
runtimeRecorder.recordEvent("prompt.submitted", {
|
|
prompt: "y".repeat(180),
|
|
});
|
|
runtimeRecorder.recordEvent("model.completed", {
|
|
get prompt() {
|
|
throw new Error("stopped recorder should not read dropped payloads");
|
|
},
|
|
});
|
|
await runtimeRecorder.flush();
|
|
|
|
const parsed = writes.map((line) => JSON.parse(line));
|
|
expect(parsed.map((event) => event.type)).toContain("trace.truncated");
|
|
const truncated = parsed.find((event) => event.type === "trace.truncated");
|
|
expect(truncated?.data).toMatchObject({
|
|
reason: "trajectory-runtime-file-size-limit",
|
|
limitBytes: 900,
|
|
});
|
|
expect(truncated?.data.droppedEvents).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("writes a session-adjacent pointer when using an override directory", () => {
|
|
const tmpDir = makeTempDir();
|
|
const sessionFile = path.join(tmpDir, "session.jsonl");
|
|
const trajectoryDir = path.join(tmpDir, "traces");
|
|
const recorder = createTrajectoryRuntimeRecorder({
|
|
env: { OPENCLAW_TRAJECTORY_DIR: trajectoryDir },
|
|
sessionId: "session-1",
|
|
sessionFile,
|
|
writer: {
|
|
filePath: path.join(trajectoryDir, "session-1.jsonl"),
|
|
write: () => undefined,
|
|
flush: async () => undefined,
|
|
},
|
|
});
|
|
|
|
expectTrajectoryRuntimeRecorder(recorder);
|
|
const pointer = JSON.parse(
|
|
fs.readFileSync(resolveTrajectoryPointerFilePath(sessionFile), "utf8"),
|
|
) as { runtimeFile?: string };
|
|
expect(pointer.runtimeFile).toBe(path.join(trajectoryDir, "session-1.jsonl"));
|
|
});
|
|
|
|
it("keeps pointer write flags usable when O_NOFOLLOW is unavailable", () => {
|
|
expect(
|
|
resolveTrajectoryPointerOpenFlags({
|
|
O_CREAT: 0x01,
|
|
O_TRUNC: 0x02,
|
|
O_WRONLY: 0x04,
|
|
}),
|
|
).toBe(0x07);
|
|
});
|
|
|
|
it("does not record runtime events when explicitly disabled", () => {
|
|
const recorder = createTrajectoryRuntimeRecorder({
|
|
env: {
|
|
OPENCLAW_TRAJECTORY: "0",
|
|
},
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:main:session-1",
|
|
sessionFile: "/tmp/session.jsonl",
|
|
writer: {
|
|
filePath: "/tmp/session.trajectory.jsonl",
|
|
write: () => undefined,
|
|
flush: async () => undefined,
|
|
},
|
|
});
|
|
|
|
expect(recorder).toBeNull();
|
|
});
|
|
});
|