test(qa): add session transcript primary coverage (#118820)

* test(qa): prove session transcript CLI management

* test(qa): register SQLite transcript durability proof
This commit is contained in:
Vincent Koc
2026-08-04 02:41:09 +08:00
committed by GitHub
parent 2b3351a743
commit 592dbf74d7
3 changed files with 258 additions and 0 deletions
@@ -0,0 +1,30 @@
title: Session and transcript CLI management
scenario:
id: session-transcript-cli-management
surface: runtime
category: session-memory.cli-session-and-transcript-management
coverage:
primary:
- session-memory.cli-session
- session-memory.transcript-management
objective: Prove the real child CLI reads canonical SQLite session state, delegates applied cleanup through a live Gateway, and materializes transcript artifacts from shared SQLite state.
successCriteria:
- The JSON sessions command observes a canonically persisted session.
- Applied missing-transcript cleanup delegates through the live Gateway and removes the targeted session.
- Transcript list and show commands read a stored transcript session as structured JSON.
- Transcript path materializes the JSONL artifact and its persisted utterance.
docsRefs:
- docs/cli/sessions.md
- docs/cli/transcripts.md
codeRefs:
- src/commands/sessions.ts
- src/commands/sessions-cleanup.ts
- src/gateway/server-methods/sessions-read.ts
- src/cli/program/register.transcripts.ts
- src/transcripts/store.ts
- test/e2e/qa-lab/runtime/session-transcript-cli-product-proof.e2e.test.ts
execution:
kind: vitest
path: test/e2e/qa-lab/runtime/session-transcript-cli-product-proof.e2e.test.ts
summary: Run the real child CLI against isolated canonical SQLite state and a live Gateway cleanup boundary.
@@ -0,0 +1,28 @@
title: SQLite session and transcript durability
scenario:
id: session-transcript-sqlite-durability
surface: runtime
category: session-memory.transcript-persistence
coverage:
primary:
- session-memory.transcript-persistence
- session-memory.durability
objective: Prove canonical SQLite session and transcript state survives migration, rollback restore, restart, contention, concurrent clients, and destructive lifecycle operations.
successCriteria:
- Startup import and doctor inspect, validate, rollback restore, and idempotence checkpoints preserve canonical SQLite state.
- Gateway restart and post-restart append retain the expected transcript history.
- SQLite busy contention and concurrent clients commit consistent session and transcript events.
- Cleanup, reset, and delete checkpoints remove targeted state while required transcript archives remain preserved.
docsRefs:
- docs/concepts/session.md
- docs/reference/session-management-compaction.md
codeRefs:
- src/config/sessions/session-accessor.ts
- src/config/sessions/cleanup-service.ts
- test/helpers/sqlite-sessions-transcripts-flip-proof.ts
- test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts
execution:
kind: vitest
path: test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts
summary: Run the existing isolated Gateway lifecycle proof across migration, restart, append, contention, cleanup, reset, delete, archive, and concurrent-client checkpoints.
@@ -0,0 +1,200 @@
// QA Lab product proof for canonical session and transcript CLI management.
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { upsertSessionEntry } from "../../../../src/config/sessions/session-accessor.js";
import { closeOpenClawAgentDatabasesForTest } from "../../../../src/state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../../../src/state/openclaw-state-db.js";
import type {
TranscriptSessionDescriptor,
TranscriptUtterance,
} from "../../../../src/transcripts/provider-types.js";
import { TranscriptsStore } from "../../../../src/transcripts/store.js";
import { summarizeTranscripts } from "../../../../src/transcripts/summary.js";
import {
createOpenClawTestInstance,
type OpenClawTestInstance,
} from "../../../helpers/openclaw-test-instance.js";
const SESSION_KEY = "agent:main:qa:missing-transcript";
const SESSION_ID = "qa-missing-transcript";
const TRANSCRIPT_ID = "qa-cli-transcript";
const TRANSCRIPT_STARTED_AT = "2026-08-03T12:00:00.000Z";
const TRANSCRIPT_TEXT = "Action item: preserve the CLI transcript artifact.";
type CommandResult = Awaited<ReturnType<OpenClawTestInstance["cli"]>>;
type SessionsJson = {
count: number;
sessions: Array<{ key: string; sessionId?: string }>;
};
type CleanupJson = {
agentId: string;
applied?: true;
afterCount: number;
beforeCount: number;
dryRun: boolean;
missing: number;
};
type TranscriptListJson = Array<{
sessionId: string;
selector: string;
title?: string;
}>;
type TranscriptShowJson = {
selector: string;
session: TranscriptSessionDescriptor;
summary: string | null;
};
type TranscriptPathJson = {
exists: boolean;
path: string;
selector: string;
sessionId: string;
};
let instance: OpenClawTestInstance | undefined;
afterEach(async () => {
closeOpenClawAgentDatabasesForTest();
closeOpenClawStateDatabaseForTest();
await instance?.cleanup();
instance = undefined;
});
function parseCommandJson<T>(label: string, result: CommandResult): T {
if (result.code !== 0) {
throw new Error(
`${label} failed with exit ${String(result.code)}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
return JSON.parse(result.stdout) as T;
}
async function seedTranscript(stateDir: string, env: NodeJS.ProcessEnv) {
const session: TranscriptSessionDescriptor = {
sessionId: TRANSCRIPT_ID,
title: "CLI transcript proof",
source: {
providerId: "manual-transcript",
kind: "posthoc-transcript",
},
startedAt: TRANSCRIPT_STARTED_AT,
stoppedAt: "2026-08-03T12:05:00.000Z",
};
const utterance: TranscriptUtterance = {
final: true,
speaker: { label: "Operator" },
text: TRANSCRIPT_TEXT,
};
const store = new TranscriptsStore(path.join(stateDir, "transcripts"), { env });
await store.writeSession(session);
await store.appendUtteranceForSession(session, utterance);
await store.writeSummary(summarizeTranscripts({ session, utterances: [utterance] }), session);
return session;
}
describe("session and transcript child CLI product proof", () => {
it(
"reads canonical SQLite state, delegates cleanup, and materializes transcript artifacts",
{ timeout: 180_000 },
async () => {
instance = await createOpenClawTestInstance({
name: "qa-session-transcript-cli",
});
instance.state.applyEnv();
await upsertSessionEntry(
{
agentId: "main",
sessionKey: SESSION_KEY,
},
{
sessionId: SESSION_ID,
updatedAt: Date.now() - 60_000,
},
);
const transcriptSession = await seedTranscript(instance.stateDir, instance.env);
closeOpenClawAgentDatabasesForTest();
closeOpenClawStateDatabaseForTest();
const sessionsBefore = parseCommandJson<SessionsJson>(
"sessions --json",
await instance.cli(["sessions", "--json"]),
);
expect(sessionsBefore.sessions).toContainEqual(
expect.objectContaining({
key: SESSION_KEY,
sessionId: SESSION_ID,
}),
);
await instance.startGateway();
expect(instance.child).toBeDefined();
expect(instance.child?.exitCode).toBeNull();
expect(instance.child?.signalCode).toBeNull();
expect(instance.child?.killed).toBe(false);
const cleanup = parseCommandJson<CleanupJson>(
"sessions cleanup",
await instance.cli(["sessions", "cleanup", "--enforce", "--fix-missing", "--json"]),
);
expect(instance.child).toBeDefined();
expect(instance.child?.exitCode).toBeNull();
expect(instance.child?.signalCode).toBeNull();
expect(instance.child?.killed).toBe(false);
expect(cleanup).toMatchObject({
agentId: "main",
applied: true,
dryRun: false,
missing: 1,
});
expect(cleanup.afterCount).toBe(cleanup.beforeCount - 1);
const sessionsAfter = parseCommandJson<SessionsJson>(
"sessions --json after cleanup",
await instance.cli(["sessions", "--json"]),
);
expect(sessionsAfter.sessions.map(({ key }) => key)).not.toContain(SESSION_KEY);
const transcriptList = parseCommandJson<TranscriptListJson>(
"transcripts list",
await instance.cli(["transcripts", "list", "--json"]),
);
expect(transcriptList).toContainEqual(
expect.objectContaining({
sessionId: TRANSCRIPT_ID,
selector: "2026-08-03/qa-cli-transcript",
title: "CLI transcript proof",
}),
);
const transcriptShow = parseCommandJson<TranscriptShowJson>(
"transcripts show",
await instance.cli(["transcripts", "show", TRANSCRIPT_ID, "--json"]),
);
expect(transcriptShow).toMatchObject({
selector: "2026-08-03/qa-cli-transcript",
session: transcriptSession,
});
expect(transcriptShow.summary).toContain(TRANSCRIPT_TEXT);
const transcriptPath = parseCommandJson<TranscriptPathJson>(
"transcripts path",
await instance.cli(["transcripts", "path", TRANSCRIPT_ID, "--transcript", "--json"]),
);
expect(transcriptPath).toMatchObject({
exists: true,
selector: "2026-08-03/qa-cli-transcript",
sessionId: TRANSCRIPT_ID,
});
expect(transcriptPath.path).toBe(
path.join(instance.stateDir, "transcripts", transcriptPath.selector, "transcript.jsonl"),
);
await expect(fs.readFile(transcriptPath.path, "utf8")).resolves.toContain(TRANSCRIPT_TEXT);
},
);
});