forked from nicobailon/pi-intercom
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreply-tracker.test.ts
More file actions
134 lines (107 loc) · 5.4 KB
/
Copy pathreply-tracker.test.ts
File metadata and controls
134 lines (107 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import test from "node:test";
import assert from "node:assert/strict";
import { getAskTimeoutMs } from "./config.ts";
import { ReplyTracker } from "./reply-tracker.ts";
import type { Message, SessionInfo } from "./types.ts";
function createSession(id: string, name: string): SessionInfo {
return {
id,
name,
cwd: "/tmp/project",
model: "test-model",
pid: 1,
startedAt: 1,
lastActivity: 1,
};
}
function createMessage(id: string, text: string, expectsReply = true): Message {
return {
id,
timestamp: 1,
expectsReply,
content: { text },
};
}
test("reply resolves from current triggered message context", () => {
const tracker = new ReplyTracker();
const from = createSession("planner-id", "planner");
const message = createMessage("ask-1", "Need a decision");
const context = tracker.recordIncomingMessage(from, message, 1000);
tracker.queueTurnContext(context);
tracker.beginTurn(1001);
assert.equal(tracker.resolveReplyTarget({}, 1002).message.id, "ask-1");
assert.equal(tracker.resolveReplyTarget({}, 1002).from.id, "planner-id");
});
test("reply resolves from single pending ask without current turn context", () => {
const tracker = new ReplyTracker();
tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "Need a decision"), 1000);
assert.equal(tracker.resolveReplyTarget({}, 1001).message.id, "ask-1");
});
test("reply with to resolves matching pending ask", () => {
const tracker = new ReplyTracker();
tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "First"), 1000);
tracker.recordIncomingMessage(createSession("reviewer-id", "reviewer"), createMessage("ask-2", "Second"), 1001);
assert.equal(tracker.resolveReplyTarget({ to: "reviewer" }, 1002).message.id, "ask-2");
assert.equal(tracker.resolveReplyTarget({ to: "planner-id" }, 1002).message.id, "ask-1");
});
test("explicit to overrides the current turn context", () => {
const tracker = new ReplyTracker();
const current = tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "First"), 1000);
tracker.recordIncomingMessage(createSession("reviewer-id", "reviewer"), createMessage("ask-2", "Second"), 1001);
tracker.queueTurnContext(current);
tracker.beginTurn(1002);
assert.equal(tracker.resolveReplyTarget({ to: "reviewer" }, 1003).message.id, "ask-2");
assert.throws(() => tracker.resolveReplyTarget({ to: "missing" }, 1003), /No pending ask from/);
});
test("replyTo resolves the exact pending ask", () => {
const tracker = new ReplyTracker();
tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "First"), 1000);
tracker.recordIncomingMessage(createSession("reviewer-id", "reviewer"), createMessage("ask-2", "Second"), 1001);
assert.equal(tracker.resolveReplyTarget({ replyTo: "ask-2" }, 1002).from.id, "reviewer-id");
assert.throws(() => tracker.resolveReplyTarget({ to: "planner", replyTo: "ask-2" }, 1002), /is not from/);
});
test("reply errors when no context and no pending asks", () => {
const tracker = new ReplyTracker();
assert.throws(() => tracker.resolveReplyTarget({}, 1000), /No active intercom context to reply to/);
});
test("reply errors when multiple pending asks and no to", () => {
const tracker = new ReplyTracker();
tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "First"), 1000);
tracker.recordIncomingMessage(createSession("reviewer-id", "reviewer"), createMessage("ask-2", "Second"), 1001);
assert.throws(() => tracker.resolveReplyTarget({}, 1002), /Multiple pending asks — specify `to`/);
});
test("reply removes pending ask after successful reply", () => {
const tracker = new ReplyTracker();
tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "Need a decision"), 1000);
tracker.markReplied("ask-1");
assert.deepEqual(tracker.listPending(1001), []);
});
test("ask timeout can be configured from environment", () => {
const previous = process.env.PI_INTERCOM_ASK_TIMEOUT_MS;
process.env.PI_INTERCOM_ASK_TIMEOUT_MS = "42";
try {
assert.equal(getAskTimeoutMs(), 42);
assert.throws(() => {
process.env.PI_INTERCOM_ASK_TIMEOUT_MS = "0";
getAskTimeoutMs();
}, /positive integer/);
} finally {
if (previous === undefined) delete process.env.PI_INTERCOM_ASK_TIMEOUT_MS;
else process.env.PI_INTERCOM_ASK_TIMEOUT_MS = previous;
}
});
test("pending asks can be explicitly dismissed without removing retryable failures", () => {
const tracker = new ReplyTracker();
tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "Need a decision"), 1000);
tracker.recordIncomingMessage(createSession("reviewer-id", "reviewer"), createMessage("ask-2", "Retryable"), 1001);
tracker.dismissPendingAsk("ask-1");
assert.deepEqual(tracker.listPending(1002).map((context) => context.message.id), ["ask-2"]);
});
test("dismissing a pending ask removes queued turn context", () => {
const tracker = new ReplyTracker();
const context = tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "Need a decision"), 1000);
tracker.queueTurnContext(context);
tracker.dismissPendingAsk("ask-1");
tracker.beginTurn(1001);
assert.throws(() => tracker.resolveReplyTarget({}, 1002), /No active intercom context to reply to/);
});