From 6d943104b407611aa50be1d3004e1f4acdfd040f Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Fri, 3 Jul 2026 10:44:48 -0700 Subject: [PATCH] test: cover trajectory rows in sqlite flip proof --- .pebbles/events.jsonl | 5 + .../path3-live-sqlite-e2e-harness.md | 4 + .../sqlite-sessions-transcripts-flip-proof.ts | 107 ++++++++++++++++++ ...essions-transcripts-flip-proof.e2e.test.ts | 12 +- 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/.pebbles/events.jsonl b/.pebbles/events.jsonl index bb9920609e90..5df01e16c622 100644 --- a/.pebbles/events.jsonl +++ b/.pebbles/events.jsonl @@ -1746,3 +1746,8 @@ {"type":"comment","timestamp":"2026-07-03T17:36:01.634601Z","issue_id":"oc-08c.8","payload":{"body":"Final proof: focused trajectory/cleanup/doctor/budget test set passed (6 Vitest shards, 142 tests), pnpm tsgo:core passed, pnpm docs:list passed, and git diff --check passed. Autoreview was attempted but blocked before review by the local codex wrapper pointing at a missing vendored binary: /Users/josh/.nvm/versions/node/v22.22.1/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex/codex ENOENT."}} {"type":"close","timestamp":"2026-07-03T17:36:05.80934Z","issue_id":"oc-08c.8","payload":{}} {"type":"close","timestamp":"2026-07-03T17:36:06.39963Z","issue_id":"oc-08c","payload":{}} +{"type":"create","timestamp":"2026-07-03T17:41:59.889705Z","issue_id":"oc-a85","payload":{"description":"Extend the SQLite sessions/transcripts flip proof harness with trajectory runtime-row evidence: append a marker-backed trajectory event through the SDK/runtime seam, assert it lands in trajectory_runtime_events, and assert no JSONL or pointer sidecar appears during the flip proof.","priority":"1","title":"AN-0009: add trajectory coverage to SQLite flip E2E","type":"task"}} +{"type":"status_update","timestamp":"2026-07-03T17:42:03.5015Z","issue_id":"oc-a85","payload":{"status":"in_progress"}} +{"type":"rename","timestamp":"2026-07-03T17:42:03.5015Z","issue_id":"oc-a85","payload":{"new_id":"oc-08c.9"}} +{"type":"dep_add","timestamp":"2026-07-03T17:42:03.5015Z","issue_id":"oc-08c.9","payload":{"dep_type":"parent-child","depends_on":"oc-08c"}} +{"type":"close","timestamp":"2026-07-03T17:44:47.739182Z","issue_id":"oc-08c.9","payload":{}} diff --git a/docs/reference/path3-live-sqlite-e2e-harness.md b/docs/reference/path3-live-sqlite-e2e-harness.md index 9db275fb81c1..d36427785a90 100644 --- a/docs/reference/path3-live-sqlite-e2e-harness.md +++ b/docs/reference/path3-live-sqlite-e2e-harness.md @@ -107,10 +107,14 @@ Each step snapshots before and after state and writes a structured assertion record: - SQLite row counts advance only where expected. +- Trajectory runtime rows advance for marker-backed proof sessions that record + runtime events. - The proof session row has the expected `session_id`, status, timestamps, metadata, and route rows. - Gateway history/session projection matches the SQLite transcript tail. - No proof-session JSONL file is created or modified. +- No proof-session `.trajectory.jsonl`, `.trajectory-path.json`, or + marker-derived `trajectory/.jsonl` sidecar is created. - Existing legacy JSONL files and `sessions.json` remain unchanged unless the step is explicitly an offline migration or archive operation. - The Gateway process does not open `.jsonl` or `sessions.json` handles. diff --git a/scripts/e2e/sqlite-sessions-transcripts-flip-proof.ts b/scripts/e2e/sqlite-sessions-transcripts-flip-proof.ts index ed54e85e4d69..9c81f71e87ca 100644 --- a/scripts/e2e/sqlite-sessions-transcripts-flip-proof.ts +++ b/scripts/e2e/sqlite-sessions-transcripts-flip-proof.ts @@ -22,6 +22,8 @@ import { getSessionEntry as getSdkSessionEntry, listSessionEntries as listSdkSessionEntries, loadTranscriptEventsSync as loadSdkTranscriptEventsSync, + appendTrajectoryRuntimeEvents, + type SessionStoreTrajectoryEvent, } from "../../src/plugin-sdk/session-store-runtime.js"; import { appendSessionTranscriptMessageByIdentity, @@ -74,6 +76,7 @@ type SqliteSessionEntryEvidence = { entry?: Record; sessionId: string; sessionKey: string; + trajectoryEvents: number; transcriptEvents: number; }; @@ -82,6 +85,7 @@ type SqliteEvidence = { path: string; sessionEntries: number; sessions: number; + trajectoryRuntimeEvents: number; transcriptEvents: number; trackedEntries: SqliteSessionEntryEvidence[]; }; @@ -98,6 +102,9 @@ type ProofCheckpoint = { type PluginSdkConsumerEvidence = { activeJsonlForSessionExists: boolean; + activeTrajectoryPointerForSessionExists: boolean; + activeTrajectoryRuntimeSidecarForSessionExists: boolean; + activeTrajectorySessionSidecarForSessionExists: boolean; appendedMessageId: string; identityMemoryKey: string; latestAssistantTextBeforeAppend: string; @@ -107,6 +114,9 @@ type PluginSdkConsumerEvidence = { sessionId: string; sessionKey: string; storeTranscriptEvents: number; + trajectoryEventsAfterAppend: number; + trajectoryEventsBeforeAppend: number; + trajectoryEventType: string; transcriptEventsAfterAppend: number; transcriptEventsBeforeAppend: number; }; @@ -1262,10 +1272,43 @@ async function runPluginSdkConsumerProbe( const transcriptEventsBeforeAppend = (await readSessionTranscriptEvents(scope)).length; const storeTranscriptEvents = loadSdkTranscriptEventsSync(scope).length; const activeJsonlPath = path.join(context.activeSessionsDir, `${sessionId}.jsonl`); + const activeTrajectorySessionSidecarPath = path.join( + context.activeSessionsDir, + `${sessionId}.trajectory.jsonl`, + ); + const activeTrajectoryPointerPath = path.join( + context.activeSessionsDir, + `${sessionId}.trajectory-path.json`, + ); + const activeTrajectoryRuntimeSidecarPath = path.join( + context.activeSessionsDir, + "trajectory", + `${sessionId}.jsonl`, + ); const activeJsonlForSessionExists = fsSync.existsSync(activeJsonlPath); if (activeJsonlForSessionExists) { throw new Error(`SDK probe found active JSONL for SQLite session at ${activeJsonlPath}`); } + const activeTrajectorySessionSidecarForSessionExists = fsSync.existsSync( + activeTrajectorySessionSidecarPath, + ); + const activeTrajectoryPointerForSessionExists = fsSync.existsSync(activeTrajectoryPointerPath); + const activeTrajectoryRuntimeSidecarForSessionExists = fsSync.existsSync( + activeTrajectoryRuntimeSidecarPath, + ); + if ( + activeTrajectorySessionSidecarForSessionExists || + activeTrajectoryPointerForSessionExists || + activeTrajectoryRuntimeSidecarForSessionExists + ) { + throw new Error( + `SDK trajectory probe found active sidecar paths: ${JSON.stringify({ + pointer: activeTrajectoryPointerPath, + runtime: activeTrajectoryRuntimeSidecarPath, + session: activeTrajectorySessionSidecarPath, + })}`, + ); + } const appended = await appendSessionTranscriptMessageByIdentity({ ...scope, @@ -1293,9 +1336,44 @@ async function runPluginSdkConsumerProbe( `SDK transcript append did not increase event count for ${context.pluginSdkSessionKey}`, ); } + const trajectoryEventsBeforeAppend = countSqliteTrajectoryRuntimeEvents( + context.agentDbPath, + sessionId, + ); + const trajectoryEventType = "e2e.sdk.trajectory"; + const trajectoryEvent: SessionStoreTrajectoryEvent = { + traceSchema: "openclaw-trajectory", + schemaVersion: 1, + traceId: sessionId, + source: "runtime", + type: trajectoryEventType, + ts: new Date().toISOString(), + seq: 1, + sourceSeq: 1, + sessionId, + sessionKey: context.pluginSdkSessionKey, + runId: "sqlite-flip-e2e-sdk", + data: { proof: "sqlite trajectory runtime row" }, + }; + appendTrajectoryRuntimeEvents({ + ...scope, + events: [trajectoryEvent], + }); + const trajectoryEventsAfterAppend = countSqliteTrajectoryRuntimeEvents( + context.agentDbPath, + sessionId, + ); + if (trajectoryEventsAfterAppend <= trajectoryEventsBeforeAppend) { + throw new Error( + `SDK trajectory append did not increase event count for ${context.pluginSdkSessionKey}`, + ); + } return { activeJsonlForSessionExists, + activeTrajectoryPointerForSessionExists, + activeTrajectoryRuntimeSidecarForSessionExists, + activeTrajectorySessionSidecarForSessionExists, appendedMessageId: appended.messageId, identityMemoryKey: identity.memoryKey, latestAssistantTextBeforeAppend: latestBefore.text, @@ -1305,6 +1383,9 @@ async function runPluginSdkConsumerProbe( sessionId, sessionKey: context.pluginSdkSessionKey, storeTranscriptEvents, + trajectoryEventsAfterAppend, + trajectoryEventsBeforeAppend, + trajectoryEventType, transcriptEventsAfterAppend, transcriptEventsBeforeAppend, }; @@ -2069,6 +2150,22 @@ function countSqliteTranscriptEvents(dbPath: string, sessionId: string): number } } +function countSqliteTrajectoryRuntimeEvents(dbPath: string, sessionId: string): number { + if (!fsSync.existsSync(dbPath)) { + return 0; + } + const db = new DatabaseSync(dbPath, { readOnly: true }); + try { + return scalarNumber( + db, + "SELECT COUNT(*) AS count FROM trajectory_runtime_events WHERE session_id = ?", + [sessionId], + ); + } finally { + db.close(); + } +} + async function captureCheckpoint( context: ProofContext, label: string, @@ -2221,6 +2318,7 @@ function readSqliteEvidence(dbPath: string, trackedSessionKeys: readonly string[ sessionEntries: 0, sessions: 0, trackedEntries: [], + trajectoryRuntimeEvents: 0, transcriptEvents: 0, }; } @@ -2232,6 +2330,10 @@ function readSqliteEvidence(dbPath: string, trackedSessionKeys: readonly string[ path: dbPath, sessionEntries: scalarNumber(db, "SELECT COUNT(*) AS count FROM session_entries"), sessions: scalarNumber(db, "SELECT COUNT(*) AS count FROM sessions"), + trajectoryRuntimeEvents: scalarNumber( + db, + "SELECT COUNT(*) AS count FROM trajectory_runtime_events", + ), trackedEntries, transcriptEvents: scalarNumber(db, "SELECT COUNT(*) AS count FROM transcript_events"), }; @@ -2262,6 +2364,11 @@ function readTrackedEntries( const result: SqliteSessionEntryEvidence = { sessionId, sessionKey: typeof row.sessionKey === "string" ? row.sessionKey : "", + trajectoryEvents: scalarNumber( + db, + "SELECT COUNT(*) AS count FROM trajectory_runtime_events WHERE session_id = ?", + [sessionId], + ), transcriptEvents: scalarNumber( db, "SELECT COUNT(*) AS count FROM transcript_events WHERE session_id = ?", diff --git a/test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts b/test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts index 6e9c30af4316..c89907131af2 100644 --- a/test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts +++ b/test/scripts/sqlite-sessions-transcripts-flip-proof.e2e.test.ts @@ -157,22 +157,32 @@ describe("SQLite sessions/transcripts flip proof harness", () => { ).toBe(true); expect(report.pluginSdkConsumer).toMatchObject({ activeJsonlForSessionExists: false, + activeTrajectoryPointerForSessionExists: false, + activeTrajectoryRuntimeSidecarForSessionExists: false, + activeTrajectorySessionSidecarForSessionExists: false, latestAssistantTextBeforeAppend: report.fullTurnAssistantText, latestAssistantTextAfterAppend: "sqlite sdk consumer appended by identity", sessionKey: report.pluginSdkSessionKey, + trajectoryEventType: "e2e.sdk.trajectory", }); expect(report.pluginSdkConsumer?.sessionFileMarker.startsWith("sqlite:")).toBe(true); expect(report.pluginSdkConsumer?.listedSessionKeys).toContain(report.pluginSdkSessionKey); expect(report.pluginSdkConsumer?.transcriptEventsAfterAppend).toBeGreaterThan( report.pluginSdkConsumer?.transcriptEventsBeforeAppend ?? 0, ); + expect(report.pluginSdkConsumer?.trajectoryEventsAfterAppend).toBeGreaterThan( + report.pluginSdkConsumer?.trajectoryEventsBeforeAppend ?? 0, + ); expect( report.checkpoints.some( (checkpoint) => checkpoint.label === "after-plugin-sdk-consumer" && + checkpoint.sqlite.trajectoryRuntimeEvents >= 1 && checkpoint.sqlite.trackedEntries.some( (entry) => - entry.sessionKey === report.pluginSdkSessionKey && entry.transcriptEvents >= 3, + entry.sessionKey === report.pluginSdkSessionKey && + entry.trajectoryEvents >= 1 && + entry.transcriptEvents >= 3, ), ), ).toBe(true);