Files
openclaw/src/trajectory/runtime.test.ts
scoootscooob a3d9c53db2 feat: add trajectory bundle export and default-on runtime capture (#70291)
* Trajectory: export session bundles by default

* Harden trajectory export diagnostics integration

* Address trajectory export review feedback

* Share diagnostics and trajectory bundle plumbing

* Harden trajectory recording and export

* Confine trajectory export outputs

* Document trajectory export command

* Harden trajectory export bundle privacy

* Redact trajectory sidecar paths

* Fix plugin install checks after rebase

* Keep queued writers working without O_NOFOLLOW

* Keep Codex trajectory writes without O_NOFOLLOW

* Harden trajectory export path handling

* Redact mixed trajectory export paths
2026-04-22 23:29:01 -07:00

170 lines
5.2 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";
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 });
}
});
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,
},
});
expect(recorder).not.toBeNull();
recorder?.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("truncates events that exceed the runtime event byte limit", () => {
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,
},
});
recorder?.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).toMatchObject({
truncated: true,
reason: "trajectory-event-size-limit",
});
expect(Buffer.byteLength(writes[0], "utf8")).toBeLessThanOrEqual(
TRAJECTORY_RUNTIME_EVENT_MAX_BYTES + 1,
);
});
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,
},
});
expect(recorder).not.toBeNull();
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();
});
});