gsd-pi 2.19.0 → 2.20.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/README.md +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +424 -30
- package/dist/resources/extensions/gsd/commands.ts +518 -36
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +39 -3
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/dist/resources/extensions/gsd/preferences.ts +125 -150
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/dist/resources/extensions/remote-questions/format.ts +154 -8
- package/dist/resources/extensions/remote-questions/manager.ts +9 -7
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +424 -30
- package/src/resources/extensions/gsd/commands.ts +518 -36
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +39 -3
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/src/resources/extensions/gsd/preferences.ts +125 -150
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/src/resources/extensions/remote-questions/format.ts +154 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -7
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// prompt-db: Tests for DB-aware inline helpers (inlineDecisionsFromDb, inlineRequirementsFromDb, inlineProjectFromDb)
|
|
2
|
+
//
|
|
3
|
+
// Validates:
|
|
4
|
+
// (a) DB-aware helpers return scoped content when DB has data
|
|
5
|
+
// (b) Helpers fall back to non-null output when DB unavailable
|
|
6
|
+
// (c) Scoped filtering actually reduces content
|
|
7
|
+
|
|
8
|
+
import { createTestContext } from './test-helpers.ts';
|
|
9
|
+
import {
|
|
10
|
+
openDatabase,
|
|
11
|
+
closeDatabase,
|
|
12
|
+
isDbAvailable,
|
|
13
|
+
insertDecision,
|
|
14
|
+
insertRequirement,
|
|
15
|
+
insertArtifact,
|
|
16
|
+
} from '../gsd-db.ts';
|
|
17
|
+
import {
|
|
18
|
+
queryDecisions,
|
|
19
|
+
queryRequirements,
|
|
20
|
+
queryProject,
|
|
21
|
+
formatDecisionsForPrompt,
|
|
22
|
+
formatRequirementsForPrompt,
|
|
23
|
+
} from '../context-store.ts';
|
|
24
|
+
|
|
25
|
+
const { assertEq, assertTrue, assertMatch, assertNoMatch, report } = createTestContext();
|
|
26
|
+
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// prompt-db: DB-aware decisions helper returns scoped content
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
console.log('\n=== prompt-db: scoped decisions from DB ===');
|
|
32
|
+
{
|
|
33
|
+
openDatabase(':memory:');
|
|
34
|
+
|
|
35
|
+
// Insert decisions across 3 milestones
|
|
36
|
+
for (let i = 1; i <= 10; i++) {
|
|
37
|
+
const milestoneNum = ((i - 1) % 3) + 1;
|
|
38
|
+
insertDecision({
|
|
39
|
+
id: `D${String(i).padStart(3, '0')}`,
|
|
40
|
+
when_context: `M00${milestoneNum}/S01`,
|
|
41
|
+
scope: 'architecture',
|
|
42
|
+
decision: `decision ${i}`,
|
|
43
|
+
choice: `choice ${i}`,
|
|
44
|
+
rationale: `rationale ${i}`,
|
|
45
|
+
revisable: 'yes',
|
|
46
|
+
superseded_by: null,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Query scoped to M001
|
|
51
|
+
const m001Decisions = queryDecisions({ milestoneId: 'M001' });
|
|
52
|
+
assertTrue(m001Decisions.length > 0, 'M001 decisions should exist');
|
|
53
|
+
assertTrue(m001Decisions.length < 10, `scoped query should return fewer than 10 (got ${m001Decisions.length})`);
|
|
54
|
+
|
|
55
|
+
// Verify all returned decisions are for M001
|
|
56
|
+
for (const d of m001Decisions) {
|
|
57
|
+
assertMatch(d.when_context, /M001/, `decision ${d.id} should be for M001`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Format and verify wrapping
|
|
61
|
+
const formatted = formatDecisionsForPrompt(m001Decisions);
|
|
62
|
+
assertTrue(formatted.length > 0, 'formatted decisions should be non-empty');
|
|
63
|
+
assertMatch(formatted, /\| # \| When \| Scope/, 'formatted decisions have table header');
|
|
64
|
+
|
|
65
|
+
// Verify the expected wrapper format that inlineDecisionsFromDb would produce
|
|
66
|
+
const wrapped = `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${formatted}`;
|
|
67
|
+
assertMatch(wrapped, /^### Decisions/, 'wrapped decisions start with ### Decisions');
|
|
68
|
+
assertMatch(wrapped, /Source:.*DECISIONS\.md/, 'wrapped decisions have source path');
|
|
69
|
+
|
|
70
|
+
closeDatabase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
+
// prompt-db: DB-aware requirements helper returns scoped content
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
76
|
+
|
|
77
|
+
console.log('\n=== prompt-db: scoped requirements from DB ===');
|
|
78
|
+
{
|
|
79
|
+
openDatabase(':memory:');
|
|
80
|
+
|
|
81
|
+
// Insert requirements across different slices
|
|
82
|
+
insertRequirement({
|
|
83
|
+
id: 'R001', class: 'functional', status: 'active',
|
|
84
|
+
description: 'feature A', why: 'needed', source: 'M001', primary_owner: 'S01',
|
|
85
|
+
supporting_slices: '', validation: 'test', notes: '', full_content: '',
|
|
86
|
+
superseded_by: null,
|
|
87
|
+
});
|
|
88
|
+
insertRequirement({
|
|
89
|
+
id: 'R002', class: 'functional', status: 'active',
|
|
90
|
+
description: 'feature B', why: 'needed', source: 'M001', primary_owner: 'S02',
|
|
91
|
+
supporting_slices: 'S01', validation: 'test', notes: '', full_content: '',
|
|
92
|
+
superseded_by: null,
|
|
93
|
+
});
|
|
94
|
+
insertRequirement({
|
|
95
|
+
id: 'R003', class: 'functional', status: 'active',
|
|
96
|
+
description: 'feature C', why: 'needed', source: 'M001', primary_owner: 'S03',
|
|
97
|
+
supporting_slices: '', validation: 'test', notes: '', full_content: '',
|
|
98
|
+
superseded_by: null,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Query scoped to S01 — should get R001 (primary) and R002 (supporting)
|
|
102
|
+
const s01Reqs = queryRequirements({ sliceId: 'S01' });
|
|
103
|
+
assertEq(s01Reqs.length, 2, 'S01 requirements should be 2 (primary + supporting)');
|
|
104
|
+
const ids = s01Reqs.map(r => r.id).sort();
|
|
105
|
+
assertEq(ids, ['R001', 'R002'], 'S01 owns R001 and supports R002');
|
|
106
|
+
|
|
107
|
+
// Unscoped query returns all 3
|
|
108
|
+
const allReqs = queryRequirements();
|
|
109
|
+
assertEq(allReqs.length, 3, 'unscoped requirements should return all 3');
|
|
110
|
+
|
|
111
|
+
// Format and verify wrapping
|
|
112
|
+
const formatted = formatRequirementsForPrompt(s01Reqs);
|
|
113
|
+
assertTrue(formatted.length > 0, 'formatted requirements should be non-empty');
|
|
114
|
+
assertMatch(formatted, /### R001/, 'formatted requirements include R001');
|
|
115
|
+
assertMatch(formatted, /### R002/, 'formatted requirements include R002');
|
|
116
|
+
assertNoMatch(formatted, /### R003/, 'formatted requirements exclude R003');
|
|
117
|
+
|
|
118
|
+
// Verify the expected wrapper format that inlineRequirementsFromDb would produce
|
|
119
|
+
const wrapped = `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${formatted}`;
|
|
120
|
+
assertMatch(wrapped, /^### Requirements/, 'wrapped requirements start with ### Requirements');
|
|
121
|
+
assertMatch(wrapped, /Source:.*REQUIREMENTS\.md/, 'wrapped requirements have source path');
|
|
122
|
+
|
|
123
|
+
closeDatabase();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
127
|
+
// prompt-db: DB-aware project helper returns content from DB
|
|
128
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
129
|
+
|
|
130
|
+
console.log('\n=== prompt-db: project content from DB ===');
|
|
131
|
+
{
|
|
132
|
+
openDatabase(':memory:');
|
|
133
|
+
|
|
134
|
+
insertArtifact({
|
|
135
|
+
path: 'PROJECT.md',
|
|
136
|
+
artifact_type: 'project',
|
|
137
|
+
milestone_id: null,
|
|
138
|
+
slice_id: null,
|
|
139
|
+
task_id: null,
|
|
140
|
+
full_content: '# Test Project\n\nThis is the project description.',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const content = queryProject();
|
|
144
|
+
assertEq(content, '# Test Project\n\nThis is the project description.', 'queryProject returns content');
|
|
145
|
+
|
|
146
|
+
// Verify the expected wrapper format that inlineProjectFromDb would produce
|
|
147
|
+
const wrapped = `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${content}`;
|
|
148
|
+
assertMatch(wrapped, /^### Project/, 'wrapped project starts with ### Project');
|
|
149
|
+
assertMatch(wrapped, /Source:.*PROJECT\.md/, 'wrapped project has source path');
|
|
150
|
+
assertMatch(wrapped, /# Test Project/, 'wrapped project includes content');
|
|
151
|
+
|
|
152
|
+
closeDatabase();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// prompt-db: fallback when DB unavailable
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
console.log('\n=== prompt-db: fallback when DB unavailable ===');
|
|
160
|
+
{
|
|
161
|
+
closeDatabase();
|
|
162
|
+
assertTrue(!isDbAvailable(), 'DB should not be available');
|
|
163
|
+
|
|
164
|
+
// queryDecisions returns [] when DB closed — helper would fall back
|
|
165
|
+
const decisions = queryDecisions({ milestoneId: 'M001' });
|
|
166
|
+
assertEq(decisions, [], 'queryDecisions returns [] when DB closed');
|
|
167
|
+
|
|
168
|
+
// queryRequirements returns [] when DB closed — helper would fall back
|
|
169
|
+
const requirements = queryRequirements({ sliceId: 'S01' });
|
|
170
|
+
assertEq(requirements, [], 'queryRequirements returns [] when DB closed');
|
|
171
|
+
|
|
172
|
+
// queryProject returns null when DB closed — helper would fall back
|
|
173
|
+
const project = queryProject();
|
|
174
|
+
assertEq(project, null, 'queryProject returns null when DB closed');
|
|
175
|
+
|
|
176
|
+
// formatDecisionsForPrompt returns '' for empty input
|
|
177
|
+
const formatted = formatDecisionsForPrompt([]);
|
|
178
|
+
assertEq(formatted, '', 'formatDecisionsForPrompt returns empty for empty input');
|
|
179
|
+
|
|
180
|
+
// formatRequirementsForPrompt returns '' for empty input
|
|
181
|
+
const formattedReqs = formatRequirementsForPrompt([]);
|
|
182
|
+
assertEq(formattedReqs, '', 'formatRequirementsForPrompt returns empty for empty input');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
+
// prompt-db: scoped filtering reduces content vs unscoped
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
+
|
|
189
|
+
console.log('\n=== prompt-db: scoped filtering reduces content ===');
|
|
190
|
+
{
|
|
191
|
+
openDatabase(':memory:');
|
|
192
|
+
|
|
193
|
+
// Insert 10 decisions across 3 milestones
|
|
194
|
+
for (let i = 1; i <= 10; i++) {
|
|
195
|
+
const milestoneNum = ((i - 1) % 3) + 1;
|
|
196
|
+
insertDecision({
|
|
197
|
+
id: `D${String(i).padStart(3, '0')}`,
|
|
198
|
+
when_context: `M00${milestoneNum}/S01`,
|
|
199
|
+
scope: 'architecture',
|
|
200
|
+
decision: `decision ${i} with some lengthy description for token measurement`,
|
|
201
|
+
choice: `choice ${i}`,
|
|
202
|
+
rationale: `rationale ${i} with additional context`,
|
|
203
|
+
revisable: 'yes',
|
|
204
|
+
superseded_by: null,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const allDecisions = queryDecisions();
|
|
209
|
+
const m001Decisions = queryDecisions({ milestoneId: 'M001' });
|
|
210
|
+
|
|
211
|
+
assertEq(allDecisions.length, 10, 'unscoped returns all 10 decisions');
|
|
212
|
+
assertTrue(m001Decisions.length < 10, `M001-scoped returns fewer than 10 (got ${m001Decisions.length})`);
|
|
213
|
+
assertTrue(m001Decisions.length > 0, 'M001-scoped returns at least 1');
|
|
214
|
+
|
|
215
|
+
// Format both and compare sizes — scoped should be shorter
|
|
216
|
+
const allFormatted = formatDecisionsForPrompt(allDecisions);
|
|
217
|
+
const scopedFormatted = formatDecisionsForPrompt(m001Decisions);
|
|
218
|
+
|
|
219
|
+
assertTrue(
|
|
220
|
+
scopedFormatted.length < allFormatted.length,
|
|
221
|
+
`scoped content (${scopedFormatted.length} chars) should be shorter than unscoped (${allFormatted.length} chars)`,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Insert requirements across 4 slices
|
|
225
|
+
for (let i = 1; i <= 8; i++) {
|
|
226
|
+
const sliceNum = ((i - 1) % 4) + 1;
|
|
227
|
+
insertRequirement({
|
|
228
|
+
id: `R${String(i).padStart(3, '0')}`,
|
|
229
|
+
class: 'functional',
|
|
230
|
+
status: 'active',
|
|
231
|
+
description: `requirement ${i} with detailed description`,
|
|
232
|
+
why: `justification ${i}`,
|
|
233
|
+
source: 'M001',
|
|
234
|
+
primary_owner: `S0${sliceNum}`,
|
|
235
|
+
supporting_slices: '',
|
|
236
|
+
validation: `validation ${i}`,
|
|
237
|
+
notes: '',
|
|
238
|
+
full_content: '',
|
|
239
|
+
superseded_by: null,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const allReqs = queryRequirements();
|
|
244
|
+
const s01Reqs = queryRequirements({ sliceId: 'S01' });
|
|
245
|
+
|
|
246
|
+
assertEq(allReqs.length, 8, 'unscoped returns all 8 requirements');
|
|
247
|
+
assertTrue(s01Reqs.length < 8, `S01-scoped returns fewer than 8 (got ${s01Reqs.length})`);
|
|
248
|
+
assertTrue(s01Reqs.length > 0, 'S01-scoped returns at least 1');
|
|
249
|
+
|
|
250
|
+
const allReqsFormatted = formatRequirementsForPrompt(allReqs);
|
|
251
|
+
const scopedReqsFormatted = formatRequirementsForPrompt(s01Reqs);
|
|
252
|
+
|
|
253
|
+
assertTrue(
|
|
254
|
+
scopedReqsFormatted.length < allReqsFormatted.length,
|
|
255
|
+
`scoped requirements (${scopedReqsFormatted.length} chars) should be shorter than unscoped (${allReqsFormatted.length} chars)`,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
closeDatabase();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
262
|
+
// prompt-db: DB helpers produce correct wrapper format
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
264
|
+
|
|
265
|
+
console.log('\n=== prompt-db: DB helpers wrapper format matches expected pattern ===');
|
|
266
|
+
{
|
|
267
|
+
openDatabase(':memory:');
|
|
268
|
+
|
|
269
|
+
insertDecision({
|
|
270
|
+
id: 'D001', when_context: 'M001/S01', scope: 'architecture',
|
|
271
|
+
decision: 'use SQLite', choice: 'better-sqlite3', rationale: 'fast',
|
|
272
|
+
revisable: 'yes', superseded_by: null,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
insertRequirement({
|
|
276
|
+
id: 'R001', class: 'functional', status: 'active',
|
|
277
|
+
description: 'persist decisions', why: 'memory', source: 'M001',
|
|
278
|
+
primary_owner: 'S01', supporting_slices: '', validation: 'test',
|
|
279
|
+
notes: '', full_content: '', superseded_by: null,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
insertArtifact({
|
|
283
|
+
path: 'PROJECT.md',
|
|
284
|
+
artifact_type: 'project',
|
|
285
|
+
milestone_id: null,
|
|
286
|
+
slice_id: null,
|
|
287
|
+
task_id: null,
|
|
288
|
+
full_content: '# Project Name\n\nDescription.',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Simulate what inlineDecisionsFromDb does
|
|
292
|
+
const decisions = queryDecisions({ milestoneId: 'M001' });
|
|
293
|
+
assertTrue(decisions.length === 1, 'got 1 decision for M001');
|
|
294
|
+
const dFormatted = formatDecisionsForPrompt(decisions);
|
|
295
|
+
const dWrapped = `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${dFormatted}`;
|
|
296
|
+
assertMatch(dWrapped, /^### Decisions\nSource: `.gsd\/DECISIONS\.md`\n\n\| #/, 'decisions wrapper format correct');
|
|
297
|
+
|
|
298
|
+
// Simulate what inlineRequirementsFromDb does
|
|
299
|
+
const reqs = queryRequirements({ sliceId: 'S01' });
|
|
300
|
+
assertTrue(reqs.length === 1, 'got 1 requirement for S01');
|
|
301
|
+
const rFormatted = formatRequirementsForPrompt(reqs);
|
|
302
|
+
const rWrapped = `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${rFormatted}`;
|
|
303
|
+
assertMatch(rWrapped, /^### Requirements\nSource: `.gsd\/REQUIREMENTS\.md`\n\n### R001/, 'requirements wrapper format correct');
|
|
304
|
+
|
|
305
|
+
// Simulate what inlineProjectFromDb does
|
|
306
|
+
const project = queryProject();
|
|
307
|
+
assertTrue(project !== null, 'project content exists');
|
|
308
|
+
const pWrapped = `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${project}`;
|
|
309
|
+
assertMatch(pWrapped, /^### Project\nSource: `.gsd\/PROJECT\.md`\n\n# Project Name/, 'project wrapper format correct');
|
|
310
|
+
|
|
311
|
+
closeDatabase();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
315
|
+
// prompt-db: re-import updates DB when source markdown changes
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
317
|
+
|
|
318
|
+
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
319
|
+
import { join } from 'node:path';
|
|
320
|
+
import { tmpdir } from 'node:os';
|
|
321
|
+
import { migrateFromMarkdown } from '../md-importer.ts';
|
|
322
|
+
|
|
323
|
+
console.log('\n=== prompt-db: re-import updates DB when source markdown changes ===');
|
|
324
|
+
{
|
|
325
|
+
// Create a temp dir simulating a project with .gsd/DECISIONS.md
|
|
326
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'prompt-db-reimport-'));
|
|
327
|
+
const gsdDir = join(tmpDir, '.gsd');
|
|
328
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
329
|
+
|
|
330
|
+
// Write initial DECISIONS.md with 2 decisions
|
|
331
|
+
const initialDecisions = `# Decisions Register
|
|
332
|
+
|
|
333
|
+
| # | When | Scope | Decision | Choice | Rationale | Revisable? |
|
|
334
|
+
|---|------|-------|----------|--------|-----------|------------|
|
|
335
|
+
| D001 | M001/S01 | architecture | use SQLite | better-sqlite3 | fast and embedded | yes |
|
|
336
|
+
| D002 | M001/S01 | tooling | use vitest | vitest | modern test runner | yes |
|
|
337
|
+
`;
|
|
338
|
+
writeFileSync(join(gsdDir, 'DECISIONS.md'), initialDecisions);
|
|
339
|
+
|
|
340
|
+
// Open in-memory DB and do initial import
|
|
341
|
+
openDatabase(':memory:');
|
|
342
|
+
migrateFromMarkdown(tmpDir);
|
|
343
|
+
|
|
344
|
+
// Verify initial state: 2 decisions
|
|
345
|
+
const initial = queryDecisions();
|
|
346
|
+
assertEq(initial.length, 2, 're-import: initial import has 2 decisions');
|
|
347
|
+
const initialIds = initial.map(d => d.id).sort();
|
|
348
|
+
assertEq(initialIds, ['D001', 'D002'], 're-import: initial decisions are D001, D002');
|
|
349
|
+
|
|
350
|
+
// Now "the LLM modifies DECISIONS.md" — add a third decision
|
|
351
|
+
const updatedDecisions = `# Decisions Register
|
|
352
|
+
|
|
353
|
+
| # | When | Scope | Decision | Choice | Rationale | Revisable? |
|
|
354
|
+
|---|------|-------|----------|--------|-----------|------------|
|
|
355
|
+
| D001 | M001/S01 | architecture | use SQLite | better-sqlite3 | fast and embedded | yes |
|
|
356
|
+
| D002 | M001/S01 | tooling | use vitest | vitest | modern test runner | yes |
|
|
357
|
+
| D003 | M001/S02 | runtime | dynamic imports | D014 pattern | lazy loading | yes |
|
|
358
|
+
`;
|
|
359
|
+
writeFileSync(join(gsdDir, 'DECISIONS.md'), updatedDecisions);
|
|
360
|
+
|
|
361
|
+
// Re-import (simulating what handleAgentEnd does)
|
|
362
|
+
migrateFromMarkdown(tmpDir);
|
|
363
|
+
|
|
364
|
+
// Verify DB now has 3 decisions
|
|
365
|
+
const afterReimport = queryDecisions();
|
|
366
|
+
assertEq(afterReimport.length, 3, 're-import: after re-import has 3 decisions');
|
|
367
|
+
const afterIds = afterReimport.map(d => d.id).sort();
|
|
368
|
+
assertEq(afterIds, ['D001', 'D002', 'D003'], 're-import: decisions are D001, D002, D003');
|
|
369
|
+
|
|
370
|
+
// Verify the new decision has correct data
|
|
371
|
+
const d003 = afterReimport.find(d => d.id === 'D003');
|
|
372
|
+
assertTrue(d003 !== undefined, 're-import: D003 exists');
|
|
373
|
+
assertEq(d003!.when_context, 'M001/S02', 're-import: D003 when_context is M001/S02');
|
|
374
|
+
assertEq(d003!.scope, 'runtime', 're-import: D003 scope is runtime');
|
|
375
|
+
assertEq(d003!.choice, 'D014 pattern', 're-import: D003 choice is D014 pattern');
|
|
376
|
+
|
|
377
|
+
// Verify scoped query picks up the new decision
|
|
378
|
+
const m001Scoped = queryDecisions({ milestoneId: 'M001' });
|
|
379
|
+
assertTrue(m001Scoped.length === 3, 're-import: all 3 decisions are for M001');
|
|
380
|
+
|
|
381
|
+
closeDatabase();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ─── Final Report ──────────────────────────────────────────────────────────
|
|
385
|
+
report();
|
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { parseSlackReply, parseDiscordResponse, formatForDiscord } from "../../remote-questions/format.ts";
|
|
6
|
+
import { parseSlackReply, parseDiscordResponse, formatForDiscord, formatForSlack, parseSlackReactionResponse, formatForTelegram, parseTelegramResponse } from "../../remote-questions/format.ts";
|
|
7
7
|
import { resolveRemoteConfig, isValidChannelId } from "../../remote-questions/config.ts";
|
|
8
8
|
import { sanitizeError } from "../../remote-questions/manager.ts";
|
|
9
9
|
|
|
@@ -94,6 +94,21 @@ test("parseDiscordResponse rejects multi-question reaction parsing", () => {
|
|
|
94
94
|
assert.match(String(result.answers.second.user_note), /single-question prompts/i);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
test("parseSlackReactionResponse handles single-question reactions", () => {
|
|
98
|
+
const result = parseSlackReactionResponse(["two"], [{
|
|
99
|
+
id: "choice",
|
|
100
|
+
header: "Choice",
|
|
101
|
+
question: "Pick one",
|
|
102
|
+
allowMultiple: false,
|
|
103
|
+
options: [
|
|
104
|
+
{ label: "Alpha", description: "A" },
|
|
105
|
+
{ label: "Beta", description: "B" },
|
|
106
|
+
],
|
|
107
|
+
}]);
|
|
108
|
+
|
|
109
|
+
assert.deepEqual(result, { answers: { choice: { answers: ["Beta"] } } });
|
|
110
|
+
});
|
|
111
|
+
|
|
97
112
|
test("parseSlackReply truncates user_note longer than 500 chars", () => {
|
|
98
113
|
const longText = "x".repeat(600);
|
|
99
114
|
const result = parseSlackReply(longText, [{
|
|
@@ -189,6 +204,65 @@ test("formatForDiscord includes context source in footer when present", () => {
|
|
|
189
204
|
assert.ok(embeds[0].footer?.text.includes("auto-mode-dispatch"), "footer should include context source");
|
|
190
205
|
});
|
|
191
206
|
|
|
207
|
+
test("formatForSlack includes context source when present", () => {
|
|
208
|
+
const blocks = formatForSlack({
|
|
209
|
+
id: "slack-1",
|
|
210
|
+
channel: "slack",
|
|
211
|
+
createdAt: Date.now(),
|
|
212
|
+
timeoutAt: Date.now() + 60000,
|
|
213
|
+
pollIntervalMs: 5000,
|
|
214
|
+
context: { source: "ask_user_questions" },
|
|
215
|
+
questions: [{
|
|
216
|
+
id: "q1",
|
|
217
|
+
header: "Confirm",
|
|
218
|
+
question: "Proceed?",
|
|
219
|
+
options: [
|
|
220
|
+
{ label: "Yes", description: "Continue" },
|
|
221
|
+
{ label: "No", description: "Stop" },
|
|
222
|
+
],
|
|
223
|
+
allowMultiple: false,
|
|
224
|
+
}],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const sourceBlock = blocks.find((block) => block.type === "context" && block.elements?.some((el) => el.text.includes("Source:")));
|
|
228
|
+
assert.ok(sourceBlock, "Slack blocks should include a context source block");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("formatForSlack multi-question prompts explain semicolon and newline reply format", () => {
|
|
232
|
+
const blocks = formatForSlack({
|
|
233
|
+
id: "slack-2",
|
|
234
|
+
channel: "slack",
|
|
235
|
+
createdAt: Date.now(),
|
|
236
|
+
timeoutAt: Date.now() + 60000,
|
|
237
|
+
pollIntervalMs: 5000,
|
|
238
|
+
questions: [
|
|
239
|
+
{
|
|
240
|
+
id: "q1",
|
|
241
|
+
header: "First",
|
|
242
|
+
question: "Pick one",
|
|
243
|
+
options: [
|
|
244
|
+
{ label: "Alpha", description: "A" },
|
|
245
|
+
{ label: "Beta", description: "B" },
|
|
246
|
+
],
|
|
247
|
+
allowMultiple: false,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "q2",
|
|
251
|
+
header: "Second",
|
|
252
|
+
question: "Explain",
|
|
253
|
+
options: [
|
|
254
|
+
{ label: "Gamma", description: "G" },
|
|
255
|
+
{ label: "Delta", description: "D" },
|
|
256
|
+
],
|
|
257
|
+
allowMultiple: false,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const instructionBlock = blocks.find((block) => block.type === "context" && block.elements?.some((el) => el.text.includes("one line per question")));
|
|
263
|
+
assert.ok(instructionBlock, "Slack multi-question prompts should explain one-line or semicolon reply format");
|
|
264
|
+
});
|
|
265
|
+
|
|
192
266
|
test("formatForDiscord omits source from footer when context is absent", () => {
|
|
193
267
|
const prompt = {
|
|
194
268
|
id: "test-2",
|
|
@@ -356,6 +430,27 @@ test("DiscordAdapter source-level: acknowledgeAnswer method exists", () => {
|
|
|
356
430
|
assert.ok(adapterSrc.includes("✅"), "should use checkmark emoji for acknowledgement");
|
|
357
431
|
});
|
|
358
432
|
|
|
433
|
+
test("SlackAdapter source-level: supports reaction polling and acknowledgement", () => {
|
|
434
|
+
const adapterSrc = readFileSync(
|
|
435
|
+
join(__dirname, "..", "..", "remote-questions", "slack-adapter.ts"),
|
|
436
|
+
"utf-8",
|
|
437
|
+
);
|
|
438
|
+
assert.ok(adapterSrc.includes("reactions.get"), "should poll Slack reactions");
|
|
439
|
+
assert.ok(adapterSrc.includes("reactions.add"), "should add Slack reactions");
|
|
440
|
+
assert.ok(adapterSrc.includes("async acknowledgeAnswer"), "should acknowledge Slack answers");
|
|
441
|
+
assert.ok(adapterSrc.includes("white_check_mark"), "should use a checkmark acknowledgement reaction");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("Slack setup source-level: offers channel picker with manual fallback", () => {
|
|
445
|
+
const commandSrc = readFileSync(
|
|
446
|
+
join(__dirname, "..", "..", "remote-questions", "remote-command.ts"),
|
|
447
|
+
"utf-8",
|
|
448
|
+
);
|
|
449
|
+
assert.ok(commandSrc.includes("users.conversations"), "Slack setup should query Slack channels");
|
|
450
|
+
assert.ok(commandSrc.includes("Select a Slack channel"), "Slack setup should present a channel picker");
|
|
451
|
+
assert.ok(commandSrc.includes("Enter channel ID manually"), "Slack setup should preserve manual fallback");
|
|
452
|
+
});
|
|
453
|
+
|
|
359
454
|
test("DiscordAdapter source-level: resolves guild ID for message URLs", () => {
|
|
360
455
|
const adapterSrc = readFileSync(
|
|
361
456
|
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|
|
@@ -369,6 +464,172 @@ test("DiscordAdapter source-level: resolves guild ID for message URLs", () => {
|
|
|
369
464
|
);
|
|
370
465
|
});
|
|
371
466
|
|
|
467
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
468
|
+
// Telegram Tests
|
|
469
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
470
|
+
|
|
471
|
+
test("formatForTelegram single-question produces inline keyboard", () => {
|
|
472
|
+
const prompt = {
|
|
473
|
+
id: "tg-1",
|
|
474
|
+
channel: "telegram" as const,
|
|
475
|
+
createdAt: Date.now(),
|
|
476
|
+
timeoutAt: Date.now() + 60000,
|
|
477
|
+
pollIntervalMs: 5000,
|
|
478
|
+
questions: [{
|
|
479
|
+
id: "q1",
|
|
480
|
+
header: "Confirm",
|
|
481
|
+
question: "Proceed?",
|
|
482
|
+
options: [
|
|
483
|
+
{ label: "Yes", description: "Continue" },
|
|
484
|
+
{ label: "No", description: "Stop" },
|
|
485
|
+
],
|
|
486
|
+
allowMultiple: false,
|
|
487
|
+
}],
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const msg = formatForTelegram(prompt);
|
|
491
|
+
assert.equal(msg.parse_mode, "HTML");
|
|
492
|
+
assert.ok(msg.text.includes("<b>GSD needs your input</b>"));
|
|
493
|
+
assert.ok(msg.text.includes("<b>Confirm</b>"));
|
|
494
|
+
assert.ok(msg.reply_markup, "single-question should have inline keyboard");
|
|
495
|
+
assert.equal(msg.reply_markup!.inline_keyboard.length, 2, "should have 2 button rows");
|
|
496
|
+
assert.equal(msg.reply_markup!.inline_keyboard[0][0].callback_data, "tg-1:0");
|
|
497
|
+
assert.equal(msg.reply_markup!.inline_keyboard[1][0].callback_data, "tg-1:1");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("formatForTelegram multi-question omits inline keyboard", () => {
|
|
501
|
+
const prompt = {
|
|
502
|
+
id: "tg-2",
|
|
503
|
+
channel: "telegram" as const,
|
|
504
|
+
createdAt: Date.now(),
|
|
505
|
+
timeoutAt: Date.now() + 60000,
|
|
506
|
+
pollIntervalMs: 5000,
|
|
507
|
+
questions: [
|
|
508
|
+
{
|
|
509
|
+
id: "q1",
|
|
510
|
+
header: "First",
|
|
511
|
+
question: "Pick",
|
|
512
|
+
options: [{ label: "A", description: "a" }],
|
|
513
|
+
allowMultiple: false,
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "q2",
|
|
517
|
+
header: "Second",
|
|
518
|
+
question: "Pick",
|
|
519
|
+
options: [{ label: "B", description: "b" }],
|
|
520
|
+
allowMultiple: false,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const msg = formatForTelegram(prompt);
|
|
526
|
+
assert.equal(msg.reply_markup, undefined, "multi-question should not have inline keyboard");
|
|
527
|
+
assert.ok(msg.text.includes("1/2"), "should show question position");
|
|
528
|
+
assert.ok(msg.text.includes("2/2"), "should show question position");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("formatForTelegram escapes HTML in user content", () => {
|
|
532
|
+
const prompt = {
|
|
533
|
+
id: "tg-3",
|
|
534
|
+
channel: "telegram" as const,
|
|
535
|
+
createdAt: Date.now(),
|
|
536
|
+
timeoutAt: Date.now() + 60000,
|
|
537
|
+
pollIntervalMs: 5000,
|
|
538
|
+
questions: [{
|
|
539
|
+
id: "q1",
|
|
540
|
+
header: "Test <script>",
|
|
541
|
+
question: "Is 5 > 3 & 2 < 4?",
|
|
542
|
+
options: [{ label: "<b>Yes</b>", description: "it's true" }],
|
|
543
|
+
allowMultiple: false,
|
|
544
|
+
}],
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const msg = formatForTelegram(prompt);
|
|
548
|
+
assert.ok(msg.text.includes("<script>"), "should escape < > in header");
|
|
549
|
+
assert.ok(msg.text.includes("5 > 3 & 2 < 4"), "should escape in question");
|
|
550
|
+
assert.ok(msg.text.includes("<b>Yes</b>"), "should escape in option label");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("parseTelegramResponse handles callback_data button press", () => {
|
|
554
|
+
const questions = [{
|
|
555
|
+
id: "choice",
|
|
556
|
+
header: "Pick",
|
|
557
|
+
question: "Choose",
|
|
558
|
+
allowMultiple: false,
|
|
559
|
+
options: [
|
|
560
|
+
{ label: "Alpha", description: "A" },
|
|
561
|
+
{ label: "Beta", description: "B" },
|
|
562
|
+
],
|
|
563
|
+
}];
|
|
564
|
+
|
|
565
|
+
const result = parseTelegramResponse("prompt-123:1", null, questions, "prompt-123");
|
|
566
|
+
assert.deepEqual(result, { answers: { choice: { answers: ["Beta"] } } });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("parseTelegramResponse handles text reply delegation", () => {
|
|
570
|
+
const questions = [{
|
|
571
|
+
id: "choice",
|
|
572
|
+
header: "Pick",
|
|
573
|
+
question: "Choose",
|
|
574
|
+
allowMultiple: false,
|
|
575
|
+
options: [
|
|
576
|
+
{ label: "Alpha", description: "A" },
|
|
577
|
+
{ label: "Beta", description: "B" },
|
|
578
|
+
],
|
|
579
|
+
}];
|
|
580
|
+
|
|
581
|
+
const result = parseTelegramResponse(null, "1", questions, "prompt-123");
|
|
582
|
+
assert.deepEqual(result, { answers: { choice: { answers: ["Alpha"] } } });
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("parseTelegramResponse handles multi-question semicolons", () => {
|
|
586
|
+
const questions = [
|
|
587
|
+
{
|
|
588
|
+
id: "first",
|
|
589
|
+
header: "First",
|
|
590
|
+
question: "Pick",
|
|
591
|
+
allowMultiple: false,
|
|
592
|
+
options: [
|
|
593
|
+
{ label: "Alpha", description: "A" },
|
|
594
|
+
{ label: "Beta", description: "B" },
|
|
595
|
+
],
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
id: "second",
|
|
599
|
+
header: "Second",
|
|
600
|
+
question: "Pick",
|
|
601
|
+
allowMultiple: false,
|
|
602
|
+
options: [
|
|
603
|
+
{ label: "Gamma", description: "G" },
|
|
604
|
+
{ label: "Delta", description: "D" },
|
|
605
|
+
],
|
|
606
|
+
},
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
const result = parseTelegramResponse(null, "2;1", questions, "prompt-123");
|
|
610
|
+
assert.deepEqual(result.answers.first.answers, ["Beta"]);
|
|
611
|
+
assert.deepEqual(result.answers.second.answers, ["Gamma"]);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("isValidChannelId validates Telegram chat IDs", () => {
|
|
615
|
+
// Valid positive ID
|
|
616
|
+
assert.equal(isValidChannelId("telegram", "12345"), true);
|
|
617
|
+
// Valid negative group ID
|
|
618
|
+
assert.equal(isValidChannelId("telegram", "-1001234567890"), true);
|
|
619
|
+
// Too short
|
|
620
|
+
assert.equal(isValidChannelId("telegram", "1234"), false);
|
|
621
|
+
// Non-numeric
|
|
622
|
+
assert.equal(isValidChannelId("telegram", "abc12345"), false);
|
|
623
|
+
// URL injection
|
|
624
|
+
assert.equal(isValidChannelId("telegram", "https://evil.com"), false);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("sanitizeError strips Telegram bot token patterns", () => {
|
|
628
|
+
const fakeToken = "1234567890:ABCdefGHIjklMNOpqrSTUvwxyz12345678";
|
|
629
|
+
const result = sanitizeError(`Token: ${fakeToken}`);
|
|
630
|
+
assert.ok(!result.includes("1234567890:ABC"), "should strip Telegram bot token");
|
|
631
|
+
});
|
|
632
|
+
|
|
372
633
|
test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
|
|
373
634
|
const adapterSrc = readFileSync(
|
|
374
635
|
join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
|