mirror of https://github.com/openclaw/openclaw.git
refactor: isolate session transcript coverage
This commit is contained in:
parent
27a8ef1284
commit
24da2c39f3
|
|
@ -10,12 +10,13 @@ const hoisted = vi.hoisted(() => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock("../../config/sessions.js", () => ({
|
||||
vi.mock("../../config/sessions/store-load.js", () => ({
|
||||
loadSessionStore: (storePath: string) => hoisted.loadSessionStoreMock(storePath),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/sessions/targets.js", () => ({
|
||||
resolveAllAgentSessionStoreTargets: (cfg: OpenClawConfig, opts: unknown) =>
|
||||
hoisted.resolveAllAgentSessionStoreTargetsMock(cfg, opts),
|
||||
resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
|
||||
updateSessionStore: vi.fn(),
|
||||
}));
|
||||
let listAcpSessionEntries: typeof import("./session-meta.js").listAcpSessionEntries;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveAllAgentSessionStoreTargets,
|
||||
resolveStorePath,
|
||||
updateSessionStore,
|
||||
} from "../../config/sessions.js";
|
||||
import { resolveStorePath } from "../../config/sessions/paths.js";
|
||||
import { loadSessionStore } from "../../config/sessions/store-load.js";
|
||||
import { updateSessionStore } from "../../config/sessions/store.runtime.js";
|
||||
import { resolveAllAgentSessionStoreTargets } from "../../config/sessions/targets.js";
|
||||
import {
|
||||
mergeSessionEntry,
|
||||
type SessionAcpMeta,
|
||||
|
|
|
|||
|
|
@ -5,15 +5,7 @@ import path from "node:path";
|
|||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { upsertAcpSessionMeta } from "../../acp/runtime/session-meta.js";
|
||||
import * as jsonFiles from "../../infra/json-files.js";
|
||||
import * as transcriptEvents from "../../sessions/transcript-events.js";
|
||||
import type { OpenClawConfig } from "../config.js";
|
||||
import {
|
||||
clearSessionStoreCacheForTest,
|
||||
loadSessionStore,
|
||||
mergeSessionEntry,
|
||||
resolveAndPersistSessionFile,
|
||||
updateSessionStore,
|
||||
} from "../sessions.js";
|
||||
import type { SessionConfig } from "../types.base.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
|
|
@ -22,8 +14,9 @@ import {
|
|||
validateSessionId,
|
||||
} from "./paths.js";
|
||||
import { evaluateSessionFreshness, resolveSessionResetPolicy } from "./reset.js";
|
||||
import { appendAssistantMessageToSessionTranscript } from "./transcript.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
import { resolveAndPersistSessionFile } from "./session-file.js";
|
||||
import { clearSessionStoreCacheForTest, loadSessionStore, updateSessionStore } from "./store.js";
|
||||
import { mergeSessionEntry, type SessionEntry } from "./types.js";
|
||||
|
||||
function useTempSessionsFixture(prefix: string) {
|
||||
let tempDir = "";
|
||||
|
|
@ -379,204 +372,6 @@ describe("session store lock (Promise chain mutex)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("appendAssistantMessageToSessionTranscript", () => {
|
||||
const fixture = useTempSessionsFixture("transcript-test-");
|
||||
const sessionId = "test-session-id";
|
||||
const sessionKey = "test-session";
|
||||
|
||||
function writeTranscriptStore() {
|
||||
fs.writeFileSync(
|
||||
fixture.storePath(),
|
||||
JSON.stringify({
|
||||
[sessionKey]: {
|
||||
sessionId,
|
||||
chatType: "direct",
|
||||
channel: "discord",
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
it("creates transcript file and appends message for valid session", async () => {
|
||||
writeTranscriptStore();
|
||||
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(fs.existsSync(result.sessionFile)).toBe(true);
|
||||
const sessionFileMode = fs.statSync(result.sessionFile).mode & 0o777;
|
||||
if (process.platform !== "win32") {
|
||||
expect(sessionFileMode).toBe(0o600);
|
||||
}
|
||||
|
||||
const lines = fs.readFileSync(result.sessionFile, "utf-8").trim().split("\n");
|
||||
expect(lines.length).toBe(2);
|
||||
|
||||
const header = JSON.parse(lines[0]);
|
||||
expect(header.type).toBe("session");
|
||||
expect(header.id).toBe(sessionId);
|
||||
|
||||
const messageLine = JSON.parse(lines[1]);
|
||||
expect(messageLine.type).toBe("message");
|
||||
expect(messageLine.message.role).toBe("assistant");
|
||||
expect(messageLine.message.content[0].type).toBe("text");
|
||||
expect(messageLine.message.content[0].text).toBe("Hello from delivery mirror!");
|
||||
}
|
||||
});
|
||||
|
||||
it("emits transcript update events for delivery mirrors", async () => {
|
||||
const sessionId = "test-session-id";
|
||||
const sessionKey = "test-session";
|
||||
const store = {
|
||||
[sessionKey]: {
|
||||
sessionId,
|
||||
chatType: "direct",
|
||||
channel: "discord",
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
|
||||
const emitSpy = vi.spyOn(transcriptEvents, "emitSessionTranscriptUpdate");
|
||||
|
||||
await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionFile,
|
||||
sessionKey,
|
||||
messageId: expect.any(String),
|
||||
message: expect.objectContaining({
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: "Hello from delivery mirror!" }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
emitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not append a duplicate delivery mirror for the same idempotency key", async () => {
|
||||
writeTranscriptStore();
|
||||
|
||||
await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
|
||||
const lines = fs.readFileSync(sessionFile, "utf-8").trim().split("\n");
|
||||
expect(lines.length).toBe(2);
|
||||
|
||||
const messageLine = JSON.parse(lines[1]);
|
||||
expect(messageLine.message.idempotencyKey).toBe("mirror:test-source-message");
|
||||
expect(messageLine.message.content[0].text).toBe("Hello from delivery mirror!");
|
||||
});
|
||||
|
||||
it("finds session entry using normalized (lowercased) key", async () => {
|
||||
const sessionId = "test-session-normalized";
|
||||
// Store key is lowercase (as written by updateSessionStore/normalizeStoreSessionKey)
|
||||
const storeKey = "agent:main:bluebubbles:direct:+15551234567";
|
||||
const store = {
|
||||
[storeKey]: {
|
||||
sessionId,
|
||||
chatType: "direct",
|
||||
channel: "bluebubbles",
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
|
||||
|
||||
// Pass a mixed-case key — append should still find the entry via normalization
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey: "agent:main:BlueBubbles:direct:+15551234567",
|
||||
text: "Hello normalized!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("finds Slack session entry using normalized (lowercased) key", async () => {
|
||||
const sessionId = "test-slack-session";
|
||||
// Slack session keys include channel type and target ID; store key is lowercase
|
||||
const storeKey = "agent:main:slack:direct:u12345abc";
|
||||
const store = {
|
||||
[storeKey]: {
|
||||
sessionId,
|
||||
chatType: "direct",
|
||||
channel: "slack",
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
|
||||
|
||||
// Pass a mixed-case key (as resolveSlackSession might produce) — normalization should match
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey: "agent:main:slack:direct:U12345ABC",
|
||||
text: "Hello Slack user!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores malformed transcript lines when checking mirror idempotency", async () => {
|
||||
writeTranscriptStore();
|
||||
|
||||
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
|
||||
fs.writeFileSync(
|
||||
sessionFile,
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "session",
|
||||
version: 1,
|
||||
id: sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
}),
|
||||
"{not-json",
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: {
|
||||
role: "assistant",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
content: [{ type: "text", text: "Hello from delivery mirror!" }],
|
||||
},
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const lines = fs.readFileSync(sessionFile, "utf-8").trim().split("\n");
|
||||
expect(lines.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAndPersistSessionFile", () => {
|
||||
const fixture = useTempSessionsFixture("session-file-test-");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as transcriptEvents from "../../sessions/transcript-events.js";
|
||||
import { resolveSessionTranscriptPathInDir } from "./paths.js";
|
||||
import { appendAssistantMessageToSessionTranscript } from "./transcript.js";
|
||||
|
||||
function useTempSessionsFixture(prefix: string) {
|
||||
let tempDir = "";
|
||||
let storePath = "";
|
||||
let sessionsDir = "";
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
sessionsDir = path.join(tempDir, "agents", "main", "sessions");
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
storePath = path.join(sessionsDir, "sessions.json");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
return {
|
||||
storePath: () => storePath,
|
||||
sessionsDir: () => sessionsDir,
|
||||
};
|
||||
}
|
||||
|
||||
describe("appendAssistantMessageToSessionTranscript", () => {
|
||||
const fixture = useTempSessionsFixture("transcript-test-");
|
||||
const sessionId = "test-session-id";
|
||||
const sessionKey = "test-session";
|
||||
|
||||
function writeTranscriptStore() {
|
||||
fs.writeFileSync(
|
||||
fixture.storePath(),
|
||||
JSON.stringify({
|
||||
[sessionKey]: {
|
||||
sessionId,
|
||||
chatType: "direct",
|
||||
channel: "discord",
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
it("creates transcript file and appends message for valid session", async () => {
|
||||
writeTranscriptStore();
|
||||
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(fs.existsSync(result.sessionFile)).toBe(true);
|
||||
const sessionFileMode = fs.statSync(result.sessionFile).mode & 0o777;
|
||||
if (process.platform !== "win32") {
|
||||
expect(sessionFileMode).toBe(0o600);
|
||||
}
|
||||
|
||||
const lines = fs.readFileSync(result.sessionFile, "utf-8").trim().split("\n");
|
||||
expect(lines.length).toBe(2);
|
||||
|
||||
const header = JSON.parse(lines[0]);
|
||||
expect(header.type).toBe("session");
|
||||
expect(header.id).toBe(sessionId);
|
||||
|
||||
const messageLine = JSON.parse(lines[1]);
|
||||
expect(messageLine.type).toBe("message");
|
||||
expect(messageLine.message.role).toBe("assistant");
|
||||
expect(messageLine.message.content[0].type).toBe("text");
|
||||
expect(messageLine.message.content[0].text).toBe("Hello from delivery mirror!");
|
||||
}
|
||||
});
|
||||
|
||||
it("emits transcript update events for delivery mirrors", async () => {
|
||||
const store = {
|
||||
[sessionKey]: {
|
||||
sessionId,
|
||||
chatType: "direct",
|
||||
channel: "discord",
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
|
||||
const emitSpy = vi.spyOn(transcriptEvents, "emitSessionTranscriptUpdate");
|
||||
|
||||
await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
|
||||
expect(emitSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionFile,
|
||||
sessionKey,
|
||||
messageId: expect.any(String),
|
||||
message: expect.objectContaining({
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: "Hello from delivery mirror!" }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
emitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not append a duplicate delivery mirror for the same idempotency key", async () => {
|
||||
writeTranscriptStore();
|
||||
|
||||
await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
|
||||
const lines = fs.readFileSync(sessionFile, "utf-8").trim().split("\n");
|
||||
expect(lines.length).toBe(2);
|
||||
|
||||
const messageLine = JSON.parse(lines[1]);
|
||||
expect(messageLine.message.idempotencyKey).toBe("mirror:test-source-message");
|
||||
expect(messageLine.message.content[0].text).toBe("Hello from delivery mirror!");
|
||||
});
|
||||
|
||||
it("finds session entry using normalized (lowercased) key", async () => {
|
||||
const storeKey = "agent:main:bluebubbles:direct:+15551234567";
|
||||
const store = {
|
||||
[storeKey]: {
|
||||
sessionId: "test-session-normalized",
|
||||
chatType: "direct",
|
||||
channel: "bluebubbles",
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
|
||||
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey: "agent:main:BlueBubbles:direct:+15551234567",
|
||||
text: "Hello normalized!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("finds Slack session entry using normalized (lowercased) key", async () => {
|
||||
const storeKey = "agent:main:slack:direct:u12345abc";
|
||||
const store = {
|
||||
[storeKey]: {
|
||||
sessionId: "test-slack-session",
|
||||
chatType: "direct",
|
||||
channel: "slack",
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
|
||||
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey: "agent:main:slack:direct:U12345ABC",
|
||||
text: "Hello Slack user!",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores malformed transcript lines when checking mirror idempotency", async () => {
|
||||
writeTranscriptStore();
|
||||
|
||||
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
|
||||
fs.writeFileSync(
|
||||
sessionFile,
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "session",
|
||||
version: 1,
|
||||
id: sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
}),
|
||||
"{not-json",
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: {
|
||||
role: "assistant",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
content: [{ type: "text", text: "Hello from delivery mirror!" }],
|
||||
},
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const result = await appendAssistantMessageToSessionTranscript({
|
||||
sessionKey,
|
||||
text: "Hello from delivery mirror!",
|
||||
idempotencyKey: "mirror:test-source-message",
|
||||
storePath: fixture.storePath(),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const lines = fs.readFileSync(sessionFile, "utf-8").trim().split("\n");
|
||||
expect(lines.length).toBe(3);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue