openclaw / src /logging /diagnostic-session-context.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw History Blame Contribute Delete
9.02 kB
// Diagnostic session context tests cover session context capture for diagnostics.
import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
appendTranscriptMessageSync,
readLatestTranscriptAssistantText,
replaceSessionEntry,
} from "../config/sessions/session-accessor.js";
import { saveCronStore } from "../cron/store.js";
import {
createOpenClawTestState,
type OpenClawTestState,
} from "../test-utils/openclaw-test-state.js";
import {
formatCronSessionDiagnosticFields,
formatStoppedCronSessionDiagnosticFields,
resolveCronSessionDiagnosticContext,
} from "./diagnostic-session-context.js";
let tempDir: string | undefined;
let testState: OpenClawTestState | undefined;
async function seedSessionTranscript(params: {
agentId: string;
sessionId: string;
sessionKey: string;
messages: unknown[];
incognito?: boolean;
}) {
const { agentId, sessionId, sessionKey, messages, incognito } = params;
await replaceSessionEntry(
{ agentId, sessionKey },
{ sessionId, updatedAt: 1, ...(incognito ? { incognito: true } : {}) },
);
for (const message of messages) {
appendTranscriptMessageSync({ agentId, sessionId, sessionKey }, { message });
}
}
describe("diagnostic session context", () => {
beforeEach(async () => {
testState = await createOpenClawTestState({
layout: "state-only",
prefix: "openclaw-diagnostic-session-",
});
tempDir = testState.stateDir;
});
afterEach(async () => {
await testState?.cleanup();
testState = undefined;
tempDir = undefined;
});
it("parses cron run session keys", () => {
expect(
resolveCronSessionDiagnosticContext({
sessionKey: "agent:clawblocker:cron:job-123:run:run-456",
}),
).toMatchObject({ agentId: "clawblocker", cronJobId: "job-123", cronRunId: "run-456" });
});
it("formats cron job and last assistant context for stalled session logs", async () => {
const stateDir = tempDir!;
await saveCronStore(path.join(stateDir, "cron", "jobs.json"), {
version: 1,
jobs: [
{
id: "job-123",
name: "Twitter Mention Moderation Agent",
enabled: true,
createdAtMs: 1_700_000_000_000,
updatedAtMs: 1_700_000_000_000,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: { kind: "systemEvent", text: "tick" },
state: {},
},
],
});
const sessionKey = "agent:clawblocker:cron:job-123:run:run-456";
await seedSessionTranscript({
agentId: "clawblocker",
sessionId: "run-456",
sessionKey,
messages: [
{ role: "user", content: "run" },
{
role: "assistant",
content: [{ type: "text", text: "There are 40\ncached mentions ready." }],
},
],
});
const context = resolveCronSessionDiagnosticContext({
sessionKey,
activeSessionId: "run-456",
});
expect(formatCronSessionDiagnosticFields(context)).toContain("cronJobId=job-123");
expect(formatCronSessionDiagnosticFields(context)).toContain("cronRunId=run-456");
expect(formatCronSessionDiagnosticFields(context)).toContain(
'cronJob="Twitter Mention Moderation Agent"',
);
expect(formatCronSessionDiagnosticFields(context)).toContain(
'lastAssistant="There are 40 cached mentions ready."',
);
expect(formatStoppedCronSessionDiagnosticFields(context)).toContain(
'stopped="Twitter Mention Moderation Agent"',
);
});
it("reads the latest visible app-agent assistant text from its SQLite transcript", async () => {
const sessionKey = "agent:oauth-agent:main";
await seedSessionTranscript({
agentId: "oauth-agent",
sessionId: "oauth-session",
sessionKey,
messages: [
{ role: "assistant", content: "older reply" },
{ role: "user", content: "later user" },
{
role: "assistant",
content: [
{
type: "text",
text: "internal reasoning",
textSignature: JSON.stringify({ v: 1, id: "reasoning", phase: "commentary" }),
},
{
type: "text",
text: "latest visible reply",
textSignature: JSON.stringify({ v: 1, id: "reply", phase: "final_answer" }),
},
],
},
],
});
const context = resolveCronSessionDiagnosticContext({
sessionKey,
activeSessionId: "oauth-session",
});
expect(context.lastAssistant).toBe("latest visible reply");
expect(formatCronSessionDiagnosticFields(context)).toBe('lastAssistant="latest visible reply"');
});
it.each(["dashboard", "subagent", "internal-session-effects"])(
"never exposes a %s incognito assistant reply to durable diagnostics",
async (sessionSurface) => {
const sessionKey = `agent:main:${sessionSurface}:incognito-private`;
const sessionId = `incognito-${sessionSurface}`;
const privateReply = `memory-only ${sessionSurface} reply`;
await seedSessionTranscript({
agentId: "main",
incognito: true,
sessionId,
sessionKey,
messages: [{ role: "assistant", content: privateReply }],
});
expect(
readLatestTranscriptAssistantText({ agentId: "main", sessionId, sessionKey })?.text,
).toBe(privateReply);
expect(
resolveCronSessionDiagnosticContext({ sessionKey, activeSessionId: sessionId }),
).toEqual({});
expect(
fs.existsSync(path.join(tempDir!, "agents", "main", "agent", "openclaw-agent.sqlite")),
).toBe(false);
},
);
it("requires the authoritative current session id before reading transcript text", async () => {
const sessionKey = "agent:oauth-agent:main";
await seedSessionTranscript({
agentId: "oauth-agent",
sessionId: "current-session",
sessionKey,
messages: [{ role: "assistant", content: "current private reply" }],
});
expect(
resolveCronSessionDiagnosticContext({
sessionKey,
activeSessionId: "rotated-session",
}).lastAssistant,
).toBeUndefined();
expect(resolveCronSessionDiagnosticContext({ sessionKey }).lastAssistant).toBeUndefined();
});
it("keeps identical session ids isolated to their owning agents", async () => {
await seedSessionTranscript({
agentId: "main",
sessionId: "shared-session-id",
sessionKey: "agent:main:main",
messages: [{ role: "assistant", content: "main private reply" }],
});
await seedSessionTranscript({
agentId: "oauth-agent",
sessionId: "shared-session-id",
sessionKey: "agent:oauth-agent:main",
messages: [{ role: "assistant", content: "oauth private reply" }],
});
expect(
resolveCronSessionDiagnosticContext({
sessionKey: "agent:oauth-agent:main",
activeSessionId: "shared-session-id",
}).lastAssistant,
).toBe("oauth private reply");
});
it("rejects malformed agent ids before resolving a default agent transcript", async () => {
await seedSessionTranscript({
agentId: "main",
sessionId: "main-session",
sessionKey: "agent:main:main",
messages: [{ role: "assistant", content: "main private reply" }],
});
expect(
resolveCronSessionDiagnosticContext({
sessionKey: "agent:../main:main",
activeSessionId: "main-session",
}),
).toEqual({});
});
it("does not treat channel-owned cron segments as cron metadata", async () => {
const sessionKey = "agent:oauth-agent:slack:cron:job-123:run:run-456";
await seedSessionTranscript({
agentId: "oauth-agent",
sessionId: "slack-session",
sessionKey,
messages: [{ role: "assistant", content: "channel reply" }],
});
const context = resolveCronSessionDiagnosticContext({
sessionKey,
activeSessionId: "slack-session",
});
expect(context.lastAssistant).toBe("channel reply");
expect(context.cronJobId).toBeUndefined();
expect(context.cronRunId).toBeUndefined();
});
it("keeps bounded quoted fields UTF-16 safe", () => {
const prefix = "a".repeat(136);
expect(
formatCronSessionDiagnosticFields({ cronJobName: `${prefix}😀${"b".repeat(140)}` }),
).toBe(`cronJob="${prefix}..."`);
});
it("does not create an agent database when its session store is missing", () => {
const databasePath = path.join(
tempDir!,
"agents",
"missing-agent",
"agent",
"openclaw-agent.sqlite",
);
expect(fs.existsSync(databasePath)).toBe(false);
expect(
resolveCronSessionDiagnosticContext({
sessionKey: "agent:missing-agent:main",
activeSessionId: "missing",
}).lastAssistant,
).toBeUndefined();
expect(fs.existsSync(databasePath)).toBe(false);
});
});