Skip to content
96 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { EMPTY_ASK, type AskState, askPage, askReducer, cutOff, history, readLines, restorable } from "./ask.ts";
5
6test("the page says which repository, issue or pull request the person is on", () => {
7 assert.deepEqual(askPage("/Acme/web/issues/12"), { path: "/Acme/web/issues/12", repo: { namespace: "acme", name: "web" }, issue: 12 });
8 assert.deepEqual(askPage("/acme/web/pull/3/files"), { path: "/acme/web/pull/3/files", repo: { namespace: "acme", name: "web" }, pull: 3 });
9 assert.deepEqual(askPage("/acme/web/tree/main"), { path: "/acme/web/tree/main", repo: { namespace: "acme", name: "web" } });
10 assert.deepEqual(askPage("/acme/web/issues"), { path: "/acme/web/issues", repo: { namespace: "acme", name: "web" } });
11 // A workspace's own pages, and g1t's, name no repository.
12 assert.deepEqual(askPage("/acme/-/chat/general"), { path: "/acme/-/chat/general" });
13 assert.deepEqual(askPage("/settings/profile"), { path: "/settings/profile" });
14 assert.deepEqual(askPage("/acme"), { path: "/acme" });
15 assert.deepEqual(askPage(""), { path: "/" });
16});
17
18function run(...actions: Parameters<typeof askReducer>[1][]): AskState {
19 return actions.reduce(askReducer, EMPTY_ASK);
20}
21
22test("an answer streams into the turn after the question", () => {
23 const state = run(
24 { type: "ask", question: " What broke? ", id: "1" },
25 { type: "event", event: { type: "meta", provider: "anthropic", provider_label: "Anthropic", model: "claude-x" } },
26 { type: "event", event: { type: "text", text: "The " } },
27 { type: "event", event: { type: "text", text: "login." } },
28 );
29 assert.equal(state.busy, true);
30 assert.equal(state.turns[0]!.content, "What broke?");
31 assert.deepEqual(
32 { content: state.turns[1]!.content, provider: state.turns[1]!.provider, model: state.turns[1]!.model, status: state.turns[1]!.status },
33 { content: "The login.", provider: "Anthropic", model: "claude-x", status: "streaming" },
34 );
35 const done = askReducer(state, { type: "event", event: { type: "done", stop_reason: "max_tokens" } });
36 assert.equal(done.busy, false);
37 assert.equal(done.turns[1]!.status, "done");
38 assert.ok(cutOff(done.turns[1]!));
39});
40
41test("one question at a time; empty ones are not asked", () => {
42 const asking = run({ type: "ask", question: "a", id: "1" });
43 assert.equal(askReducer(asking, { type: "ask", question: "b", id: "2" }), asking);
44 assert.equal(askReducer(EMPTY_ASK, { type: "ask", question: " ", id: "1" }), EMPTY_ASK);
45});
46
47test("stopping keeps what came; an error says what went wrong; new conversation starts over", () => {
48 const stopped = run({ type: "ask", question: "a", id: "1" }, { type: "event", event: { type: "text", text: "par" } }, { type: "stop" });
49 assert.equal(stopped.busy, false);
50 assert.deepEqual([stopped.turns[1]!.status, stopped.turns[1]!.content], ["stopped", "par"]);
51 // Nothing after the stop lands on the answer.
52 assert.equal(askReducer(stopped, { type: "event", event: { type: "text", text: "tial" } }).turns[1]!.content, "par");
53
54 const failed = run({ type: "ask", question: "a", id: "1" }, { type: "event", event: { type: "error", message: "Anthropic did not accept the credentials." } });
55 assert.equal(failed.turns[1]!.error, "Anthropic did not accept the credentials.");
56 assert.equal(failed.busy, false);
57 assert.equal(run({ type: "ask", question: "a", id: "1" }, { type: "failed", message: "Too many questions." }).turns[1]!.status, "error");
58 assert.equal(askReducer(failed, { type: "reset" }), EMPTY_ASK);
59});
60
61test("what is sent alternates, leaving out questions whose answer failed", () => {
62 const state = run(
63 { type: "ask", question: "one", id: "1" },
64 { type: "event", event: { type: "text", text: "first" } },
65 { type: "event", event: { type: "done", stop_reason: "end_turn" } },
66 { type: "ask", question: "two", id: "2" },
67 { type: "event", event: { type: "error", message: "no" } },
68 { type: "ask", question: "three", id: "3" },
69 { type: "event", event: { type: "text", text: "cut" } },
70 { type: "stop" },
71 );
72 assert.deepEqual(history(state.turns, " four "), [
73 { role: "user", content: "one" },
74 { role: "assistant", content: "first" },
75 { role: "user", content: "three" },
76 { role: "assistant", content: "cut" },
77 { role: "user", content: "four" },
78 ]);
79});
80
81test("lines are read as they arrive, across chunks", () => {
82 const first = readLines("", '{"type":"meta","provider":"openai","provider_label":"OpenAI","model":"gpt-5"}\n{"type":"te');
83 assert.equal(first.events.length, 1);
84 assert.equal(first.rest, '{"type":"te');
85 const second = readLines(first.rest, 'xt","text":"Hi"}\nnot json\n{"type":"other"}\n');
86 assert.deepEqual(second.events, [{ type: "text", text: "Hi" }]);
87 assert.equal(second.rest, "");
88});
89
90test("a conversation kept in the tab comes back stopped where it was", () => {
91 const kept = restorable({ turns: [{ id: "1-q", role: "user", content: "a", status: "done" }, { id: "1-a", role: "assistant", content: "b", status: "streaming" }], busy: true });
92 assert.equal(kept.busy, false);
93 assert.equal(kept.turns[1]!.status, "stopped");
94 assert.deepEqual(restorable("junk"), EMPTY_ASK);
95 assert.deepEqual(restorable({ turns: [{ role: "system", content: "x" }] }).turns, []);
96});