| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { ChatLiveEvent, ChatMessage } from "@g1t/contracts"; |
| 5 | |
| 6 | import { askedFrom, answerLabel, anyAnswering, applyLive, cutOff, fromG1t, refTarget, suggestions } from "./g1t-chat.ts"; |
| 7 | |
| 8 | const g1t = { kind: "system" as const, id: "g1t", name: "g1t", display_name: "g1t", avatar: null, role: null }; |
| 9 | const ana = { kind: "user" as const, id: "u1", name: "ana", display_name: "Ana", avatar: null, role: null }; |
| 10 | |
| 11 | function message(id: string, extra: Partial<ChatMessage> = {}): ChatMessage { |
| 12 | return { |
| 13 | id, |
| 14 | channel_id: "chn_1", |
| 15 | author: ana, |
| 16 | kind: "text", |
| 17 | body: "", |
| 18 | card: null, |
| 19 | thread_root: null, |
| 20 | reply_count: 0, |
| 21 | last_reply_at: null, |
| 22 | created_at: "2026-10-12T00:00:00Z", |
| 23 | edited_at: null, |
| 24 | deleted_at: null, |
| 25 | ...extra, |
| 26 | }; |
| 27 | } |
| 28 | |
| 29 | test("an answer streams into its message, then the stored one takes over", () => { |
| 30 | let list = [message("msg_1", { body: "What shipped?" })]; |
| 31 | const placeholder = message("msg_2", { author: g1t, answer: { state: "thinking" } }); |
| 32 | list = applyLive(list, { type: "message.created", message: placeholder }, "chn_1"); |
| 33 | assert.ok(anyAnswering(list)); |
| 34 | list = applyLive(list, { type: "message.streaming", channel_id: "chn_1", id: "msg_2", body: "Two", answer: { state: "streaming" } }, "chn_1"); |
| 35 | assert.equal(list[1]!.body, "Two"); |
| 36 | const done: ChatMessage = { ...placeholder, body: "Two pull requests.", answer: { state: "done", provider_label: "Anthropic", model: "m" } }; |
| 37 | list = applyLive(list, { type: "message.updated", message: done }, "chn_1"); |
| 38 | assert.equal(list[1]!.body, "Two pull requests."); |
| 39 | assert.ok(!anyAnswering(list)); |
| 40 | // A frame that arrives after the end changes nothing. |
| 41 | list = applyLive(list, { type: "message.streaming", channel_id: "chn_1", id: "msg_2", body: "Tw", answer: { state: "streaming" } }, "chn_1"); |
| 42 | assert.equal(list[1]!.body, "Two pull requests."); |
| 43 | assert.equal(answerLabel(list[1]!.answer), "Anthropic · m"); |
| 44 | }); |
| 45 | |
| 46 | test("other conversations, thread replies and deletions", () => { |
| 47 | const list = [message("msg_1"), message("msg_2")]; |
| 48 | const elsewhere: ChatLiveEvent = { type: "message.created", message: message("msg_3", { channel_id: "chn_2" }) }; |
| 49 | assert.equal(applyLive(list, elsewhere, "chn_1").length, 2); |
| 50 | assert.equal(applyLive(list, { type: "message.created", message: message("msg_3", { thread_root: "msg_1" }) }, "chn_1").length, 2); |
| 51 | assert.deepEqual(applyLive(list, { type: "message.deleted", channel_id: "chn_1", id: "msg_1" }, "chn_1").map((m) => m.id), ["msg_2"]); |
| 52 | // Out of order arrivals land in order. |
| 53 | assert.deepEqual(applyLive([message("msg_3")], { type: "message.created", message: message("msg_2") }, "chn_1").map((m) => m.id), ["msg_2", "msg_3"]); |
| 54 | }); |
| 55 | |
| 56 | test("who g1t is, and answers cut off at their limit", () => { |
| 57 | assert.ok(fromG1t({ author: g1t })); |
| 58 | assert.ok(!fromG1t({ author: ana })); |
| 59 | assert.ok(cutOff({ state: "done", stop_reason: "max_tokens" })); |
| 60 | assert.equal(answerLabel({ state: "thinking" }), null); |
| 61 | }); |
| 62 | |
| 63 | test("suggestions fit the page", () => { |
| 64 | assert.match(suggestions({ pull: 4, repo: { namespace: "acme", name: "web" } })[0]!, /#4/); |
| 65 | assert.match(suggestions({ issue: 12 }).join(" "), /blocking #12/); |
| 66 | assert.match(suggestions({ repo: { namespace: "acme", name: "web" } })[0]!, /web/); |
| 67 | assert.equal(suggestions({}).length, 4); |
| 68 | }); |
| 69 | |
| 70 | test("links into the site show as what they lead to", () => { |
| 71 | assert.deepEqual(refTarget("/acme/web/issues/12"), { kind: "issue", repo: "acme/web", number: 12, href: "/acme/web/issues/12" }); |
| 72 | assert.deepEqual(refTarget("https://g1t.sh/Acme/web/pull/7#discussion"), { kind: "issue", repo: "acme/web", number: 7, href: "/Acme/web/pull/7#discussion" }); |
| 73 | assert.deepEqual(refTarget("/acme/web/commit/0123abcd"), { kind: "commit", repo: "acme/web", sha: "0123abcd", href: "/acme/web/commit/0123abcd" }); |
| 74 | assert.deepEqual(refTarget("https://g1t.example/acme/-/chat", "https://g1t.example"), { kind: "page", href: "/acme/-/chat" }); |
| 75 | assert.equal(refTarget("https://example.com/acme/web/issues/1"), null); |
| 76 | assert.equal(refTarget("//evil.example/x"), null); |
| 77 | assert.equal(refTarget("javascript:alert(1)"), null); |
| 78 | }); |
| 79 | |
| 80 | test("a question asked on a page says which, and one written in Chat says nothing", () => { |
| 81 | const repo = { namespace: "flagon-io", name: "g1t" }; |
| 82 | assert.deepEqual(askedFrom({ path: "/flagon-io/g1t/pull/8", repo, pull: 8 }), { label: "pull request #8 in flagon-io/g1t", href: "/flagon-io/g1t/pull/8" }); |
| 83 | assert.deepEqual(askedFrom({ repo, issue: 21 }), { label: "issue #21 in flagon-io/g1t", href: "/flagon-io/g1t/issues/21" }); |
| 84 | assert.equal(askedFrom({ path: "/flagon-io/g1t", repo })?.label, "flagon-io/g1t"); |
| 85 | assert.equal(askedFrom({ path: "/flagon-io/-/chat/dm/chn_1" }), null); |
| 86 | assert.equal(askedFrom({ time_zone: "America/Denver" }), null); |
| 87 | assert.equal(askedFrom(null), null); |
| 88 | }); |