import fs from "node:fs";import os from "node:os";import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import type { ProviderEvent } from "@t3tools/contracts";import { PersistenceService } from "./persistenceService"; const tempDirs: string[] = [];const ISO_BASE_MS = Date.parse("2026-02-19T00:00:00.000Z"); function makeTempDir(prefix: string): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); tempDirs.push(dir); return dir;} function iso(offsetMs = 0): string { return new Date(ISO_BASE_MS + offsetMs).toISOString();} afterEach(() => { for (const dir of tempDirs.splice(0, tempDirs.length)) { fs.rmSync(dir, { recursive: true, force: true }); }}); describe("PersistenceService", () => { it("stores app settings in metadata with partial updates", () => { const stateDir = makeTempDir("t3code-persistence-settings-state-"); const dbPath = path.join(stateDir, "state.sqlite"); const service = new PersistenceService({ dbPath }); try { expect(service.getAppSettings()).toEqual({ codexBinaryPath: "", codexHomePath: "", }); expect( service.updateAppSettings({ codexBinaryPath: " /opt/codex/bin/codex ", }), ).toEqual({ codexBinaryPath: "/opt/codex/bin/codex", codexHomePath: "", }); expect( service.updateAppSettings({ codexHomePath: " /Users/theo/.codex ", }), ).toEqual({ codexBinaryPath: "/opt/codex/bin/codex", codexHomePath: "/Users/theo/.codex", }); } finally { service.close(); } const reopened = new PersistenceService({ dbPath }); try { expect(reopened.getAppSettings()).toEqual({ codexBinaryPath: "/opt/codex/bin/codex", codexHomePath: "/Users/theo/.codex", }); } finally { reopened.close(); } }); it("persists projects/threads/messages and serves bootstrap + catch-up", () => { const stateDir = makeTempDir("t3code-persistence-state-"); const projectDir = makeTempDir("t3code-persistence-project-"); const service = new PersistenceService({ dbPath: path.join(stateDir, "state.sqlite"), }); try { const addedProject = service.addProject({ cwd: projectDir }); expect(addedProject.created).toBe(true); const createdThread = service.createThread({ projectId: addedProject.project.id, title: "Thread 1", model: "gpt-5.3-codex", }).thread; service.bindSessionToThread("sess-1", createdThread.id, "runtime-thread-1"); service.persistUserMessageForTurn({ sessionId: "sess-1", clientMessageId: "msg-user-1", clientMessageText: "hello world", input: "hello world", attachments: [], }); const baseEvent: Omit<ProviderEvent, "id" | "createdAt" | "method" | "payload"> = { kind: "notification", provider: "codex", sessionId: "sess-1", }; const event1 = service.ingestProviderEvent({ ...baseEvent, id: "evt-1", createdAt: iso(10), method: "item/started", payload: { item: { type: "agentMessage", id: "assistant-msg-1", text: "", }, }, }); expect(event1?.seq).toBeGreaterThan(0); const event2 = service.ingestProviderEvent({ ...baseEvent, id: "evt-2", createdAt: iso(20), method: "item/agentMessage/delta", itemId: "assistant-msg-1", textDelta: "hi", payload: { itemId: "assistant-msg-1", delta: "hi", }, }); expect((event2?.seq ?? 0) > (event1?.seq ?? 0)).toBe(true); const event3 = service.ingestProviderEvent({ ...baseEvent, id: "evt-3", createdAt: iso(30), method: "item/completed", payload: { item: { type: "agentMessage", id: "assistant-msg-1", text: "hi there", }, }, }); expect((event3?.seq ?? 0) > (event2?.seq ?? 0)).toBe(true); const event4 = service.ingestProviderEvent({ ...baseEvent, id: "evt-4", createdAt: iso(40), method: "turn/completed", turnId: "turn-1", payload: { turn: { id: "turn-1", status: "completed", }, }, }); expect((event4?.seq ?? 0) > (event3?.seq ?? 0)).toBe(true); const snapshot = service.loadSnapshot(); expect(snapshot.projects).toHaveLength(1); expect(snapshot.threads).toHaveLength(1); const thread = snapshot.threads[0]; expect(thread?.messages.map((message) => message.id)).toEqual([ "msg-user-1", "assistant-msg-1", ]); expect(thread?.messages[1]?.text).toBe("hi there"); expect(thread?.turnDiffSummaries[0]?.turnId).toBe("turn-1"); expect(thread?.turnDiffSummaries[0]?.assistantMessageId).toBe("assistant-msg-1"); const catchUp = service.catchUp({ afterSeq: 0 }); expect(catchUp.events.length).toBeGreaterThan(0); const sequences = catchUp.events.map((event) => event.seq); expect(sequences.every((sequence) => Number.isInteger(sequence))).toBe(true); for (let index = 1; index < sequences.length; index += 1) { expect(sequences[index]).toBeGreaterThan(sequences[index - 1] ?? Number.NEGATIVE_INFINITY); } expect(catchUp.events.some((event) => event.eventType === "project.upsert")).toBe(true); expect(catchUp.events.some((event) => event.eventType === "thread.upsert")).toBe(true); expect(catchUp.events.some((event) => event.eventType === "message.upsert")).toBe(true); const providerCatchUp = service.providerCatchUp({ afterSeq: 0 }); expect(providerCatchUp.lastProviderSeq).toBe(event4?.seq ?? 0); expect(providerCatchUp.events.map((event) => event.id)).toEqual([ "evt-1", "evt-2", "evt-3", "evt-4", ]); const providerSeqs = providerCatchUp.events.map((event) => event.seq ?? 0); for (let index = 1; index < providerSeqs.length; index += 1) { expect(providerSeqs[index]).toBeGreaterThan(providerSeqs[index - 1] ?? Number.NEGATIVE_INFINITY); } const providerCatchUpAfterSecond = service.providerCatchUp({ afterSeq: event2?.seq ?? 0, }); expect(providerCatchUpAfterSecond.events.map((event) => event.id)).toEqual(["evt-3", "evt-4"]); } finally { service.close(); } }); it("stores turn diff summaries parsed from checkpoint diffs", () => { const stateDir = makeTempDir("t3code-persistence-diff-state-"); const projectDir = makeTempDir("t3code-persistence-diff-project-"); const service = new PersistenceService({ dbPath: path.join(stateDir, "state.sqlite"), }); try { const project = service.addProject({ cwd: projectDir }).project; const thread = service.createThread({ projectId: project.id, title: "Thread 1", model: "gpt-5.3-codex", }).thread; service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1"); service.persistTurnDiffSummaryFromCheckpoint({ sessionId: "sess-1", runtimeThreadId: "runtime-thread-1", turnId: "turn-1", checkpointTurnCount: 1, completedAt: "2026-02-19T00:00:00.000Z", status: "completed", diff: [ "diff --git a/src/app.ts b/src/app.ts", "index 1111111..2222222 100644", "--- a/src/app.ts", "+++ b/src/app.ts", "@@ -1,1 +1,2 @@", "-console.log('a')", "+console.log('b')", "+console.log('c')", ].join("\n"), }); service.ingestProviderEvent({ id: "evt-agent-msg-1", kind: "notification", provider: "codex", sessionId: "sess-1", createdAt: "2026-02-19T00:00:01.000Z", method: "item/completed", turnId: "turn-1", itemId: "assistant-msg-1", payload: { item: { type: "agentMessage", id: "assistant-msg-1", text: "done", }, threadId: "runtime-thread-1", turnId: "turn-1", }, }); const snapshot = service.loadSnapshot(); const summary = snapshot.threads[0]?.turnDiffSummaries[0]; expect(summary?.turnId).toBe("turn-1"); expect(summary?.checkpointTurnCount).toBe(1); expect(summary?.assistantMessageId).toBe("assistant-msg-1"); expect(summary?.files[0]?.path).toBe("src/app.ts"); expect(summary?.files[0]?.additions).toBe(2); expect(summary?.files[0]?.deletions).toBe(1); } finally { service.close(); } }); it("advances listMessages.nextOffset by fetched row count even when rows fail to parse", () => { const stateDir = makeTempDir("t3code-persistence-list-messages-state-"); const projectDir = makeTempDir("t3code-persistence-list-messages-project-"); const service = new PersistenceService({ dbPath: path.join(stateDir, "state.sqlite"), }); try { const project = service.addProject({ cwd: projectDir }).project; const thread = service.createThread({ projectId: project.id, title: "Thread 1", model: "gpt-5.3-codex", }).thread; service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1"); service.persistUserMessageForTurn({ sessionId: "sess-1", clientMessageId: "msg-1", clientMessageText: "first", input: "first", attachments: [], }); service.persistUserMessageForTurn({ sessionId: "sess-1", clientMessageId: "msg-2", clientMessageText: "second", input: "second", attachments: [], }); const db = ( service as unknown as { db: { prepare: (sql: string) => { run: (...params: unknown[]) => unknown } }; } ).db; db.prepare("UPDATE documents SET data_json = ? WHERE id = ?;").run( "{bad-json", `message:${thread.id}:msg-1`, ); const firstPage = service.listMessages({ threadId: thread.id, limit: 1, offset: 0, }); expect(firstPage.messages).toEqual([]); expect(firstPage.total).toBe(2); expect(firstPage.nextOffset).toBe(1); const secondPage = service.listMessages({ threadId: thread.id, limit: 1, offset: firstPage.nextOffset ?? 0, }); expect(secondPage.messages.map((message) => message.id)).toEqual(["msg-2"]); expect(secondPage.nextOffset).toBeNull(); } finally { service.close(); } }); it("does not throw when state event listeners fail after commit", () => { const stateDir = makeTempDir("t3code-persistence-state-events-"); const projectDir = makeTempDir("t3code-persistence-state-events-project-"); const service = new PersistenceService({ dbPath: path.join(stateDir, "state.sqlite"), }); try { service.on("stateEvent", () => { throw new Error("listener failed"); }); const result = service.addProject({ cwd: projectDir }); expect(result.created).toBe(true); expect(service.listProjects()).toHaveLength(1); } finally { service.close(); } }); it("preserves checkpointTurnCount=0 when merging turn summaries", () => { const stateDir = makeTempDir("t3code-persistence-checkpoint-zero-state-"); const projectDir = makeTempDir("t3code-persistence-checkpoint-zero-project-"); const service = new PersistenceService({ dbPath: path.join(stateDir, "state.sqlite"), }); try { const project = service.addProject({ cwd: projectDir }).project; const thread = service.createThread({ projectId: project.id, title: "Thread 1", model: "gpt-5.3-codex", }).thread; service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1"); service.persistTurnDiffSummaryFromCheckpoint({ sessionId: "sess-1", runtimeThreadId: "runtime-thread-1", turnId: "turn-1", checkpointTurnCount: 0, completedAt: "2026-02-19T00:00:00.000Z", diff: "", }); service.ingestProviderEvent({ id: "evt-assistant-completed", kind: "notification", provider: "codex", sessionId: "sess-1", createdAt: "2026-02-19T00:00:01.000Z", method: "item/completed", turnId: "turn-1", payload: { item: { type: "agentMessage", id: "assistant-msg-1", text: "done", }, }, }); const snapshot = service.loadSnapshot(); const summary = snapshot.threads[0]?.turnDiffSummaries.find( (entry) => entry.turnId === "turn-1", ); expect(summary?.checkpointTurnCount).toBe(0); expect(summary?.assistantMessageId).toBe("assistant-msg-1"); } finally { service.close(); } }); it("does not let out-of-order item/started overwrite accumulated assistant text", () => { const stateDir = makeTempDir("t3code-persistence-item-order-state-"); const projectDir = makeTempDir("t3code-persistence-item-order-project-"); const service = new PersistenceService({ dbPath: path.join(stateDir, "state.sqlite"), }); try { const project = service.addProject({ cwd: projectDir }).project; const thread = service.createThread({ projectId: project.id, title: "Thread 1", model: "gpt-5.3-codex", }).thread; service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1"); service.ingestProviderEvent({ id: "evt-delta", kind: "notification", provider: "codex", sessionId: "sess-1", createdAt: "2026-02-19T00:00:02.000Z", method: "item/agentMessage/delta", itemId: "assistant-msg-1", textDelta: "hello", payload: { itemId: "assistant-msg-1", delta: "hello", }, }); service.ingestProviderEvent({ id: "evt-started-late", kind: "notification", provider: "codex", sessionId: "sess-1", createdAt: "2026-02-19T00:00:03.000Z", method: "item/started", payload: { item: { type: "agentMessage", id: "assistant-msg-1", text: "", }, }, }); const messages = service.listMessages({ threadId: thread.id, limit: 10, offset: 0, }).messages; const assistantMessage = messages.find((message) => message.id === "assistant-msg-1"); expect(assistantMessage?.text).toBe("hello"); } finally { service.close(); } }); });
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { ProviderEvent } from "@t3tools/contracts";
import { PersistenceService } from "./persistenceService";
const tempDirs: string[] = [];
const ISO_BASE_MS = Date.parse("2026-02-19T00:00:00.000Z");
function makeTempDir(prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
function iso(offsetMs = 0): string {
return new Date(ISO_BASE_MS + offsetMs).toISOString();
}
afterEach(() => {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("PersistenceService", () => {
it("stores app settings in metadata with partial updates", () => {
const stateDir = makeTempDir("t3code-persistence-settings-state-");
const dbPath = path.join(stateDir, "state.sqlite");
const service = new PersistenceService({ dbPath });
try {
expect(service.getAppSettings()).toEqual({
codexBinaryPath: "",
codexHomePath: "",
});
expect(
service.updateAppSettings({
codexBinaryPath: " /opt/codex/bin/codex ",
}),
).toEqual({
codexBinaryPath: "/opt/codex/bin/codex",
codexHomePath: "",
});
expect(
service.updateAppSettings({
codexHomePath: " /Users/theo/.codex ",
}),
).toEqual({
codexBinaryPath: "/opt/codex/bin/codex",
codexHomePath: "/Users/theo/.codex",
});
} finally {
service.close();
}
const reopened = new PersistenceService({ dbPath });
try {
expect(reopened.getAppSettings()).toEqual({
codexBinaryPath: "/opt/codex/bin/codex",
codexHomePath: "/Users/theo/.codex",
});
} finally {
reopened.close();
}
});
it("persists projects/threads/messages and serves bootstrap + catch-up", () => {
const stateDir = makeTempDir("t3code-persistence-state-");
const projectDir = makeTempDir("t3code-persistence-project-");
const service = new PersistenceService({
dbPath: path.join(stateDir, "state.sqlite"),
});
try {
const addedProject = service.addProject({ cwd: projectDir });
expect(addedProject.created).toBe(true);
const createdThread = service.createThread({
projectId: addedProject.project.id,
title: "Thread 1",
model: "gpt-5.3-codex",
}).thread;
service.bindSessionToThread("sess-1", createdThread.id, "runtime-thread-1");
service.persistUserMessageForTurn({
sessionId: "sess-1",
clientMessageId: "msg-user-1",
clientMessageText: "hello world",
input: "hello world",
attachments: [],
});
const baseEvent: Omit<ProviderEvent, "id" | "createdAt" | "method" | "payload"> = {
kind: "notification",
provider: "codex",
sessionId: "sess-1",
};
const event1 = service.ingestProviderEvent({
...baseEvent,
id: "evt-1",
createdAt: iso(10),
method: "item/started",
payload: {
item: {
type: "agentMessage",
id: "assistant-msg-1",
text: "",
},
},
});
expect(event1?.seq).toBeGreaterThan(0);
const event2 = service.ingestProviderEvent({
...baseEvent,
id: "evt-2",
createdAt: iso(20),
method: "item/agentMessage/delta",
itemId: "assistant-msg-1",
textDelta: "hi",
payload: {
itemId: "assistant-msg-1",
delta: "hi",
},
});
expect((event2?.seq ?? 0) > (event1?.seq ?? 0)).toBe(true);
const event3 = service.ingestProviderEvent({
...baseEvent,
id: "evt-3",
createdAt: iso(30),
method: "item/completed",
payload: {
item: {
type: "agentMessage",
id: "assistant-msg-1",
text: "hi there",
},
},
});
expect((event3?.seq ?? 0) > (event2?.seq ?? 0)).toBe(true);
const event4 = service.ingestProviderEvent({
...baseEvent,
id: "evt-4",
createdAt: iso(40),
method: "turn/completed",
turnId: "turn-1",
payload: {
turn: {
id: "turn-1",
status: "completed",
},
},
});
expect((event4?.seq ?? 0) > (event3?.seq ?? 0)).toBe(true);
const snapshot = service.loadSnapshot();
expect(snapshot.projects).toHaveLength(1);
expect(snapshot.threads).toHaveLength(1);
const thread = snapshot.threads[0];
expect(thread?.messages.map((message) => message.id)).toEqual([
"msg-user-1",
"assistant-msg-1",
]);
expect(thread?.messages[1]?.text).toBe("hi there");
expect(thread?.turnDiffSummaries[0]?.turnId).toBe("turn-1");
expect(thread?.turnDiffSummaries[0]?.assistantMessageId).toBe("assistant-msg-1");
const catchUp = service.catchUp({ afterSeq: 0 });
expect(catchUp.events.length).toBeGreaterThan(0);
const sequences = catchUp.events.map((event) => event.seq);
expect(sequences.every((sequence) => Number.isInteger(sequence))).toBe(true);
for (let index = 1; index < sequences.length; index += 1) {
expect(sequences[index]).toBeGreaterThan(sequences[index - 1] ?? Number.NEGATIVE_INFINITY);
}
expect(catchUp.events.some((event) => event.eventType === "project.upsert")).toBe(true);
expect(catchUp.events.some((event) => event.eventType === "thread.upsert")).toBe(true);
expect(catchUp.events.some((event) => event.eventType === "message.upsert")).toBe(true);
const providerCatchUp = service.providerCatchUp({ afterSeq: 0 });
expect(providerCatchUp.lastProviderSeq).toBe(event4?.seq ?? 0);
expect(providerCatchUp.events.map((event) => event.id)).toEqual([
"evt-1",
"evt-2",
"evt-3",
"evt-4",
]);
const providerSeqs = providerCatchUp.events.map((event) => event.seq ?? 0);
for (let index = 1; index < providerSeqs.length; index += 1) {
expect(providerSeqs[index]).toBeGreaterThan(providerSeqs[index - 1] ?? Number.NEGATIVE_INFINITY);
}
const providerCatchUpAfterSecond = service.providerCatchUp({
afterSeq: event2?.seq ?? 0,
});
expect(providerCatchUpAfterSecond.events.map((event) => event.id)).toEqual(["evt-3", "evt-4"]);
} finally {
service.close();
}
});
it("stores turn diff summaries parsed from checkpoint diffs", () => {
const stateDir = makeTempDir("t3code-persistence-diff-state-");
const projectDir = makeTempDir("t3code-persistence-diff-project-");
const service = new PersistenceService({
dbPath: path.join(stateDir, "state.sqlite"),
});
try {
const project = service.addProject({ cwd: projectDir }).project;
const thread = service.createThread({
projectId: project.id,
title: "Thread 1",
model: "gpt-5.3-codex",
}).thread;
service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1");
service.persistTurnDiffSummaryFromCheckpoint({
sessionId: "sess-1",
runtimeThreadId: "runtime-thread-1",
turnId: "turn-1",
checkpointTurnCount: 1,
completedAt: "2026-02-19T00:00:00.000Z",
status: "completed",
diff: [
"diff --git a/src/app.ts b/src/app.ts",
"index 1111111..2222222 100644",
"--- a/src/app.ts",
"+++ b/src/app.ts",
"@@ -1,1 +1,2 @@",
"-console.log('a')",
"+console.log('b')",
"+console.log('c')",
].join("\n"),
});
service.ingestProviderEvent({
id: "evt-agent-msg-1",
kind: "notification",
provider: "codex",
sessionId: "sess-1",
createdAt: "2026-02-19T00:00:01.000Z",
method: "item/completed",
turnId: "turn-1",
itemId: "assistant-msg-1",
payload: {
item: {
type: "agentMessage",
id: "assistant-msg-1",
text: "done",
},
threadId: "runtime-thread-1",
turnId: "turn-1",
},
});
const snapshot = service.loadSnapshot();
const summary = snapshot.threads[0]?.turnDiffSummaries[0];
expect(summary?.turnId).toBe("turn-1");
expect(summary?.checkpointTurnCount).toBe(1);
expect(summary?.assistantMessageId).toBe("assistant-msg-1");
expect(summary?.files[0]?.path).toBe("src/app.ts");
expect(summary?.files[0]?.additions).toBe(2);
expect(summary?.files[0]?.deletions).toBe(1);
} finally {
service.close();
}
});
it("advances listMessages.nextOffset by fetched row count even when rows fail to parse", () => {
const stateDir = makeTempDir("t3code-persistence-list-messages-state-");
const projectDir = makeTempDir("t3code-persistence-list-messages-project-");
const service = new PersistenceService({
dbPath: path.join(stateDir, "state.sqlite"),
});
try {
const project = service.addProject({ cwd: projectDir }).project;
const thread = service.createThread({
projectId: project.id,
title: "Thread 1",
model: "gpt-5.3-codex",
}).thread;
service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1");
service.persistUserMessageForTurn({
sessionId: "sess-1",
clientMessageId: "msg-1",
clientMessageText: "first",
input: "first",
attachments: [],
});
service.persistUserMessageForTurn({
sessionId: "sess-1",
clientMessageId: "msg-2",
clientMessageText: "second",
input: "second",
attachments: [],
});
const db = (
service as unknown as {
db: { prepare: (sql: string) => { run: (...params: unknown[]) => unknown } };
}
).db;
db.prepare("UPDATE documents SET data_json = ? WHERE id = ?;").run(
"{bad-json",
`message:${thread.id}:msg-1`,
);
const firstPage = service.listMessages({
threadId: thread.id,
limit: 1,
offset: 0,
});
expect(firstPage.messages).toEqual([]);
expect(firstPage.total).toBe(2);
expect(firstPage.nextOffset).toBe(1);
const secondPage = service.listMessages({
threadId: thread.id,
limit: 1,
offset: firstPage.nextOffset ?? 0,
});
expect(secondPage.messages.map((message) => message.id)).toEqual(["msg-2"]);
expect(secondPage.nextOffset).toBeNull();
} finally {
service.close();
}
});
it("does not throw when state event listeners fail after commit", () => {
const stateDir = makeTempDir("t3code-persistence-state-events-");
const projectDir = makeTempDir("t3code-persistence-state-events-project-");
const service = new PersistenceService({
dbPath: path.join(stateDir, "state.sqlite"),
});
try {
service.on("stateEvent", () => {
throw new Error("listener failed");
});
const result = service.addProject({ cwd: projectDir });
expect(result.created).toBe(true);
expect(service.listProjects()).toHaveLength(1);
} finally {
service.close();
}
});
it("preserves checkpointTurnCount=0 when merging turn summaries", () => {
const stateDir = makeTempDir("t3code-persistence-checkpoint-zero-state-");
const projectDir = makeTempDir("t3code-persistence-checkpoint-zero-project-");
const service = new PersistenceService({
dbPath: path.join(stateDir, "state.sqlite"),
});
try {
const project = service.addProject({ cwd: projectDir }).project;
const thread = service.createThread({
projectId: project.id,
title: "Thread 1",
model: "gpt-5.3-codex",
}).thread;
service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1");
service.persistTurnDiffSummaryFromCheckpoint({
sessionId: "sess-1",
runtimeThreadId: "runtime-thread-1",
turnId: "turn-1",
checkpointTurnCount: 0,
completedAt: "2026-02-19T00:00:00.000Z",
diff: "",
});
service.ingestProviderEvent({
id: "evt-assistant-completed",
kind: "notification",
provider: "codex",
sessionId: "sess-1",
createdAt: "2026-02-19T00:00:01.000Z",
method: "item/completed",
turnId: "turn-1",
payload: {
item: {
type: "agentMessage",
id: "assistant-msg-1",
text: "done",
},
},
});
const snapshot = service.loadSnapshot();
const summary = snapshot.threads[0]?.turnDiffSummaries.find(
(entry) => entry.turnId === "turn-1",
);
expect(summary?.checkpointTurnCount).toBe(0);
expect(summary?.assistantMessageId).toBe("assistant-msg-1");
} finally {
service.close();
}
});
it("does not let out-of-order item/started overwrite accumulated assistant text", () => {
const stateDir = makeTempDir("t3code-persistence-item-order-state-");
const projectDir = makeTempDir("t3code-persistence-item-order-project-");
const service = new PersistenceService({
dbPath: path.join(stateDir, "state.sqlite"),
});
try {
const project = service.addProject({ cwd: projectDir }).project;
const thread = service.createThread({
projectId: project.id,
title: "Thread 1",
model: "gpt-5.3-codex",
}).thread;
service.bindSessionToThread("sess-1", thread.id, "runtime-thread-1");
service.ingestProviderEvent({
id: "evt-delta",
kind: "notification",
provider: "codex",
sessionId: "sess-1",
createdAt: "2026-02-19T00:00:02.000Z",
method: "item/agentMessage/delta",
itemId: "assistant-msg-1",
textDelta: "hello",
payload: {
itemId: "assistant-msg-1",
delta: "hello",
},
});
service.ingestProviderEvent({
id: "evt-started-late",
kind: "notification",
provider: "codex",
sessionId: "sess-1",
createdAt: "2026-02-19T00:00:03.000Z",
method: "item/started",
payload: {
item: {
type: "agentMessage",
id: "assistant-msg-1",
text: "",
},
},
});
const messages = service.listMessages({
threadId: thread.id,
limit: 10,
offset: 0,
}).messages;
const assistantMessage = messages.find((message) => message.id === "assistant-msg-1");
expect(assistantMessage?.text).toBe("hello");
} finally {
service.close();
}
});
});