-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathreply-tracker.test.ts
More file actions
123 lines (98 loc) · 4.81 KB
/
Copy pathreply-tracker.test.ts
File metadata and controls
123 lines (98 loc) · 4.81 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
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("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/);
});