import { MessageId, ThreadId, TurnId } from "@t3tools/contracts";import { assert, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer"; import { ProjectionThreadMessageRepository } from "../Services/ProjectionThreadMessages.ts";import { ProjectionThreadMessageRepositoryLive } from "./ProjectionThreadMessages.ts";import { SqlitePersistenceMemory } from "./Sqlite.ts"; const layer = it.layer( ProjectionThreadMessageRepositoryLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),); layer("ProjectionThreadMessageRepository", (it) => { it.effect("finds the latest live user-message time within one thread", () => Effect.gen(function* () { const repository = yield* ProjectionThreadMessageRepository; const threadId = ThreadId.make("thread-latest-user-message"); assert.isNull(yield* repository.getLatestUserMessageAt({ threadId })); yield* repository.upsert({ messageId: MessageId.make("import:codex:latest-user-message:000000"), threadId, turnId: null, role: "user", text: "Imported prompt", isStreaming: false, createdAt: "2026-02-28T19:05:06.000Z", updatedAt: "2026-02-28T19:05:06.000Z", }); assert.isNull(yield* repository.getLatestUserMessageAt({ threadId })); const messages = [ { role: "user", createdAt: "2026-02-28T19:05:02.000Z" }, { role: "user", createdAt: "2026-02-28T19:05:01.000Z" }, { role: "assistant", createdAt: "2026-02-28T19:05:03.000Z" }, { role: "system", createdAt: "2026-02-28T19:05:04.000Z" }, ] as const; for (const [index, message] of messages.entries()) { yield* repository.upsert({ messageId: MessageId.make(`latest-user-message-${index}`), threadId, turnId: null, ...message, text: "Message body", isStreaming: false, updatedAt: "2026-02-28T19:06:00.000Z", }); } yield* repository.upsert({ messageId: MessageId.make("latest-user-message-other-thread"), threadId: ThreadId.make("thread-latest-user-message-other"), turnId: null, role: "user", text: "Other thread", isStreaming: false, createdAt: "2026-02-28T19:05:05.000Z", updatedAt: "2026-02-28T19:05:05.000Z", }); assert.strictEqual( yield* repository.getLatestUserMessageAt({ threadId }), "2026-02-28T19:05:02.000Z", ); yield* repository.deleteByThreadId({ threadId }); assert.isNull(yield* repository.getLatestUserMessageAt({ threadId })); }), ); it.effect("appends streaming text and applies attachment updates", () => Effect.gen(function* () { const repository = yield* ProjectionThreadMessageRepository; const threadId = ThreadId.make("thread-streaming-append"); const messageId = MessageId.make("message-streaming-append"); const createdAt = "2026-02-28T19:05:00.000Z"; const attachments = [ { type: "image" as const, id: "thread-streaming-append-att-1", name: "example.png", mimeType: "image/png", sizeBytes: 5, }, ]; yield* repository.appendStreaming({ messageId, threadId, turnId: null, role: "assistant", text: "hello", attachments, createdAt, updatedAt: createdAt, }); yield* repository.appendStreaming({ messageId, threadId, turnId: null, role: "assistant", text: " world", createdAt: "2026-02-28T19:05:01.000Z", updatedAt: "2026-02-28T19:05:01.000Z", }); const rowWithPreservedAttachments = yield* repository.getByMessageId({ messageId }); assert.equal(rowWithPreservedAttachments._tag, "Some"); if (rowWithPreservedAttachments._tag === "Some") { assert.deepEqual(rowWithPreservedAttachments.value.attachments, attachments); } yield* repository.appendStreaming({ messageId, threadId, turnId: null, role: "assistant", text: "", attachments: [], createdAt: "2026-02-28T19:05:02.000Z", updatedAt: "2026-02-28T19:05:02.000Z", }); const row = yield* repository.getByMessageId({ messageId }); assert.equal(row._tag, "Some"); if (row._tag === "Some") { assert.equal(row.value.text, "hello world"); assert.deepEqual(row.value.attachments, []); assert.equal(row.value.createdAt, createdAt); assert.equal(row.value.updatedAt, "2026-02-28T19:05:02.000Z"); assert.isTrue(row.value.isStreaming); } }), ); it.effect("preserves existing attachments when upsert omits attachments", () => Effect.gen(function* () { const repository = yield* ProjectionThreadMessageRepository; const threadId = ThreadId.make("thread-preserve-attachments"); const messageId = MessageId.make("message-preserve-attachments"); const createdAt = "2026-02-28T19:00:00.000Z"; const updatedAt = "2026-02-28T19:00:01.000Z"; const persistedAttachments = [ { type: "image" as const, id: "thread-preserve-attachments-att-1", name: "example.png", mimeType: "image/png", sizeBytes: 5, }, ]; yield* repository.upsert({ messageId, threadId, turnId: null, role: "user", text: "initial", attachments: persistedAttachments, isStreaming: false, createdAt, updatedAt, }); yield* repository.upsert({ messageId, threadId, turnId: null, role: "user", text: "updated", isStreaming: false, createdAt, updatedAt: "2026-02-28T19:00:02.000Z", }); const rows = yield* repository.listByThreadId({ threadId }); assert.equal(rows.length, 1); assert.equal(rows[0]?.text, "updated"); assert.deepEqual(rows[0]?.attachments, persistedAttachments); const rowById = yield* repository.getByMessageId({ messageId }); assert.equal(rowById._tag, "Some"); if (rowById._tag === "Some") { assert.equal(rowById.value.text, "updated"); assert.deepEqual(rowById.value.attachments, persistedAttachments); } }), ); it.effect("allows explicit attachment clearing with an empty array", () => Effect.gen(function* () { const repository = yield* ProjectionThreadMessageRepository; const threadId = ThreadId.make("thread-clear-attachments"); const messageId = MessageId.make("message-clear-attachments"); const createdAt = "2026-02-28T19:10:00.000Z"; yield* repository.upsert({ messageId, threadId, turnId: null, role: "assistant", text: "with attachment", attachments: [ { type: "image", id: "thread-clear-attachments-att-1", name: "example.png", mimeType: "image/png", sizeBytes: 5, }, ], isStreaming: false, createdAt, updatedAt: "2026-02-28T19:10:01.000Z", }); yield* repository.upsert({ messageId, threadId, turnId: null, role: "assistant", text: "cleared", attachments: [], isStreaming: false, createdAt, updatedAt: "2026-02-28T19:10:02.000Z", }); const rows = yield* repository.listByThreadId({ threadId }); assert.equal(rows.length, 1); assert.equal(rows[0]?.text, "cleared"); assert.deepEqual(rows[0]?.attachments, []); }), ); it.effect("checks assistant turn state without hydrating message text", () => Effect.gen(function* () { const repository = yield* ProjectionThreadMessageRepository; const threadId = ThreadId.make("thread-assistant-turn-state"); const turnId = TurnId.make("turn-assistant-state"); const createdAt = "2026-03-01T00:00:00.000Z"; yield* repository.upsert({ messageId: MessageId.make("message-assistant-turn-state"), threadId, turnId, role: "assistant", text: "large text that the existence query must not select", isStreaming: false, createdAt, updatedAt: createdAt, }); assert.equal( yield* repository.hasAssistantMessageForTurn({ threadId, turnId, streamingOnly: false, }), true, ); assert.equal( yield* repository.hasAssistantMessageForTurn({ threadId, turnId, streamingOnly: true, }), false, ); assert.equal( yield* repository.hasAssistantMessageForTurn({ threadId, turnId: TurnId.make("turn-assistant-state-missing"), streamingOnly: false, }), false, ); }), );});
import { MessageId, ThreadId, TurnId } from "@t3tools/contracts";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { ProjectionThreadMessageRepository } from "../Services/ProjectionThreadMessages.ts";
import { ProjectionThreadMessageRepositoryLive } from "./ProjectionThreadMessages.ts";
import { SqlitePersistenceMemory } from "./Sqlite.ts";
const layer = it.layer(
ProjectionThreadMessageRepositoryLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),
);
layer("ProjectionThreadMessageRepository", (it) => {
it.effect("finds the latest live user-message time within one thread", () =>
Effect.gen(function* () {
const repository = yield* ProjectionThreadMessageRepository;
const threadId = ThreadId.make("thread-latest-user-message");
assert.isNull(yield* repository.getLatestUserMessageAt({ threadId }));
yield* repository.upsert({
messageId: MessageId.make("import:codex:latest-user-message:000000"),
threadId,
turnId: null,
role: "user",
text: "Imported prompt",
isStreaming: false,
createdAt: "2026-02-28T19:05:06.000Z",
updatedAt: "2026-02-28T19:05:06.000Z",
});
assert.isNull(yield* repository.getLatestUserMessageAt({ threadId }));
const messages = [
{ role: "user", createdAt: "2026-02-28T19:05:02.000Z" },
{ role: "user", createdAt: "2026-02-28T19:05:01.000Z" },
{ role: "assistant", createdAt: "2026-02-28T19:05:03.000Z" },
{ role: "system", createdAt: "2026-02-28T19:05:04.000Z" },
] as const;
for (const [index, message] of messages.entries()) {
yield* repository.upsert({
messageId: MessageId.make(`latest-user-message-${index}`),
threadId,
turnId: null,
...message,
text: "Message body",
isStreaming: false,
updatedAt: "2026-02-28T19:06:00.000Z",
});
}
yield* repository.upsert({
messageId: MessageId.make("latest-user-message-other-thread"),
threadId: ThreadId.make("thread-latest-user-message-other"),
turnId: null,
role: "user",
text: "Other thread",
isStreaming: false,
createdAt: "2026-02-28T19:05:05.000Z",
updatedAt: "2026-02-28T19:05:05.000Z",
});
assert.strictEqual(
yield* repository.getLatestUserMessageAt({ threadId }),
"2026-02-28T19:05:02.000Z",
);
yield* repository.deleteByThreadId({ threadId });
assert.isNull(yield* repository.getLatestUserMessageAt({ threadId }));
}),
);
it.effect("appends streaming text and applies attachment updates", () =>
Effect.gen(function* () {
const repository = yield* ProjectionThreadMessageRepository;
const threadId = ThreadId.make("thread-streaming-append");
const messageId = MessageId.make("message-streaming-append");
const createdAt = "2026-02-28T19:05:00.000Z";
const attachments = [
{
type: "image" as const,
id: "thread-streaming-append-att-1",
name: "example.png",
mimeType: "image/png",
sizeBytes: 5,
},
];
yield* repository.appendStreaming({
messageId,
threadId,
turnId: null,
role: "assistant",
text: "hello",
attachments,
createdAt,
updatedAt: createdAt,
});
yield* repository.appendStreaming({
messageId,
threadId,
turnId: null,
role: "assistant",
text: " world",
createdAt: "2026-02-28T19:05:01.000Z",
updatedAt: "2026-02-28T19:05:01.000Z",
});
const rowWithPreservedAttachments = yield* repository.getByMessageId({ messageId });
assert.equal(rowWithPreservedAttachments._tag, "Some");
if (rowWithPreservedAttachments._tag === "Some") {
assert.deepEqual(rowWithPreservedAttachments.value.attachments, attachments);
}
yield* repository.appendStreaming({
messageId,
threadId,
turnId: null,
role: "assistant",
text: "",
attachments: [],
createdAt: "2026-02-28T19:05:02.000Z",
updatedAt: "2026-02-28T19:05:02.000Z",
});
const row = yield* repository.getByMessageId({ messageId });
assert.equal(row._tag, "Some");
if (row._tag === "Some") {
assert.equal(row.value.text, "hello world");
assert.deepEqual(row.value.attachments, []);
assert.equal(row.value.createdAt, createdAt);
assert.equal(row.value.updatedAt, "2026-02-28T19:05:02.000Z");
assert.isTrue(row.value.isStreaming);
}
}),
);
it.effect("preserves existing attachments when upsert omits attachments", () =>
Effect.gen(function* () {
const repository = yield* ProjectionThreadMessageRepository;
const threadId = ThreadId.make("thread-preserve-attachments");
const messageId = MessageId.make("message-preserve-attachments");
const createdAt = "2026-02-28T19:00:00.000Z";
const updatedAt = "2026-02-28T19:00:01.000Z";
const persistedAttachments = [
{
type: "image" as const,
id: "thread-preserve-attachments-att-1",
name: "example.png",
mimeType: "image/png",
sizeBytes: 5,
},
];
yield* repository.upsert({
messageId,
threadId,
turnId: null,
role: "user",
text: "initial",
attachments: persistedAttachments,
isStreaming: false,
createdAt,
updatedAt,
});
yield* repository.upsert({
messageId,
threadId,
turnId: null,
role: "user",
text: "updated",
isStreaming: false,
createdAt,
updatedAt: "2026-02-28T19:00:02.000Z",
});
const rows = yield* repository.listByThreadId({ threadId });
assert.equal(rows.length, 1);
assert.equal(rows[0]?.text, "updated");
assert.deepEqual(rows[0]?.attachments, persistedAttachments);
const rowById = yield* repository.getByMessageId({ messageId });
assert.equal(rowById._tag, "Some");
if (rowById._tag === "Some") {
assert.equal(rowById.value.text, "updated");
assert.deepEqual(rowById.value.attachments, persistedAttachments);
}
}),
);
it.effect("allows explicit attachment clearing with an empty array", () =>
Effect.gen(function* () {
const repository = yield* ProjectionThreadMessageRepository;
const threadId = ThreadId.make("thread-clear-attachments");
const messageId = MessageId.make("message-clear-attachments");
const createdAt = "2026-02-28T19:10:00.000Z";
yield* repository.upsert({
messageId,
threadId,
turnId: null,
role: "assistant",
text: "with attachment",
attachments: [
{
type: "image",
id: "thread-clear-attachments-att-1",
name: "example.png",
mimeType: "image/png",
sizeBytes: 5,
},
],
isStreaming: false,
createdAt,
updatedAt: "2026-02-28T19:10:01.000Z",
});
yield* repository.upsert({
messageId,
threadId,
turnId: null,
role: "assistant",
text: "cleared",
attachments: [],
isStreaming: false,
createdAt,
updatedAt: "2026-02-28T19:10:02.000Z",
});
const rows = yield* repository.listByThreadId({ threadId });
assert.equal(rows.length, 1);
assert.equal(rows[0]?.text, "cleared");
assert.deepEqual(rows[0]?.attachments, []);
}),
);
it.effect("checks assistant turn state without hydrating message text", () =>
Effect.gen(function* () {
const repository = yield* ProjectionThreadMessageRepository;
const threadId = ThreadId.make("thread-assistant-turn-state");
const turnId = TurnId.make("turn-assistant-state");
const createdAt = "2026-03-01T00:00:00.000Z";
yield* repository.upsert({
messageId: MessageId.make("message-assistant-turn-state"),
threadId,
turnId,
role: "assistant",
text: "large text that the existence query must not select",
isStreaming: false,
createdAt,
updatedAt: createdAt,
});
assert.equal(
yield* repository.hasAssistantMessageForTurn({
threadId,
turnId,
streamingOnly: false,
}),
true,
);
assert.equal(
yield* repository.hasAssistantMessageForTurn({
threadId,
turnId,
streamingOnly: true,
}),
false,
);
assert.equal(
yield* repository.hasAssistantMessageForTurn({
threadId,
turnId: TurnId.make("turn-assistant-state-missing"),
streamingOnly: false,
}),
false,
);
}),
);
});