gsd-pi 2.18.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +45 -15
- package/dist/resources/extensions/gsd/auto.ts +276 -19
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +139 -3
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/dist/resources/extensions/gsd/metrics.ts +48 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +73 -0
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/dist/resources/extensions/remote-questions/format.ts +12 -6
- package/dist/resources/extensions/remote-questions/manager.ts +8 -0
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +45 -15
- package/src/resources/extensions/gsd/auto.ts +276 -19
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +139 -3
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/metrics.ts +48 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +73 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/src/resources/extensions/remote-questions/format.ts +12 -6
- package/src/resources/extensions/remote-questions/manager.ts +8 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
resolveModelForComplexity,
|
|
6
|
+
escalateTier,
|
|
7
|
+
defaultRoutingConfig,
|
|
8
|
+
} from "../model-router.js";
|
|
9
|
+
import type { DynamicRoutingConfig, RoutingDecision } from "../model-router.js";
|
|
10
|
+
import type { ClassificationResult } from "../complexity-classifier.js";
|
|
11
|
+
|
|
12
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function makeClassification(tier: "light" | "standard" | "heavy", reason = "test"): ClassificationResult {
|
|
15
|
+
return { tier, reason, downgraded: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const AVAILABLE_MODELS = [
|
|
19
|
+
"claude-opus-4-6",
|
|
20
|
+
"claude-sonnet-4-6",
|
|
21
|
+
"claude-haiku-4-5",
|
|
22
|
+
"gpt-4o-mini",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// ─── Passthrough when disabled ───────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
test("returns configured model when routing is disabled", () => {
|
|
28
|
+
const config = { ...defaultRoutingConfig(), enabled: false };
|
|
29
|
+
const result = resolveModelForComplexity(
|
|
30
|
+
makeClassification("light"),
|
|
31
|
+
{ primary: "claude-opus-4-6", fallbacks: [] },
|
|
32
|
+
config,
|
|
33
|
+
AVAILABLE_MODELS,
|
|
34
|
+
);
|
|
35
|
+
assert.equal(result.modelId, "claude-opus-4-6");
|
|
36
|
+
assert.equal(result.wasDowngraded, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("returns configured model when no phase config", () => {
|
|
40
|
+
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
41
|
+
const result = resolveModelForComplexity(
|
|
42
|
+
makeClassification("light"),
|
|
43
|
+
undefined,
|
|
44
|
+
config,
|
|
45
|
+
AVAILABLE_MODELS,
|
|
46
|
+
);
|
|
47
|
+
assert.equal(result.modelId, "");
|
|
48
|
+
assert.equal(result.wasDowngraded, false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ─── Downgrade-only semantics ────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
test("does not downgrade when tier matches configured model tier", () => {
|
|
54
|
+
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
55
|
+
const result = resolveModelForComplexity(
|
|
56
|
+
makeClassification("heavy"),
|
|
57
|
+
{ primary: "claude-opus-4-6", fallbacks: [] },
|
|
58
|
+
config,
|
|
59
|
+
AVAILABLE_MODELS,
|
|
60
|
+
);
|
|
61
|
+
assert.equal(result.modelId, "claude-opus-4-6");
|
|
62
|
+
assert.equal(result.wasDowngraded, false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("does not upgrade beyond configured model", () => {
|
|
66
|
+
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
67
|
+
// Configured model is sonnet (standard), classification says heavy
|
|
68
|
+
const result = resolveModelForComplexity(
|
|
69
|
+
makeClassification("heavy"),
|
|
70
|
+
{ primary: "claude-sonnet-4-6", fallbacks: [] },
|
|
71
|
+
config,
|
|
72
|
+
AVAILABLE_MODELS,
|
|
73
|
+
);
|
|
74
|
+
assert.equal(result.modelId, "claude-sonnet-4-6");
|
|
75
|
+
assert.equal(result.wasDowngraded, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("downgrades from opus to haiku for light tier", () => {
|
|
79
|
+
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
80
|
+
const result = resolveModelForComplexity(
|
|
81
|
+
makeClassification("light"),
|
|
82
|
+
{ primary: "claude-opus-4-6", fallbacks: [] },
|
|
83
|
+
config,
|
|
84
|
+
AVAILABLE_MODELS,
|
|
85
|
+
);
|
|
86
|
+
// Should pick haiku or gpt-4o-mini (cheapest light tier)
|
|
87
|
+
assert.ok(
|
|
88
|
+
result.modelId === "claude-haiku-4-5" || result.modelId === "gpt-4o-mini",
|
|
89
|
+
`Expected light-tier model, got ${result.modelId}`,
|
|
90
|
+
);
|
|
91
|
+
assert.equal(result.wasDowngraded, true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("downgrades from opus to sonnet for standard tier", () => {
|
|
95
|
+
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
96
|
+
const result = resolveModelForComplexity(
|
|
97
|
+
makeClassification("standard"),
|
|
98
|
+
{ primary: "claude-opus-4-6", fallbacks: [] },
|
|
99
|
+
config,
|
|
100
|
+
AVAILABLE_MODELS,
|
|
101
|
+
);
|
|
102
|
+
assert.equal(result.modelId, "claude-sonnet-4-6");
|
|
103
|
+
assert.equal(result.wasDowngraded, true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── Explicit tier_models ────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
test("uses explicit tier_models when configured", () => {
|
|
109
|
+
const config: DynamicRoutingConfig = {
|
|
110
|
+
...defaultRoutingConfig(),
|
|
111
|
+
enabled: true,
|
|
112
|
+
tier_models: { light: "gpt-4o-mini", standard: "claude-sonnet-4-6" },
|
|
113
|
+
};
|
|
114
|
+
const result = resolveModelForComplexity(
|
|
115
|
+
makeClassification("light"),
|
|
116
|
+
{ primary: "claude-opus-4-6", fallbacks: [] },
|
|
117
|
+
config,
|
|
118
|
+
AVAILABLE_MODELS,
|
|
119
|
+
);
|
|
120
|
+
assert.equal(result.modelId, "gpt-4o-mini");
|
|
121
|
+
assert.equal(result.wasDowngraded, true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ─── Fallback chain construction ─────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
test("fallback chain includes configured primary as last resort", () => {
|
|
127
|
+
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
128
|
+
const result = resolveModelForComplexity(
|
|
129
|
+
makeClassification("light"),
|
|
130
|
+
{ primary: "claude-opus-4-6", fallbacks: ["claude-sonnet-4-6"] },
|
|
131
|
+
config,
|
|
132
|
+
AVAILABLE_MODELS,
|
|
133
|
+
);
|
|
134
|
+
assert.ok(result.wasDowngraded);
|
|
135
|
+
// Fallbacks should include the configured fallbacks and primary
|
|
136
|
+
assert.ok(result.fallbacks.includes("claude-opus-4-6"), "primary should be in fallbacks");
|
|
137
|
+
assert.ok(result.fallbacks.includes("claude-sonnet-4-6"), "configured fallback should be in fallbacks");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── Escalation ──────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
test("escalateTier moves light → standard", () => {
|
|
143
|
+
assert.equal(escalateTier("light"), "standard");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("escalateTier moves standard → heavy", () => {
|
|
147
|
+
assert.equal(escalateTier("standard"), "heavy");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("escalateTier returns null for heavy (max)", () => {
|
|
151
|
+
assert.equal(escalateTier("heavy"), null);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── No suitable model available ─────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
test("falls back to configured model when no light-tier model available", () => {
|
|
157
|
+
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
158
|
+
// Only heavy-tier models available
|
|
159
|
+
const result = resolveModelForComplexity(
|
|
160
|
+
makeClassification("light"),
|
|
161
|
+
{ primary: "claude-opus-4-6", fallbacks: [] },
|
|
162
|
+
config,
|
|
163
|
+
["claude-opus-4-6"],
|
|
164
|
+
);
|
|
165
|
+
assert.equal(result.modelId, "claude-opus-4-6");
|
|
166
|
+
assert.equal(result.wasDowngraded, false);
|
|
167
|
+
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseSlackReply, parseDiscordResponse, formatForDiscord } from "../../remote-questions/format.ts";
|
|
4
7
|
import { resolveRemoteConfig, isValidChannelId } from "../../remote-questions/config.ts";
|
|
5
8
|
import { sanitizeError } from "../../remote-questions/manager.ts";
|
|
6
9
|
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
7
13
|
test("parseSlackReply handles single-number single-question answers", () => {
|
|
8
14
|
const result = parseSlackReply("2", [{
|
|
9
15
|
id: "choice",
|
|
@@ -153,3 +159,223 @@ test("sanitizeError preserves short safe messages", () => {
|
|
|
153
159
|
assert.equal(sanitizeError("Connection refused"), "Connection refused");
|
|
154
160
|
});
|
|
155
161
|
|
|
162
|
+
|
|
163
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
164
|
+
// Discord Parity Tests
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
166
|
+
|
|
167
|
+
test("formatForDiscord includes context source in footer when present", () => {
|
|
168
|
+
const prompt = {
|
|
169
|
+
id: "test-1",
|
|
170
|
+
channel: "discord" as const,
|
|
171
|
+
createdAt: Date.now(),
|
|
172
|
+
timeoutAt: Date.now() + 60000,
|
|
173
|
+
pollIntervalMs: 5000,
|
|
174
|
+
context: { source: "auto-mode-dispatch" },
|
|
175
|
+
questions: [{
|
|
176
|
+
id: "q1",
|
|
177
|
+
header: "Confirm",
|
|
178
|
+
question: "Proceed?",
|
|
179
|
+
options: [
|
|
180
|
+
{ label: "Yes", description: "Continue" },
|
|
181
|
+
{ label: "No", description: "Stop" },
|
|
182
|
+
],
|
|
183
|
+
allowMultiple: false,
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const { embeds } = formatForDiscord(prompt);
|
|
188
|
+
assert.equal(embeds.length, 1);
|
|
189
|
+
assert.ok(embeds[0].footer?.text.includes("auto-mode-dispatch"), "footer should include context source");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("formatForDiscord omits source from footer when context is absent", () => {
|
|
193
|
+
const prompt = {
|
|
194
|
+
id: "test-2",
|
|
195
|
+
channel: "discord" as const,
|
|
196
|
+
createdAt: Date.now(),
|
|
197
|
+
timeoutAt: Date.now() + 60000,
|
|
198
|
+
pollIntervalMs: 5000,
|
|
199
|
+
questions: [{
|
|
200
|
+
id: "q1",
|
|
201
|
+
header: "Choice",
|
|
202
|
+
question: "Pick one",
|
|
203
|
+
options: [
|
|
204
|
+
{ label: "A", description: "Alpha" },
|
|
205
|
+
{ label: "B", description: "Beta" },
|
|
206
|
+
],
|
|
207
|
+
allowMultiple: false,
|
|
208
|
+
}],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const { embeds } = formatForDiscord(prompt);
|
|
212
|
+
assert.ok(!embeds[0].footer?.text.includes("Source:"), "footer should not include Source when context absent");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("formatForDiscord multi-question footer includes question position", () => {
|
|
216
|
+
const prompt = {
|
|
217
|
+
id: "test-3",
|
|
218
|
+
channel: "discord" as const,
|
|
219
|
+
createdAt: Date.now(),
|
|
220
|
+
timeoutAt: Date.now() + 60000,
|
|
221
|
+
pollIntervalMs: 5000,
|
|
222
|
+
questions: [
|
|
223
|
+
{
|
|
224
|
+
id: "q1",
|
|
225
|
+
header: "First",
|
|
226
|
+
question: "Pick",
|
|
227
|
+
options: [{ label: "A", description: "a" }],
|
|
228
|
+
allowMultiple: false,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "q2",
|
|
232
|
+
header: "Second",
|
|
233
|
+
question: "Pick",
|
|
234
|
+
options: [{ label: "B", description: "b" }],
|
|
235
|
+
allowMultiple: false,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const { embeds } = formatForDiscord(prompt);
|
|
241
|
+
assert.equal(embeds.length, 2);
|
|
242
|
+
assert.ok(embeds[0].footer?.text.includes("1/2"), "first embed footer should show 1/2");
|
|
243
|
+
assert.ok(embeds[1].footer?.text.includes("2/2"), "second embed footer should show 2/2");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("formatForDiscord single-question generates reaction emojis", () => {
|
|
247
|
+
const prompt = {
|
|
248
|
+
id: "test-4",
|
|
249
|
+
channel: "discord" as const,
|
|
250
|
+
createdAt: Date.now(),
|
|
251
|
+
timeoutAt: Date.now() + 60000,
|
|
252
|
+
pollIntervalMs: 5000,
|
|
253
|
+
questions: [{
|
|
254
|
+
id: "q1",
|
|
255
|
+
header: "Pick",
|
|
256
|
+
question: "Choose",
|
|
257
|
+
options: [
|
|
258
|
+
{ label: "A", description: "a" },
|
|
259
|
+
{ label: "B", description: "b" },
|
|
260
|
+
{ label: "C", description: "c" },
|
|
261
|
+
],
|
|
262
|
+
allowMultiple: false,
|
|
263
|
+
}],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const { reactionEmojis } = formatForDiscord(prompt);
|
|
267
|
+
assert.equal(reactionEmojis.length, 3, "should generate 3 reaction emojis for 3 options");
|
|
268
|
+
assert.equal(reactionEmojis[0], "1️⃣");
|
|
269
|
+
assert.equal(reactionEmojis[1], "2️⃣");
|
|
270
|
+
assert.equal(reactionEmojis[2], "3️⃣");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("formatForDiscord multi-question generates no reaction emojis", () => {
|
|
274
|
+
const prompt = {
|
|
275
|
+
id: "test-5",
|
|
276
|
+
channel: "discord" as const,
|
|
277
|
+
createdAt: Date.now(),
|
|
278
|
+
timeoutAt: Date.now() + 60000,
|
|
279
|
+
pollIntervalMs: 5000,
|
|
280
|
+
questions: [
|
|
281
|
+
{
|
|
282
|
+
id: "q1",
|
|
283
|
+
header: "First",
|
|
284
|
+
question: "Pick",
|
|
285
|
+
options: [{ label: "A", description: "a" }],
|
|
286
|
+
allowMultiple: false,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "q2",
|
|
290
|
+
header: "Second",
|
|
291
|
+
question: "Pick",
|
|
292
|
+
options: [{ label: "B", description: "b" }],
|
|
293
|
+
allowMultiple: false,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const { reactionEmojis } = formatForDiscord(prompt);
|
|
299
|
+
assert.equal(reactionEmojis.length, 0, "multi-question should not generate reaction emojis");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("parseDiscordResponse handles multi-question text reply via semicolons", () => {
|
|
303
|
+
const result = parseDiscordResponse([], "1;2", [
|
|
304
|
+
{
|
|
305
|
+
id: "first",
|
|
306
|
+
header: "First",
|
|
307
|
+
question: "Pick one",
|
|
308
|
+
allowMultiple: false,
|
|
309
|
+
options: [
|
|
310
|
+
{ label: "Alpha", description: "A" },
|
|
311
|
+
{ label: "Beta", description: "B" },
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "second",
|
|
316
|
+
header: "Second",
|
|
317
|
+
question: "Pick one",
|
|
318
|
+
allowMultiple: false,
|
|
319
|
+
options: [
|
|
320
|
+
{ label: "Gamma", description: "G" },
|
|
321
|
+
{ label: "Delta", description: "D" },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
assert.deepEqual(result.answers.first.answers, ["Alpha"]);
|
|
327
|
+
assert.deepEqual(result.answers.second.answers, ["Delta"]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("parseDiscordResponse handles multiple reactions for allowMultiple question", () => {
|
|
331
|
+
const result = parseDiscordResponse(
|
|
332
|
+
[{ emoji: "1️⃣", count: 1 }, { emoji: "3️⃣", count: 1 }],
|
|
333
|
+
null,
|
|
334
|
+
[{
|
|
335
|
+
id: "choice",
|
|
336
|
+
header: "Choice",
|
|
337
|
+
question: "Pick any",
|
|
338
|
+
allowMultiple: true,
|
|
339
|
+
options: [
|
|
340
|
+
{ label: "Alpha", description: "A" },
|
|
341
|
+
{ label: "Beta", description: "B" },
|
|
342
|
+
{ label: "Gamma", description: "G" },
|
|
343
|
+
],
|
|
344
|
+
}],
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
assert.deepEqual(result.answers.choice.answers, ["Alpha", "Gamma"]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("DiscordAdapter source-level: acknowledgeAnswer method exists", () => {
|
|
351
|
+
const adapterSrc = readFileSync(
|
|
352
|
+
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|
|
353
|
+
"utf-8",
|
|
354
|
+
);
|
|
355
|
+
assert.ok(adapterSrc.includes("async acknowledgeAnswer"), "should have acknowledgeAnswer method");
|
|
356
|
+
assert.ok(adapterSrc.includes("✅"), "should use checkmark emoji for acknowledgement");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("DiscordAdapter source-level: resolves guild ID for message URLs", () => {
|
|
360
|
+
const adapterSrc = readFileSync(
|
|
361
|
+
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|
|
362
|
+
"utf-8",
|
|
363
|
+
);
|
|
364
|
+
assert.ok(adapterSrc.includes("guildId"), "should track guild ID");
|
|
365
|
+
assert.ok(adapterSrc.includes("guild_id"), "should read guild_id from channel info");
|
|
366
|
+
assert.ok(
|
|
367
|
+
adapterSrc.includes("discord.com/channels/"),
|
|
368
|
+
"should construct message URL with guild/channel/message format",
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
|
|
373
|
+
const adapterSrc = readFileSync(
|
|
374
|
+
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|
|
375
|
+
"utf-8",
|
|
376
|
+
);
|
|
377
|
+
assert.ok(
|
|
378
|
+
adapterSrc.includes("threadUrl: messageUrl"),
|
|
379
|
+
"sendPrompt should set threadUrl to the constructed message URL",
|
|
380
|
+
);
|
|
381
|
+
});
|