mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
test: dedupe gateway network and transcript suites
This commit is contained in:
@@ -14,16 +14,13 @@ import {
|
||||
} from "./net.js";
|
||||
|
||||
describe("resolveHostName", () => {
|
||||
it("normalizes IPv4/hostname and IPv6 host forms", () => {
|
||||
const cases = [
|
||||
{ input: "localhost:18789", expected: "localhost" },
|
||||
{ input: "127.0.0.1:18789", expected: "127.0.0.1" },
|
||||
{ input: "[::1]:18789", expected: "::1" },
|
||||
{ input: "::1", expected: "::1" },
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
expect(resolveHostName(testCase.input), testCase.input).toBe(testCase.expected);
|
||||
}
|
||||
it.each([
|
||||
{ input: "localhost:18789", expected: "localhost" },
|
||||
{ input: "127.0.0.1:18789", expected: "127.0.0.1" },
|
||||
{ input: "[::1]:18789", expected: "::1" },
|
||||
{ input: "::1", expected: "::1" },
|
||||
] as const)("normalizes host form for $input", ({ input, expected }) => {
|
||||
expect(resolveHostName(input), input).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,36 +277,32 @@ describe("resolveClientIp", () => {
|
||||
});
|
||||
|
||||
describe("resolveGatewayListenHosts", () => {
|
||||
it("resolves listen hosts for non-loopback and loopback variants", async () => {
|
||||
const cases = [
|
||||
{
|
||||
name: "non-loopback host passthrough",
|
||||
host: "0.0.0.0",
|
||||
canBindToHost: async () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
expected: ["0.0.0.0"],
|
||||
it.each([
|
||||
{
|
||||
name: "non-loopback host passthrough",
|
||||
host: "0.0.0.0",
|
||||
canBindToHost: async () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
{
|
||||
name: "loopback with IPv6 available",
|
||||
host: "127.0.0.1",
|
||||
canBindToHost: async () => true,
|
||||
expected: ["127.0.0.1", "::1"],
|
||||
},
|
||||
{
|
||||
name: "loopback with IPv6 unavailable",
|
||||
host: "127.0.0.1",
|
||||
canBindToHost: async () => false,
|
||||
expected: ["127.0.0.1"],
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
const hosts = await resolveGatewayListenHosts(testCase.host, {
|
||||
canBindToHost: testCase.canBindToHost,
|
||||
});
|
||||
expect(hosts, testCase.name).toEqual(testCase.expected);
|
||||
}
|
||||
expected: ["0.0.0.0"],
|
||||
},
|
||||
{
|
||||
name: "loopback with IPv6 available",
|
||||
host: "127.0.0.1",
|
||||
canBindToHost: async () => true,
|
||||
expected: ["127.0.0.1", "::1"],
|
||||
},
|
||||
{
|
||||
name: "loopback with IPv6 unavailable",
|
||||
host: "127.0.0.1",
|
||||
canBindToHost: async () => false,
|
||||
expected: ["127.0.0.1"],
|
||||
},
|
||||
] as const)("resolves listen hosts: $name", async ({ host, canBindToHost, expected }) => {
|
||||
const hosts = await resolveGatewayListenHosts(host, {
|
||||
canBindToHost,
|
||||
});
|
||||
expect(hosts).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,47 +311,45 @@ describe("pickPrimaryLanIPv4", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("prefers en0, then eth0, then any non-internal IPv4, otherwise undefined", () => {
|
||||
const cases = [
|
||||
{
|
||||
name: "prefers en0",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo0: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
en0: [{ address: "192.168.1.42", family: "IPv4" }],
|
||||
}),
|
||||
expected: "192.168.1.42",
|
||||
},
|
||||
{
|
||||
name: "falls back to eth0",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
eth0: [{ address: "10.0.0.5", family: "IPv4" }],
|
||||
}),
|
||||
expected: "10.0.0.5",
|
||||
},
|
||||
{
|
||||
name: "falls back to any non-internal interface",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
wlan0: [{ address: "172.16.0.99", family: "IPv4" }],
|
||||
}),
|
||||
expected: "172.16.0.99",
|
||||
},
|
||||
{
|
||||
name: "no non-internal interface",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
}),
|
||||
expected: undefined,
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
vi.spyOn(os, "networkInterfaces").mockReturnValue(testCase.interfaces);
|
||||
expect(pickPrimaryLanIPv4(), testCase.name).toBe(testCase.expected);
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
name: "prefers en0",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo0: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
en0: [{ address: "192.168.1.42", family: "IPv4" }],
|
||||
}),
|
||||
expected: "192.168.1.42",
|
||||
},
|
||||
{
|
||||
name: "falls back to eth0",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
eth0: [{ address: "10.0.0.5", family: "IPv4" }],
|
||||
}),
|
||||
expected: "10.0.0.5",
|
||||
},
|
||||
{
|
||||
name: "falls back to any non-internal interface",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
wlan0: [{ address: "172.16.0.99", family: "IPv4" }],
|
||||
}),
|
||||
expected: "172.16.0.99",
|
||||
},
|
||||
{
|
||||
name: "no non-internal interface",
|
||||
interfaces: makeNetworkInterfacesSnapshot({
|
||||
lo: [{ address: "127.0.0.1", family: "IPv4", internal: true }],
|
||||
}),
|
||||
expected: undefined,
|
||||
},
|
||||
] as const)(
|
||||
"prefers en0, then eth0, then any non-internal IPv4: $name",
|
||||
({ interfaces, expected }) => {
|
||||
vi.spyOn(os, "networkInterfaces").mockReturnValue(interfaces);
|
||||
expect(pickPrimaryLanIPv4()).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("throws when interface discovery throws", () => {
|
||||
vi.spyOn(os, "networkInterfaces").mockImplementation(() => {
|
||||
@@ -456,48 +447,44 @@ describe("isPrivateOrLoopbackHost", () => {
|
||||
});
|
||||
|
||||
describe("isSecureWebSocketUrl", () => {
|
||||
it("defaults to loopback-only ws:// and rejects private/public remote ws://", () => {
|
||||
const cases = [
|
||||
// wss:// always accepted
|
||||
{ input: "wss://127.0.0.1:18789", expected: true },
|
||||
{ input: "wss://localhost:18789", expected: true },
|
||||
{ input: "wss://remote.example.com:18789", expected: true },
|
||||
{ input: "wss://192.168.1.100:18789", expected: true },
|
||||
// ws:// loopback accepted
|
||||
{ input: "ws://127.0.0.1:18789", expected: true },
|
||||
{ input: "ws://localhost:18789", expected: true },
|
||||
{ input: "ws://[::1]:18789", expected: true },
|
||||
{ input: "ws://127.0.0.42:18789", expected: true },
|
||||
// ws:// private/public remote addresses rejected by default
|
||||
{ input: "ws://10.0.0.5:18789", expected: false },
|
||||
{ input: "ws://10.42.1.100:18789", expected: false },
|
||||
{ input: "ws://172.16.0.1:18789", expected: false },
|
||||
{ input: "ws://172.31.255.254:18789", expected: false },
|
||||
{ input: "ws://192.168.1.100:18789", expected: false },
|
||||
{ input: "ws://169.254.10.20:18789", expected: false },
|
||||
{ input: "ws://100.64.0.1:18789", expected: false },
|
||||
{ input: "ws://[fc00::1]:18789", expected: false },
|
||||
{ input: "ws://[fd12:3456:789a::1]:18789", expected: false },
|
||||
{ input: "ws://[fe80::1]:18789", expected: false },
|
||||
{ input: "ws://[::]:18789", expected: false },
|
||||
{ input: "ws://[ff02::1]:18789", expected: false },
|
||||
// ws:// public addresses rejected
|
||||
{ input: "ws://remote.example.com:18789", expected: false },
|
||||
{ input: "ws://1.1.1.1:18789", expected: false },
|
||||
{ input: "ws://8.8.8.8:18789", expected: false },
|
||||
{ input: "ws://203.0.113.10:18789", expected: false },
|
||||
// invalid URLs
|
||||
{ input: "not-a-url", expected: false },
|
||||
{ input: "", expected: false },
|
||||
{ input: "http://127.0.0.1:18789", expected: true },
|
||||
{ input: "https://127.0.0.1:18789", expected: true },
|
||||
{ input: "https://remote.example.com:18789", expected: true },
|
||||
{ input: "http://remote.example.com:18789", expected: false },
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
expect(isSecureWebSocketUrl(testCase.input), testCase.input).toBe(testCase.expected);
|
||||
}
|
||||
it.each([
|
||||
// wss:// always accepted
|
||||
{ input: "wss://127.0.0.1:18789", expected: true },
|
||||
{ input: "wss://localhost:18789", expected: true },
|
||||
{ input: "wss://remote.example.com:18789", expected: true },
|
||||
{ input: "wss://192.168.1.100:18789", expected: true },
|
||||
// ws:// loopback accepted
|
||||
{ input: "ws://127.0.0.1:18789", expected: true },
|
||||
{ input: "ws://localhost:18789", expected: true },
|
||||
{ input: "ws://[::1]:18789", expected: true },
|
||||
{ input: "ws://127.0.0.42:18789", expected: true },
|
||||
// ws:// private/public remote addresses rejected by default
|
||||
{ input: "ws://10.0.0.5:18789", expected: false },
|
||||
{ input: "ws://10.42.1.100:18789", expected: false },
|
||||
{ input: "ws://172.16.0.1:18789", expected: false },
|
||||
{ input: "ws://172.31.255.254:18789", expected: false },
|
||||
{ input: "ws://192.168.1.100:18789", expected: false },
|
||||
{ input: "ws://169.254.10.20:18789", expected: false },
|
||||
{ input: "ws://100.64.0.1:18789", expected: false },
|
||||
{ input: "ws://[fc00::1]:18789", expected: false },
|
||||
{ input: "ws://[fd12:3456:789a::1]:18789", expected: false },
|
||||
{ input: "ws://[fe80::1]:18789", expected: false },
|
||||
{ input: "ws://[::]:18789", expected: false },
|
||||
{ input: "ws://[ff02::1]:18789", expected: false },
|
||||
// ws:// public addresses rejected
|
||||
{ input: "ws://remote.example.com:18789", expected: false },
|
||||
{ input: "ws://1.1.1.1:18789", expected: false },
|
||||
{ input: "ws://8.8.8.8:18789", expected: false },
|
||||
{ input: "ws://203.0.113.10:18789", expected: false },
|
||||
// invalid URLs
|
||||
{ input: "not-a-url", expected: false },
|
||||
{ input: "", expected: false },
|
||||
{ input: "http://127.0.0.1:18789", expected: true },
|
||||
{ input: "https://127.0.0.1:18789", expected: true },
|
||||
{ input: "https://remote.example.com:18789", expected: true },
|
||||
{ input: "http://remote.example.com:18789", expected: false },
|
||||
] as const)("defaults secure websocket behavior for $input", ({ input, expected }) => {
|
||||
expect(isSecureWebSocketUrl(input), input).toBe(expected);
|
||||
});
|
||||
|
||||
it("allows private ws:// only when opt-in is enabled", () => {
|
||||
|
||||
@@ -57,51 +57,47 @@ describe("readFirstUserMessageFromTranscript", () => {
|
||||
storePath = nextStorePath;
|
||||
});
|
||||
|
||||
test("extracts first user text across supported content formats", () => {
|
||||
const cases = [
|
||||
{
|
||||
sessionId: "test-session-1",
|
||||
lines: [
|
||||
JSON.stringify({ type: "session", version: 1, id: "test-session-1" }),
|
||||
JSON.stringify({ message: { role: "user", content: "Hello world" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Hi there" } }),
|
||||
],
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
sessionId: "test-session-2",
|
||||
lines: [
|
||||
JSON.stringify({ type: "session", version: 1, id: "test-session-2" }),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Array message content" }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
expected: "Array message content",
|
||||
},
|
||||
{
|
||||
sessionId: "test-session-2b",
|
||||
lines: [
|
||||
JSON.stringify({ type: "session", version: 1, id: "test-session-2b" }),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Input text content" }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
expected: "Input text content",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
const transcriptPath = path.join(tmpDir, `${testCase.sessionId}.jsonl`);
|
||||
fs.writeFileSync(transcriptPath, testCase.lines.join("\n"), "utf-8");
|
||||
const result = readFirstUserMessageFromTranscript(testCase.sessionId, storePath);
|
||||
expect(result, testCase.sessionId).toBe(testCase.expected);
|
||||
}
|
||||
test.each([
|
||||
{
|
||||
sessionId: "test-session-1",
|
||||
lines: [
|
||||
JSON.stringify({ type: "session", version: 1, id: "test-session-1" }),
|
||||
JSON.stringify({ message: { role: "user", content: "Hello world" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Hi there" } }),
|
||||
],
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
sessionId: "test-session-2",
|
||||
lines: [
|
||||
JSON.stringify({ type: "session", version: 1, id: "test-session-2" }),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Array message content" }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
expected: "Array message content",
|
||||
},
|
||||
{
|
||||
sessionId: "test-session-2b",
|
||||
lines: [
|
||||
JSON.stringify({ type: "session", version: 1, id: "test-session-2b" }),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: "Input text content" }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
expected: "Input text content",
|
||||
},
|
||||
] as const)("extracts first user text for $sessionId", ({ sessionId, lines, expected }) => {
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
|
||||
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
|
||||
expect(result, sessionId).toBe(expected);
|
||||
});
|
||||
test("skips non-user messages to find first user message", () => {
|
||||
const sessionId = "test-session-3";
|
||||
@@ -198,34 +194,33 @@ describe("readLastMessagePreviewFromTranscript", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("returns the last user or assistant message from transcript", () => {
|
||||
const cases = [
|
||||
{
|
||||
sessionId: "test-last-user",
|
||||
lines: [
|
||||
JSON.stringify({ message: { role: "user", content: "First user" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "First assistant" } }),
|
||||
JSON.stringify({ message: { role: "user", content: "Last user message" } }),
|
||||
],
|
||||
expected: "Last user message",
|
||||
},
|
||||
{
|
||||
sessionId: "test-last-assistant",
|
||||
lines: [
|
||||
JSON.stringify({ message: { role: "user", content: "User question" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Final assistant reply" } }),
|
||||
],
|
||||
expected: "Final assistant reply",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
const transcriptPath = path.join(tmpDir, `${testCase.sessionId}.jsonl`);
|
||||
fs.writeFileSync(transcriptPath, testCase.lines.join("\n"), "utf-8");
|
||||
const result = readLastMessagePreviewFromTranscript(testCase.sessionId, storePath);
|
||||
expect(result).toBe(testCase.expected);
|
||||
}
|
||||
});
|
||||
test.each([
|
||||
{
|
||||
sessionId: "test-last-user",
|
||||
lines: [
|
||||
JSON.stringify({ message: { role: "user", content: "First user" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "First assistant" } }),
|
||||
JSON.stringify({ message: { role: "user", content: "Last user message" } }),
|
||||
],
|
||||
expected: "Last user message",
|
||||
},
|
||||
{
|
||||
sessionId: "test-last-assistant",
|
||||
lines: [
|
||||
JSON.stringify({ message: { role: "user", content: "User question" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: "Final assistant reply" } }),
|
||||
],
|
||||
expected: "Final assistant reply",
|
||||
},
|
||||
] as const)(
|
||||
"returns the last user or assistant message from transcript for $sessionId",
|
||||
({ sessionId, lines, expected }) => {
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
|
||||
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
|
||||
expect(result).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
test("skips system messages to find last user/assistant", () => {
|
||||
const sessionId = "test-last-skip-system";
|
||||
@@ -266,32 +261,32 @@ describe("readLastMessagePreviewFromTranscript", () => {
|
||||
expect(result).toBe("Valid first");
|
||||
});
|
||||
|
||||
test("handles array/output_text content formats", () => {
|
||||
const cases = [
|
||||
{
|
||||
sessionId: "test-last-array",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Array content response" }],
|
||||
},
|
||||
expected: "Array content response",
|
||||
test.each([
|
||||
{
|
||||
sessionId: "test-last-array",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Array content response" }],
|
||||
},
|
||||
{
|
||||
sessionId: "test-last-output-text",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "Output text response" }],
|
||||
},
|
||||
expected: "Output text response",
|
||||
expected: "Array content response",
|
||||
},
|
||||
{
|
||||
sessionId: "test-last-output-text",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "Output text response" }],
|
||||
},
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
const transcriptPath = path.join(tmpDir, `${testCase.sessionId}.jsonl`);
|
||||
fs.writeFileSync(transcriptPath, JSON.stringify({ message: testCase.message }), "utf-8");
|
||||
const result = readLastMessagePreviewFromTranscript(testCase.sessionId, storePath);
|
||||
expect(result, testCase.sessionId).toBe(testCase.expected);
|
||||
}
|
||||
});
|
||||
expected: "Output text response",
|
||||
},
|
||||
] as const)(
|
||||
"handles array/output_text content format for $sessionId",
|
||||
({ sessionId, message, expected }) => {
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
fs.writeFileSync(transcriptPath, JSON.stringify({ message }), "utf-8");
|
||||
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
|
||||
expect(result, sessionId).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
test("skips empty content to find previous message", () => {
|
||||
const sessionId = "test-last-skip-empty";
|
||||
@@ -506,56 +501,40 @@ describe("readSessionMessages", () => {
|
||||
expect(typeof marker.timestamp).toBe("number");
|
||||
});
|
||||
|
||||
test("reads cross-agent absolute sessionFile across store-root layouts", () => {
|
||||
const cases = [
|
||||
{
|
||||
sessionId: "cross-agent-default-root",
|
||||
sessionFile: path.join(
|
||||
tmpDir,
|
||||
"agents",
|
||||
"ops",
|
||||
"sessions",
|
||||
"cross-agent-default-root.jsonl",
|
||||
),
|
||||
wrongStorePath: path.join(tmpDir, "agents", "main", "sessions", "sessions.json"),
|
||||
message: { role: "user", content: "from-ops" },
|
||||
},
|
||||
{
|
||||
sessionId: "cross-agent-custom-root",
|
||||
sessionFile: path.join(
|
||||
tmpDir,
|
||||
"custom",
|
||||
"agents",
|
||||
"ops",
|
||||
"sessions",
|
||||
"cross-agent-custom-root.jsonl",
|
||||
),
|
||||
wrongStorePath: path.join(tmpDir, "custom", "agents", "main", "sessions", "sessions.json"),
|
||||
message: { role: "assistant", content: "from-custom-ops" },
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
fs.mkdirSync(path.dirname(testCase.sessionFile), { recursive: true });
|
||||
test.each([
|
||||
{
|
||||
sessionId: "cross-agent-default-root",
|
||||
sessionFileParts: ["agents", "ops", "sessions", "cross-agent-default-root.jsonl"],
|
||||
wrongStorePathParts: ["agents", "main", "sessions", "sessions.json"],
|
||||
message: { role: "user", content: "from-ops" },
|
||||
},
|
||||
{
|
||||
sessionId: "cross-agent-custom-root",
|
||||
sessionFileParts: ["custom", "agents", "ops", "sessions", "cross-agent-custom-root.jsonl"],
|
||||
wrongStorePathParts: ["custom", "agents", "main", "sessions", "sessions.json"],
|
||||
message: { role: "assistant", content: "from-custom-ops" },
|
||||
},
|
||||
] as const)(
|
||||
"reads cross-agent absolute sessionFile across store-root layouts for $sessionId",
|
||||
({ sessionId, sessionFileParts, wrongStorePathParts, message }) => {
|
||||
const sessionFile = path.join(tmpDir, ...sessionFileParts);
|
||||
const wrongStorePath = path.join(tmpDir, ...wrongStorePathParts);
|
||||
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
testCase.sessionFile,
|
||||
sessionFile,
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: testCase.sessionId }),
|
||||
JSON.stringify({ message: testCase.message }),
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({ message }),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const out = readSessionMessages(
|
||||
testCase.sessionId,
|
||||
testCase.wrongStorePath,
|
||||
testCase.sessionFile,
|
||||
);
|
||||
const out = readSessionMessages(sessionId, wrongStorePath, sessionFile);
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0]).toMatchObject(testCase.message);
|
||||
expect(out[0]).toMatchObject(message);
|
||||
expect((out[0] as { __openclaw?: { seq?: number } }).__openclaw?.seq).toBe(1);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("readSessionPreviewItemsFromTranscript", () => {
|
||||
@@ -819,29 +798,22 @@ describe("resolveSessionTranscriptCandidates", () => {
|
||||
});
|
||||
|
||||
describe("resolveSessionTranscriptCandidates safety", () => {
|
||||
test("keeps cross-agent absolute sessionFile for standard and custom store roots", () => {
|
||||
const cases = [
|
||||
{
|
||||
storePath: "/tmp/openclaw/agents/main/sessions/sessions.json",
|
||||
sessionFile: "/tmp/openclaw/agents/ops/sessions/sess-safe.jsonl",
|
||||
},
|
||||
{
|
||||
storePath: "/srv/custom/agents/main/sessions/sessions.json",
|
||||
sessionFile: "/srv/custom/agents/ops/sessions/sess-safe.jsonl",
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
const candidates = resolveSessionTranscriptCandidates(
|
||||
"sess-safe",
|
||||
testCase.storePath,
|
||||
testCase.sessionFile,
|
||||
);
|
||||
expect(candidates.map((value) => path.resolve(value))).toContain(
|
||||
path.resolve(testCase.sessionFile),
|
||||
);
|
||||
}
|
||||
});
|
||||
test.each([
|
||||
{
|
||||
storePath: "/tmp/openclaw/agents/main/sessions/sessions.json",
|
||||
sessionFile: "/tmp/openclaw/agents/ops/sessions/sess-safe.jsonl",
|
||||
},
|
||||
{
|
||||
storePath: "/srv/custom/agents/main/sessions/sessions.json",
|
||||
sessionFile: "/srv/custom/agents/ops/sessions/sess-safe.jsonl",
|
||||
},
|
||||
] as const)(
|
||||
"keeps cross-agent absolute sessionFile candidate for $storePath",
|
||||
({ storePath, sessionFile }) => {
|
||||
const candidates = resolveSessionTranscriptCandidates("sess-safe", storePath, sessionFile);
|
||||
expect(candidates.map((value) => path.resolve(value))).toContain(path.resolve(sessionFile));
|
||||
},
|
||||
);
|
||||
|
||||
test("drops unsafe session IDs instead of producing traversal paths", () => {
|
||||
const candidates = resolveSessionTranscriptCandidates(
|
||||
@@ -956,34 +928,35 @@ describe("archiveSessionTranscripts", () => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
test("archives transcript from default and explicit sessionFile paths", () => {
|
||||
const cases = [
|
||||
{
|
||||
sessionId: "sess-archive-1",
|
||||
transcriptPath: path.join(tmpDir, "sess-archive-1.jsonl"),
|
||||
args: { sessionId: "sess-archive-1", storePath, reason: "reset" as const },
|
||||
},
|
||||
{
|
||||
test.each([
|
||||
{
|
||||
sessionId: "sess-archive-1",
|
||||
transcriptFileName: "sess-archive-1.jsonl",
|
||||
buildArgs: () => ({ sessionId: "sess-archive-1", storePath, reason: "reset" as const }),
|
||||
},
|
||||
{
|
||||
sessionId: "sess-archive-2",
|
||||
transcriptFileName: "custom-transcript.jsonl",
|
||||
buildArgs: () => ({
|
||||
sessionId: "sess-archive-2",
|
||||
transcriptPath: path.join(tmpDir, "custom-transcript.jsonl"),
|
||||
args: {
|
||||
sessionId: "sess-archive-2",
|
||||
storePath: undefined,
|
||||
sessionFile: path.join(tmpDir, "custom-transcript.jsonl"),
|
||||
reason: "reset" as const,
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
fs.writeFileSync(testCase.transcriptPath, '{"type":"session"}\n', "utf-8");
|
||||
const archived = archiveSessionTranscripts(testCase.args);
|
||||
storePath: undefined,
|
||||
sessionFile: path.join(tmpDir, "custom-transcript.jsonl"),
|
||||
reason: "reset" as const,
|
||||
}),
|
||||
},
|
||||
] as const)(
|
||||
"archives transcript from default and explicit sessionFile path for $sessionId",
|
||||
({ transcriptFileName, buildArgs }) => {
|
||||
const transcriptPath = path.join(tmpDir, transcriptFileName);
|
||||
const args = buildArgs();
|
||||
fs.writeFileSync(transcriptPath, '{"type":"session"}\n', "utf-8");
|
||||
const archived = archiveSessionTranscripts(args);
|
||||
expect(archived).toHaveLength(1);
|
||||
expect(archived[0]).toContain(".reset.");
|
||||
expect(fs.existsSync(testCase.transcriptPath)).toBe(false);
|
||||
expect(fs.existsSync(transcriptPath)).toBe(false);
|
||||
expect(fs.existsSync(archived[0])).toBe(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("returns empty array when no transcript files exist", () => {
|
||||
const archived = archiveSessionTranscripts({
|
||||
|
||||
Reference in New Issue
Block a user