Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

72 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { readFileSync, readdirSync } from "node:fs";
3import { DatabaseSync } from "node:sqlite";
4import { test } from "node:test";
5
6import { G1T_KEY } from "@g1t/contracts/chat";
7
8import { markReadUpTo } from "./read.ts";
9import { tally } from "./unread.ts";
10
11/** D1, as far as the read mark uses it, over node's SQLite with chat's migrations. */
12function fakeD1(): D1Database & { raw: DatabaseSync } {
13 const db = new DatabaseSync(":memory:");
14 const dir = new URL("../migrations/", import.meta.url);
15 for (const file of readdirSync(dir).sort()) db.exec(readFileSync(new URL(file, dir), "utf8"));
16 const statement = (sql: string, params: unknown[] = []): Record<string, unknown> => ({
17 bind: (...values: unknown[]) => statement(sql, values),
18 run: async () => ({ success: true, meta: { changes: Number(db.prepare(sql).run(...(params as never[])).changes) }, results: [] }),
19 });
20 return { raw: db, prepare: (sql: string) => statement(sql) } as unknown as D1Database & { raw: DatabaseSync };
21}
22
23const AT = "2026-10-20T12:00:00.000Z";
24const ANA = "user:u_ana";
25
26/** Ana's direct message with g1t: her question (msg_01) and g1t's answer (msg_02), unread from msg_00 on. */
27function dm() {
28 const d1 = fakeD1();
29 d1.raw
30 .prepare("INSERT INTO channels (id, workspace_id, kind, name, created_by, created_at, last_message_at) VALUES ('chn_dm', 'wsp_1', 'dm', NULL, ?, ?, ?)")
31 .run(ANA, AT, AT);
32 for (const principal of [ANA, G1T_KEY])
33 d1.raw.prepare("INSERT INTO channel_members (channel_id, principal, last_read_id, joined_at) VALUES ('chn_dm', ?, 'msg_00', ?)").run(principal, AT);
34 for (const [id, author] of [
35 ["msg_01", ANA],
36 ["msg_02", G1T_KEY],
37 ])
38 d1.raw.prepare("INSERT INTO messages (id, channel_id, author, body, created_at) VALUES (?, 'chn_dm', ?, 'text', ?)").run(id, author, AT);
39 return d1;
40}
41
42const mark = (d1: ReturnType<typeof dm>) =>
43 (d1.raw.prepare("SELECT last_read_id FROM channel_members WHERE channel_id = 'chn_dm' AND principal = ?").get(ANA) as { last_read_id: string }).last_read_id;
44
45const unread = (d1: ReturnType<typeof dm>) => {
46 const rows = d1.raw.prepare("SELECT channel_id, id, author, NULL AS mentions FROM messages WHERE id > ?").all(mark(d1)) as never[];
47 return tally(rows, ANA, "ana", new Map([["chn_dm", mark(d1)]])).get("chn_dm")?.unread ?? 0;
48};
49
50test("a read from any surface (Home's Summarize, the Ask panel, Chat) moves the read mark, and the answer is no longer unread", async () => {
51 const d1 = dm();
52 assert.equal(unread(d1), 1, "g1t's answer is unread; Ana's own question is not");
53 assert.equal(await markReadUpTo(d1, "chn_dm", ANA, "msg_02"), true);
54 assert.equal(mark(d1), "msg_02");
55 assert.equal(unread(d1), 0);
56});
57
58test("the read mark only moves forward, and says when it did not move", async () => {
59 const d1 = dm();
60 await markReadUpTo(d1, "chn_dm", ANA, "msg_02");
61 assert.equal(await markReadUpTo(d1, "chn_dm", ANA, "msg_01"), false, "an older surface reading late does not mark the answer unread");
62 assert.equal(await markReadUpTo(d1, "chn_dm", ANA, "msg_02"), false, "the same read twice is told once");
63 assert.equal(mark(d1), "msg_02");
64});
65
66test("a read marks only the reader's own place", async () => {
67 const d1 = dm();
68 assert.equal(await markReadUpTo(d1, "chn_dm", "user:u_ben", "msg_02"), false, "not a member: nothing moves");
69 await markReadUpTo(d1, "chn_dm", ANA, "msg_02");
70 const g1t = d1.raw.prepare("SELECT last_read_id FROM channel_members WHERE principal = ?").get(G1T_KEY) as { last_read_id: string };
71 assert.equal(g1t.last_read_id, "msg_00");
72});