@opengsd/gsd-pi 1.0.2-dev.50223bc → 1.0.2-dev.5961fbf

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.
Files changed (251) hide show
  1. package/dist/resource-loader.d.ts +5 -0
  2. package/dist/resource-loader.js +24 -8
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  7. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  8. package/dist/web/standalone/.next/BUILD_ID +1 -1
  9. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  10. package/dist/web/standalone/.next/build-manifest.json +2 -2
  11. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  12. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  29. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  30. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  31. package/dist/web/standalone/.next/server/app/index.html +1 -1
  32. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  39. package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
  40. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  42. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  43. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  44. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  45. package/dist/web/standalone/package.json +0 -1
  46. package/dist/worktree-cli.d.ts +0 -2
  47. package/dist/worktree-cli.js +21 -9
  48. package/package.json +9 -4
  49. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  50. package/packages/cloud-mcp-gateway/package.json +5 -4
  51. package/packages/contracts/package.json +2 -2
  52. package/packages/daemon/bin/gsd-daemon.js +14 -0
  53. package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
  54. package/packages/daemon/bin/gsd-mcp.js +14 -0
  55. package/packages/daemon/dist/channel-manager.d.ts +53 -0
  56. package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
  57. package/packages/daemon/dist/channel-manager.js +167 -0
  58. package/packages/daemon/dist/channel-manager.js.map +1 -0
  59. package/packages/daemon/dist/cli.d.ts +3 -0
  60. package/packages/daemon/dist/cli.d.ts.map +1 -0
  61. package/packages/daemon/dist/cli.js +94 -0
  62. package/packages/daemon/dist/cli.js.map +1 -0
  63. package/packages/daemon/dist/cloud-cli.d.ts +7 -0
  64. package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
  65. package/packages/daemon/dist/cloud-cli.js +96 -0
  66. package/packages/daemon/dist/cloud-cli.js.map +1 -0
  67. package/packages/daemon/dist/cloud-config.d.ts +18 -0
  68. package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
  69. package/packages/daemon/dist/cloud-config.js +209 -0
  70. package/packages/daemon/dist/cloud-config.js.map +1 -0
  71. package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
  72. package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
  73. package/packages/daemon/dist/cloud-config.test.js +132 -0
  74. package/packages/daemon/dist/cloud-config.test.js.map +1 -0
  75. package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
  76. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
  77. package/packages/daemon/dist/cloud-runtime.js +180 -0
  78. package/packages/daemon/dist/cloud-runtime.js.map +1 -0
  79. package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
  80. package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
  81. package/packages/daemon/dist/cloud-runtime.test.js +28 -0
  82. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
  83. package/packages/daemon/dist/cloud-token.d.ts +3 -0
  84. package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
  85. package/packages/daemon/dist/cloud-token.js +37 -0
  86. package/packages/daemon/dist/cloud-token.js.map +1 -0
  87. package/packages/daemon/dist/commands.d.ts +25 -0
  88. package/packages/daemon/dist/commands.d.ts.map +1 -0
  89. package/packages/daemon/dist/commands.js +81 -0
  90. package/packages/daemon/dist/commands.js.map +1 -0
  91. package/packages/daemon/dist/config.d.ts +17 -0
  92. package/packages/daemon/dist/config.d.ts.map +1 -0
  93. package/packages/daemon/dist/config.js +146 -0
  94. package/packages/daemon/dist/config.js.map +1 -0
  95. package/packages/daemon/dist/daemon.d.ts +38 -0
  96. package/packages/daemon/dist/daemon.d.ts.map +1 -0
  97. package/packages/daemon/dist/daemon.js +194 -0
  98. package/packages/daemon/dist/daemon.js.map +1 -0
  99. package/packages/daemon/dist/daemon.test.d.ts +2 -0
  100. package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
  101. package/packages/daemon/dist/daemon.test.js +692 -0
  102. package/packages/daemon/dist/daemon.test.js.map +1 -0
  103. package/packages/daemon/dist/discord-bot.d.ts +70 -0
  104. package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
  105. package/packages/daemon/dist/discord-bot.js +433 -0
  106. package/packages/daemon/dist/discord-bot.js.map +1 -0
  107. package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
  108. package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
  109. package/packages/daemon/dist/discord-bot.test.js +667 -0
  110. package/packages/daemon/dist/discord-bot.test.js.map +1 -0
  111. package/packages/daemon/dist/event-bridge.d.ts +72 -0
  112. package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
  113. package/packages/daemon/dist/event-bridge.js +366 -0
  114. package/packages/daemon/dist/event-bridge.js.map +1 -0
  115. package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
  116. package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
  117. package/packages/daemon/dist/event-bridge.test.js +528 -0
  118. package/packages/daemon/dist/event-bridge.test.js.map +1 -0
  119. package/packages/daemon/dist/event-formatter.d.ts +34 -0
  120. package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
  121. package/packages/daemon/dist/event-formatter.js +355 -0
  122. package/packages/daemon/dist/event-formatter.js.map +1 -0
  123. package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
  124. package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
  125. package/packages/daemon/dist/event-formatter.test.js +333 -0
  126. package/packages/daemon/dist/event-formatter.test.js.map +1 -0
  127. package/packages/daemon/dist/index.d.ts +25 -0
  128. package/packages/daemon/dist/index.d.ts.map +1 -0
  129. package/packages/daemon/dist/index.js +17 -0
  130. package/packages/daemon/dist/index.js.map +1 -0
  131. package/packages/daemon/dist/launchd.d.ts +49 -0
  132. package/packages/daemon/dist/launchd.d.ts.map +1 -0
  133. package/packages/daemon/dist/launchd.js +188 -0
  134. package/packages/daemon/dist/launchd.js.map +1 -0
  135. package/packages/daemon/dist/launchd.test.d.ts +2 -0
  136. package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
  137. package/packages/daemon/dist/launchd.test.js +296 -0
  138. package/packages/daemon/dist/launchd.test.js.map +1 -0
  139. package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
  140. package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
  141. package/packages/daemon/dist/local-tool-executor.js +307 -0
  142. package/packages/daemon/dist/local-tool-executor.js.map +1 -0
  143. package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
  144. package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
  145. package/packages/daemon/dist/local-tool-executor.test.js +111 -0
  146. package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
  147. package/packages/daemon/dist/logger.d.ts +25 -0
  148. package/packages/daemon/dist/logger.d.ts.map +1 -0
  149. package/packages/daemon/dist/logger.js +72 -0
  150. package/packages/daemon/dist/logger.js.map +1 -0
  151. package/packages/daemon/dist/mcp-cli.d.ts +3 -0
  152. package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
  153. package/packages/daemon/dist/mcp-cli.js +8 -0
  154. package/packages/daemon/dist/mcp-cli.js.map +1 -0
  155. package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
  156. package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
  157. package/packages/daemon/dist/mcp-cli.test.js +13 -0
  158. package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
  159. package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
  160. package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
  161. package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
  162. package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
  163. package/packages/daemon/dist/message-batcher.d.ts +78 -0
  164. package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
  165. package/packages/daemon/dist/message-batcher.js +173 -0
  166. package/packages/daemon/dist/message-batcher.js.map +1 -0
  167. package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
  168. package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
  169. package/packages/daemon/dist/message-batcher.test.js +242 -0
  170. package/packages/daemon/dist/message-batcher.test.js.map +1 -0
  171. package/packages/daemon/dist/orchestrator.d.ts +98 -0
  172. package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
  173. package/packages/daemon/dist/orchestrator.js +359 -0
  174. package/packages/daemon/dist/orchestrator.js.map +1 -0
  175. package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
  176. package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
  177. package/packages/daemon/dist/orchestrator.test.js +425 -0
  178. package/packages/daemon/dist/orchestrator.test.js.map +1 -0
  179. package/packages/daemon/dist/project-scanner.d.ts +18 -0
  180. package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
  181. package/packages/daemon/dist/project-scanner.js +90 -0
  182. package/packages/daemon/dist/project-scanner.js.map +1 -0
  183. package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
  184. package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
  185. package/packages/daemon/dist/project-scanner.test.js +183 -0
  186. package/packages/daemon/dist/project-scanner.test.js.map +1 -0
  187. package/packages/daemon/dist/session-manager.d.ts +70 -0
  188. package/packages/daemon/dist/session-manager.d.ts.map +1 -0
  189. package/packages/daemon/dist/session-manager.js +358 -0
  190. package/packages/daemon/dist/session-manager.js.map +1 -0
  191. package/packages/daemon/dist/session-manager.test.d.ts +9 -0
  192. package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
  193. package/packages/daemon/dist/session-manager.test.js +616 -0
  194. package/packages/daemon/dist/session-manager.test.js.map +1 -0
  195. package/packages/daemon/dist/types.d.ts +133 -0
  196. package/packages/daemon/dist/types.d.ts.map +1 -0
  197. package/packages/daemon/dist/types.js +8 -0
  198. package/packages/daemon/dist/types.js.map +1 -0
  199. package/packages/daemon/dist/verbosity.d.ts +27 -0
  200. package/packages/daemon/dist/verbosity.d.ts.map +1 -0
  201. package/packages/daemon/dist/verbosity.js +86 -0
  202. package/packages/daemon/dist/verbosity.js.map +1 -0
  203. package/packages/daemon/dist/verbosity.test.d.ts +2 -0
  204. package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
  205. package/packages/daemon/dist/verbosity.test.js +136 -0
  206. package/packages/daemon/dist/verbosity.test.js.map +1 -0
  207. package/packages/daemon/package.json +9 -8
  208. package/packages/gsd-agent-core/package.json +6 -6
  209. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  210. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  211. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  212. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  213. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  214. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  215. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  216. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  217. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  218. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  219. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  220. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  221. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  222. package/packages/gsd-agent-modes/package.json +8 -8
  223. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  224. package/packages/mcp-server/package.json +6 -5
  225. package/packages/native/package.json +3 -3
  226. package/packages/pi-agent-core/package.json +4 -4
  227. package/packages/pi-ai/bin/pi-ai.js +14 -0
  228. package/packages/pi-ai/dist/models.generated.d.ts +0 -17
  229. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  230. package/packages/pi-ai/dist/models.generated.js +18 -35
  231. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  232. package/packages/pi-ai/package.json +5 -4
  233. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  234. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  235. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  236. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  237. package/packages/pi-coding-agent/package.json +9 -9
  238. package/packages/pi-tui/package.json +2 -2
  239. package/packages/rpc-client/package.json +3 -3
  240. package/pkg/package.json +1 -1
  241. package/scripts/ensure-workspace-builds.cjs +4 -4
  242. package/scripts/install/deps.js +10 -0
  243. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  244. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  245. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  246. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  247. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  248. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  249. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  250. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
  251. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_ssgManifest.js +0 -0
@@ -0,0 +1,667 @@
1
+ import { describe, it, afterEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, readFileSync, rmSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { ChannelType } from 'discord.js';
8
+ import { isAuthorized, validateDiscordConfig } from './discord-bot.js';
9
+ import { sanitizeChannelName, ChannelManager } from './channel-manager.js';
10
+ import { buildCommands, formatSessionStatus } from './commands.js';
11
+ import { Daemon } from './daemon.js';
12
+ import { Logger } from './logger.js';
13
+ import { validateConfig } from './config.js';
14
+ // ---------- helpers ----------
15
+ function tmpDir() {
16
+ return mkdtempSync(join(tmpdir(), `discord-test-${randomUUID().slice(0, 8)}-`));
17
+ }
18
+ const cleanupDirs = [];
19
+ afterEach(() => {
20
+ while (cleanupDirs.length) {
21
+ const d = cleanupDirs.pop();
22
+ if (existsSync(d))
23
+ rmSync(d, { recursive: true, force: true });
24
+ }
25
+ });
26
+ // ---------- isAuthorized ----------
27
+ describe('isAuthorized', () => {
28
+ it('returns true when userId matches ownerId', () => {
29
+ assert.equal(isAuthorized('12345', '12345'), true);
30
+ });
31
+ it('returns false when userId does not match ownerId', () => {
32
+ assert.equal(isAuthorized('12345', '99999'), false);
33
+ });
34
+ it('returns false when ownerId is empty', () => {
35
+ assert.equal(isAuthorized('12345', ''), false);
36
+ });
37
+ it('returns false when userId is empty', () => {
38
+ assert.equal(isAuthorized('', '12345'), false);
39
+ });
40
+ it('returns false when both are empty', () => {
41
+ assert.equal(isAuthorized('', ''), false);
42
+ });
43
+ });
44
+ // ---------- validateDiscordConfig ----------
45
+ describe('validateDiscordConfig', () => {
46
+ it('passes with all required fields', () => {
47
+ assert.doesNotThrow(() => {
48
+ validateDiscordConfig({
49
+ token: 'test-token',
50
+ guild_id: 'g123',
51
+ owner_id: 'o456',
52
+ });
53
+ });
54
+ });
55
+ it('throws on undefined config', () => {
56
+ assert.throws(() => validateDiscordConfig(undefined), (err) => {
57
+ assert.ok(err.message.includes('undefined'));
58
+ return true;
59
+ });
60
+ });
61
+ it('throws on missing token', () => {
62
+ assert.throws(() => validateDiscordConfig({ token: '', guild_id: 'g1', owner_id: 'o1' }), (err) => {
63
+ assert.ok(err.message.includes('token'));
64
+ return true;
65
+ });
66
+ });
67
+ it('throws on whitespace-only token', () => {
68
+ assert.throws(() => validateDiscordConfig({ token: ' ', guild_id: 'g1', owner_id: 'o1' }), (err) => {
69
+ assert.ok(err.message.includes('token'));
70
+ return true;
71
+ });
72
+ });
73
+ it('throws on missing guild_id', () => {
74
+ assert.throws(() => validateDiscordConfig({ token: 'tok', guild_id: '', owner_id: 'o1' }), (err) => {
75
+ assert.ok(err.message.includes('guild_id'));
76
+ return true;
77
+ });
78
+ });
79
+ it('throws on missing owner_id', () => {
80
+ assert.throws(() => validateDiscordConfig({ token: 'tok', guild_id: 'g1', owner_id: '' }), (err) => {
81
+ assert.ok(err.message.includes('owner_id'));
82
+ return true;
83
+ });
84
+ });
85
+ });
86
+ // ---------- Daemon wiring ----------
87
+ describe('Daemon + DiscordBot wiring', () => {
88
+ it('does not create DiscordBot when discord config is absent', async () => {
89
+ const dir = tmpDir();
90
+ cleanupDirs.push(dir);
91
+ const logPath = join(dir, 'no-discord.log');
92
+ const config = {
93
+ discord: undefined,
94
+ projects: { scan_roots: [] },
95
+ log: { file: logPath, level: 'debug', max_size_mb: 50 },
96
+ };
97
+ const logger = new Logger({ filePath: logPath, level: 'debug' });
98
+ const daemon = new Daemon(config, logger);
99
+ await daemon.start();
100
+ const origExit = process.exit;
101
+ // @ts-expect-error — overriding process.exit for test
102
+ process.exit = () => { };
103
+ try {
104
+ await daemon.shutdown();
105
+ }
106
+ finally {
107
+ process.exit = origExit;
108
+ }
109
+ const content = readFileSync(logPath, 'utf-8');
110
+ // Should NOT have any bot-related log entries
111
+ assert.ok(!content.includes('bot ready'));
112
+ assert.ok(!content.includes('discord bot login failed'));
113
+ assert.ok(!content.includes('bot destroyed'));
114
+ });
115
+ it('logs error when discord config has token but login fails (no real gateway)', async () => {
116
+ const dir = tmpDir();
117
+ cleanupDirs.push(dir);
118
+ const logPath = join(dir, 'bad-token.log');
119
+ const config = {
120
+ discord: {
121
+ token: 'invalid-token-that-will-fail-login',
122
+ guild_id: 'g1',
123
+ owner_id: 'o1',
124
+ },
125
+ projects: { scan_roots: [] },
126
+ log: { file: logPath, level: 'debug', max_size_mb: 50 },
127
+ };
128
+ const logger = new Logger({ filePath: logPath, level: 'debug' });
129
+ const daemon = new Daemon(config, logger);
130
+ // start() should NOT throw — bot login failure is non-fatal
131
+ await daemon.start();
132
+ const origExit = process.exit;
133
+ // @ts-expect-error — overriding process.exit for test
134
+ process.exit = () => { };
135
+ try {
136
+ await daemon.shutdown();
137
+ }
138
+ finally {
139
+ process.exit = origExit;
140
+ }
141
+ // Small flush delay
142
+ await new Promise((r) => setTimeout(r, 50));
143
+ const content = readFileSync(logPath, 'utf-8');
144
+ // Should have logged the login failure
145
+ assert.ok(content.includes('discord bot login failed'), 'should log bot login failure');
146
+ // Token should never appear in logs
147
+ assert.ok(!content.includes('invalid-token-that-will-fail-login'), 'token must not appear in logs');
148
+ });
149
+ it('does not attempt login when discord config has no token', async () => {
150
+ const dir = tmpDir();
151
+ cleanupDirs.push(dir);
152
+ const logPath = join(dir, 'no-token.log');
153
+ // Config with discord block but empty token
154
+ const config = {
155
+ discord: {
156
+ token: '',
157
+ guild_id: 'g1',
158
+ owner_id: 'o1',
159
+ },
160
+ projects: { scan_roots: [] },
161
+ log: { file: logPath, level: 'debug', max_size_mb: 50 },
162
+ };
163
+ const logger = new Logger({ filePath: logPath, level: 'debug' });
164
+ const daemon = new Daemon(config, logger);
165
+ await daemon.start();
166
+ const origExit = process.exit;
167
+ // @ts-expect-error — overriding process.exit for test
168
+ process.exit = () => { };
169
+ try {
170
+ await daemon.shutdown();
171
+ }
172
+ finally {
173
+ process.exit = origExit;
174
+ }
175
+ const content = readFileSync(logPath, 'utf-8');
176
+ // Should not attempt login — no token
177
+ assert.ok(!content.includes('discord bot login failed'));
178
+ assert.ok(!content.includes('bot ready'));
179
+ });
180
+ });
181
+ // ---------- sanitizeChannelName ----------
182
+ describe('sanitizeChannelName', () => {
183
+ it('converts basic path to gsd-prefixed name', () => {
184
+ assert.equal(sanitizeChannelName('/home/user/my-project'), 'gsd-my-project');
185
+ });
186
+ it('converts path with special characters to hyphens', () => {
187
+ assert.equal(sanitizeChannelName('/home/user/My_Cool.Project!v2'), 'gsd-my-cool-project-v2');
188
+ });
189
+ it('truncates very long names to 100 chars', () => {
190
+ const longName = 'a'.repeat(200);
191
+ const result = sanitizeChannelName(`/home/${longName}`);
192
+ assert.ok(result.length <= 100, `Expected <= 100 chars, got ${result.length}`);
193
+ assert.ok(result.startsWith('gsd-'));
194
+ });
195
+ it('cleans leading/trailing dots and underscores', () => {
196
+ assert.equal(sanitizeChannelName('/home/...___project___...'), 'gsd-project');
197
+ });
198
+ it('returns gsd-unnamed for empty basename', () => {
199
+ assert.equal(sanitizeChannelName(''), 'gsd-unnamed');
200
+ assert.equal(sanitizeChannelName('/'), 'gsd-unnamed');
201
+ });
202
+ it('returns gsd-unnamed for basename with only special chars', () => {
203
+ assert.equal(sanitizeChannelName('/home/!!!'), 'gsd-unnamed');
204
+ });
205
+ it('collapses consecutive hyphens', () => {
206
+ assert.equal(sanitizeChannelName('/home/a---b---c'), 'gsd-a-b-c');
207
+ });
208
+ it('handles Windows-style backslash paths', () => {
209
+ assert.equal(sanitizeChannelName('C:\\Users\\lex\\my-project'), 'gsd-my-project');
210
+ });
211
+ it('handles name at exact prefix + 96 chars = 100 char limit', () => {
212
+ // gsd- is 4 chars, so a 96-char basename should produce exactly 100
213
+ const name96 = 'a'.repeat(96);
214
+ const result = sanitizeChannelName(`/home/${name96}`);
215
+ assert.equal(result.length, 100);
216
+ assert.equal(result, `gsd-${'a'.repeat(96)}`);
217
+ });
218
+ it('handles whitespace-only basename', () => {
219
+ assert.equal(sanitizeChannelName('/home/ '), 'gsd-unnamed');
220
+ });
221
+ });
222
+ // ---------- ChannelManager ----------
223
+ describe('ChannelManager', () => {
224
+ // Helper to create a mock Guild with controllable channel cache and create method
225
+ function createMockGuild() {
226
+ const channels = new Map();
227
+ let createCounter = 0;
228
+ const mockGuild = {
229
+ id: 'guild-123', // @everyone role ID matches guild ID
230
+ channels: {
231
+ cache: {
232
+ get: (id) => channels.get(id),
233
+ find: (fn) => {
234
+ for (const ch of channels.values()) {
235
+ if (fn(ch))
236
+ return ch;
237
+ }
238
+ return undefined;
239
+ },
240
+ },
241
+ create: async (opts) => {
242
+ createCounter++;
243
+ const id = `chan-${createCounter}`;
244
+ const ch = {
245
+ id,
246
+ name: opts.name,
247
+ type: opts.type,
248
+ parentId: opts.parent ?? null,
249
+ edit: async (editOpts) => {
250
+ // Simulate edit — update parent
251
+ ch.parentId = editOpts.parent ?? ch.parentId;
252
+ return ch;
253
+ },
254
+ };
255
+ channels.set(id, ch);
256
+ return ch;
257
+ },
258
+ },
259
+ _channels: channels, // internal for test inspection
260
+ _getCreateCount: () => createCounter,
261
+ };
262
+ return mockGuild;
263
+ }
264
+ function createMockLogger() {
265
+ const entries = [];
266
+ return {
267
+ debug: (msg, data) => entries.push({ level: 'debug', msg, data }),
268
+ info: (msg, data) => entries.push({ level: 'info', msg, data }),
269
+ warn: (msg, data) => entries.push({ level: 'warn', msg, data }),
270
+ error: (msg, data) => entries.push({ level: 'error', msg, data }),
271
+ entries,
272
+ close: async () => { },
273
+ };
274
+ }
275
+ it('resolveCategory creates category when not found', async () => {
276
+ const guild = createMockGuild();
277
+ const logger = createMockLogger();
278
+ const mgr = new ChannelManager({ guild: guild, logger: logger });
279
+ const cat = await mgr.resolveCategory();
280
+ assert.equal(cat.name, 'GSD Projects');
281
+ assert.equal(cat.type, ChannelType.GuildCategory);
282
+ });
283
+ it('resolveCategory returns cached category on second call', async () => {
284
+ const guild = createMockGuild();
285
+ const logger = createMockLogger();
286
+ const mgr = new ChannelManager({ guild: guild, logger: logger });
287
+ const cat1 = await mgr.resolveCategory();
288
+ const cat2 = await mgr.resolveCategory();
289
+ assert.equal(cat1.id, cat2.id);
290
+ // Only one create call should have been made
291
+ assert.equal(guild._getCreateCount(), 1);
292
+ });
293
+ it('resolveCategory finds existing category by name', async () => {
294
+ const guild = createMockGuild();
295
+ // Pre-populate a matching category
296
+ guild._channels.set('existing-cat', {
297
+ id: 'existing-cat',
298
+ name: 'GSD Projects',
299
+ type: ChannelType.GuildCategory,
300
+ parentId: null,
301
+ });
302
+ const logger = createMockLogger();
303
+ const mgr = new ChannelManager({ guild: guild, logger: logger });
304
+ const cat = await mgr.resolveCategory();
305
+ assert.equal(cat.id, 'existing-cat');
306
+ // No create calls — found existing
307
+ assert.equal(guild._getCreateCount(), 0);
308
+ });
309
+ it('createProjectChannel creates text channel under category', async () => {
310
+ const guild = createMockGuild();
311
+ const logger = createMockLogger();
312
+ const mgr = new ChannelManager({ guild: guild, logger: logger });
313
+ const channel = await mgr.createProjectChannel('/home/user/my-project');
314
+ assert.equal(channel.name, 'gsd-my-project');
315
+ assert.equal(channel.type, ChannelType.GuildText);
316
+ // Category was created first (chan-1), then channel (chan-2)
317
+ assert.equal(channel.parentId, 'chan-1');
318
+ });
319
+ it('archiveChannel moves channel to archive category', async () => {
320
+ const guild = createMockGuild();
321
+ const logger = createMockLogger();
322
+ const mgr = new ChannelManager({ guild: guild, logger: logger });
323
+ // Create a project channel first
324
+ const channel = await mgr.createProjectChannel('/home/user/project');
325
+ const channelId = channel.id;
326
+ // Archive it
327
+ await mgr.archiveChannel(channelId);
328
+ // The channel should have been edit()-ed with the archive category as parent
329
+ const archived = guild._channels.get(channelId);
330
+ // Archive category was created as the 3rd channel (chan-3): category(chan-1), text(chan-2), archive(chan-3)
331
+ assert.equal(archived.parentId, 'chan-3');
332
+ // Verify archive log
333
+ const archiveLog = logger.entries.find((e) => e.msg === 'channel archived');
334
+ assert.ok(archiveLog, 'should log channel archived');
335
+ assert.equal(archiveLog.data.channelId, channelId);
336
+ });
337
+ it('archiveChannel warns when channel not found', async () => {
338
+ const guild = createMockGuild();
339
+ const logger = createMockLogger();
340
+ const mgr = new ChannelManager({ guild: guild, logger: logger });
341
+ await mgr.archiveChannel('nonexistent-id');
342
+ const warnLog = logger.entries.find((e) => e.msg === 'archive target not found');
343
+ assert.ok(warnLog, 'should warn about missing channel');
344
+ });
345
+ it('uses custom category name when provided', async () => {
346
+ const guild = createMockGuild();
347
+ const logger = createMockLogger();
348
+ const mgr = new ChannelManager({
349
+ guild: guild,
350
+ logger: logger,
351
+ categoryName: 'Custom Category',
352
+ });
353
+ const cat = await mgr.resolveCategory();
354
+ assert.equal(cat.name, 'Custom Category');
355
+ });
356
+ });
357
+ // ---------- buildCommands ----------
358
+ describe('buildCommands', () => {
359
+ it('returns array with correct command names', () => {
360
+ const commands = buildCommands();
361
+ assert.equal(commands.length, 4);
362
+ const names = commands.map((c) => c.name);
363
+ assert.ok(names.includes('gsd-status'), 'should include gsd-status');
364
+ assert.ok(names.includes('gsd-start'), 'should include gsd-start');
365
+ assert.ok(names.includes('gsd-stop'), 'should include gsd-stop');
366
+ assert.ok(names.includes('gsd-verbose'), 'should include gsd-verbose');
367
+ });
368
+ it('each command has a description', () => {
369
+ const commands = buildCommands();
370
+ for (const cmd of commands) {
371
+ assert.ok(cmd.description, `command ${cmd.name} should have a description`);
372
+ assert.ok(cmd.description.length > 0, `command ${cmd.name} description should be non-empty`);
373
+ }
374
+ });
375
+ });
376
+ // ---------- formatSessionStatus ----------
377
+ describe('formatSessionStatus', () => {
378
+ function mockSession(overrides = {}) {
379
+ return {
380
+ sessionId: 'sess-1',
381
+ projectDir: '/home/user/project',
382
+ projectName: 'project',
383
+ status: 'running',
384
+ client: {},
385
+ events: [],
386
+ pendingBlocker: null,
387
+ cost: { totalCost: 0.1234, tokens: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0 } },
388
+ startTime: Date.now() - 120_000, // 2 minutes ago
389
+ ...overrides,
390
+ };
391
+ }
392
+ it('returns "No active sessions." for empty array', () => {
393
+ assert.equal(formatSessionStatus([]), 'No active sessions.');
394
+ });
395
+ it('formats single session with project name and status', () => {
396
+ const result = formatSessionStatus([mockSession()]);
397
+ assert.ok(result.includes('project'), 'should contain project name');
398
+ assert.ok(result.includes('running'), 'should contain status');
399
+ assert.ok(result.includes('$'), 'should contain cost');
400
+ });
401
+ it('formats multiple sessions on separate lines', () => {
402
+ const sessions = [
403
+ mockSession({ projectName: 'alpha', status: 'running' }),
404
+ mockSession({ projectName: 'beta', status: 'blocked' }),
405
+ ];
406
+ const result = formatSessionStatus(sessions);
407
+ assert.ok(result.includes('alpha'), 'should contain first project');
408
+ assert.ok(result.includes('beta'), 'should contain second project');
409
+ const lines = result.split('\n');
410
+ assert.equal(lines.length, 2, 'should have one line per session');
411
+ });
412
+ it('formats 5 sessions correctly', () => {
413
+ const sessions = Array.from({ length: 5 }, (_, i) => mockSession({ projectName: `proj-${i}`, status: i % 2 === 0 ? 'running' : 'completed' }));
414
+ const result = formatSessionStatus(sessions);
415
+ const lines = result.split('\n');
416
+ assert.equal(lines.length, 5);
417
+ for (let i = 0; i < 5; i++) {
418
+ assert.ok(lines[i].includes(`proj-${i}`));
419
+ }
420
+ });
421
+ });
422
+ // ---------- Command dispatch (mock interaction) ----------
423
+ describe('command dispatch', () => {
424
+ // Minimal mock of a ChatInputCommandInteraction
425
+ function mockInteraction(commandName, userId = 'owner-1') {
426
+ let replied = false;
427
+ let replyContent = '';
428
+ return {
429
+ user: { id: userId },
430
+ type: 2, // InteractionType.ApplicationCommand
431
+ isChatInputCommand: () => true,
432
+ commandName,
433
+ reply: async (opts) => {
434
+ replied = true;
435
+ replyContent = opts.content;
436
+ },
437
+ _getReplied: () => replied,
438
+ _getReplyContent: () => replyContent,
439
+ };
440
+ }
441
+ // Minimal mock of a non-command interaction
442
+ function mockNonCommandInteraction(userId = 'owner-1') {
443
+ let replied = false;
444
+ return {
445
+ user: { id: userId },
446
+ type: 3, // InteractionType.MessageComponent
447
+ isChatInputCommand: () => false,
448
+ _getReplied: () => replied,
449
+ };
450
+ }
451
+ // We can't easily test through DiscordBot.handleInteraction since it's private.
452
+ // Instead, test the pure functions that the handler calls, and test auth guard
453
+ // behavior via the mock interaction flow.
454
+ // The command routing logic is tested indirectly through integration of the
455
+ // pure helpers (buildCommands, formatSessionStatus, isAuthorized).
456
+ it('gsd-status with no sessions produces empty message', () => {
457
+ // Tests the formatSessionStatus path that /gsd-status calls
458
+ const result = formatSessionStatus([]);
459
+ assert.equal(result, 'No active sessions.');
460
+ });
461
+ it('unknown command name is not in buildCommands list', () => {
462
+ const commands = buildCommands();
463
+ const names = commands.map((c) => c.name);
464
+ assert.ok(!names.includes('gsd-unknown'), 'unknown should not be in command list');
465
+ });
466
+ it('auth guard rejects non-owner on interaction', () => {
467
+ // Simulates the first check in handleInteraction
468
+ const authorized = isAuthorized('intruder-999', 'owner-1');
469
+ assert.equal(authorized, false);
470
+ });
471
+ it('auth guard accepts owner on interaction', () => {
472
+ const authorized = isAuthorized('owner-1', 'owner-1');
473
+ assert.equal(authorized, true);
474
+ });
475
+ });
476
+ // ---------- Config validation: new fields ----------
477
+ describe('validateConfig — control_channel_id and orchestrator', () => {
478
+ it('parses control_channel_id from discord block', () => {
479
+ const config = validateConfig({
480
+ discord: {
481
+ token: 'tok',
482
+ guild_id: 'g1',
483
+ owner_id: 'o1',
484
+ control_channel_id: 'ch-123',
485
+ },
486
+ });
487
+ assert.equal(config.discord?.control_channel_id, 'ch-123');
488
+ });
489
+ it('omits control_channel_id when not present', () => {
490
+ const config = validateConfig({
491
+ discord: {
492
+ token: 'tok',
493
+ guild_id: 'g1',
494
+ owner_id: 'o1',
495
+ },
496
+ });
497
+ assert.equal(config.discord?.control_channel_id, undefined);
498
+ });
499
+ it('parses orchestrator model and max_tokens', () => {
500
+ const config = validateConfig({
501
+ discord: {
502
+ token: 'tok',
503
+ guild_id: 'g1',
504
+ owner_id: 'o1',
505
+ orchestrator: { model: 'claude-opus-2025', max_tokens: 2048 },
506
+ },
507
+ });
508
+ assert.equal(config.discord?.orchestrator?.model, 'claude-opus-2025');
509
+ assert.equal(config.discord?.orchestrator?.max_tokens, 2048);
510
+ });
511
+ it('missing orchestrator block results in undefined', () => {
512
+ const config = validateConfig({
513
+ discord: {
514
+ token: 'tok',
515
+ guild_id: 'g1',
516
+ owner_id: 'o1',
517
+ },
518
+ });
519
+ assert.equal(config.discord?.orchestrator, undefined);
520
+ });
521
+ it('empty orchestrator block has no model or max_tokens', () => {
522
+ const config = validateConfig({
523
+ discord: {
524
+ token: 'tok',
525
+ guild_id: 'g1',
526
+ owner_id: 'o1',
527
+ orchestrator: {},
528
+ },
529
+ });
530
+ // orchestrator object should exist but with no values set
531
+ assert.ok(config.discord?.orchestrator !== undefined);
532
+ assert.equal(config.discord?.orchestrator?.model, undefined);
533
+ assert.equal(config.discord?.orchestrator?.max_tokens, undefined);
534
+ });
535
+ it('ignores non-numeric max_tokens', () => {
536
+ const config = validateConfig({
537
+ discord: {
538
+ token: 'tok',
539
+ guild_id: 'g1',
540
+ owner_id: 'o1',
541
+ orchestrator: { max_tokens: 'not a number' },
542
+ },
543
+ });
544
+ assert.equal(config.discord?.orchestrator?.max_tokens, undefined);
545
+ });
546
+ it('ignores non-string model', () => {
547
+ const config = validateConfig({
548
+ discord: {
549
+ token: 'tok',
550
+ guild_id: 'g1',
551
+ owner_id: 'o1',
552
+ orchestrator: { model: 42 },
553
+ },
554
+ });
555
+ assert.equal(config.discord?.orchestrator?.model, undefined);
556
+ });
557
+ });
558
+ // ---------- Daemon wiring: orchestrator ----------
559
+ describe('Daemon orchestrator wiring', () => {
560
+ it('orchestrator is undefined when control_channel_id is not set', async () => {
561
+ const dir = tmpDir();
562
+ cleanupDirs.push(dir);
563
+ const logPath = join(dir, 'no-orchestrator.log');
564
+ const config = {
565
+ discord: undefined,
566
+ projects: { scan_roots: [] },
567
+ log: { file: logPath, level: 'debug', max_size_mb: 50 },
568
+ };
569
+ const logger = new Logger({ filePath: logPath, level: 'debug' });
570
+ const daemon = new Daemon(config, logger);
571
+ await daemon.start();
572
+ assert.equal(daemon.getOrchestrator(), undefined);
573
+ const origExit = process.exit;
574
+ // @ts-expect-error — overriding process.exit for test
575
+ process.exit = () => { };
576
+ try {
577
+ await daemon.shutdown();
578
+ }
579
+ finally {
580
+ process.exit = origExit;
581
+ }
582
+ });
583
+ it('orchestrator is undefined when discord has no control_channel_id', async () => {
584
+ // Even with a discord block that fails login, orchestrator should not be created
585
+ // because there's no control_channel_id
586
+ const dir = tmpDir();
587
+ cleanupDirs.push(dir);
588
+ const logPath = join(dir, 'no-ctl-chan.log');
589
+ const config = {
590
+ discord: {
591
+ token: 'bad-token',
592
+ guild_id: 'g1',
593
+ owner_id: 'o1',
594
+ // control_channel_id intentionally omitted
595
+ },
596
+ projects: { scan_roots: [] },
597
+ log: { file: logPath, level: 'debug', max_size_mb: 50 },
598
+ };
599
+ const logger = new Logger({ filePath: logPath, level: 'debug' });
600
+ const daemon = new Daemon(config, logger);
601
+ await daemon.start();
602
+ // Login fails, so orchestrator can't be wired regardless. But the code path
603
+ // that checks control_channel_id comes after successful login/eventBridge wiring.
604
+ // Since login fails, orchestrator is undefined.
605
+ assert.equal(daemon.getOrchestrator(), undefined);
606
+ const origExit = process.exit;
607
+ // @ts-expect-error — overriding process.exit for test
608
+ process.exit = () => { };
609
+ try {
610
+ await daemon.shutdown();
611
+ }
612
+ finally {
613
+ process.exit = origExit;
614
+ }
615
+ });
616
+ });
617
+ // ---------- /gsd-start and /gsd-stop logic paths ----------
618
+ describe('/gsd-start and /gsd-stop logic', () => {
619
+ // These test the observable logic paths exercised by the handlers.
620
+ // Since handleGsdStart/handleGsdStop are private, we test the data layer
621
+ // they depend on — project scanning, session listing, and edge cases.
622
+ it('/gsd-start: scanForProjects returning 0 projects', async () => {
623
+ // Simulates the "no projects" path
624
+ const { scanForProjects } = await import('./project-scanner.js');
625
+ // With no scan roots, should return empty
626
+ const projects = await scanForProjects([]);
627
+ assert.equal(projects.length, 0);
628
+ });
629
+ it('/gsd-stop: getAllSessions returns empty when no sessions active', async () => {
630
+ const { SessionManager } = await import('./session-manager.js');
631
+ const dir = tmpDir();
632
+ cleanupDirs.push(dir);
633
+ const logPath = join(dir, 'sm-test.log');
634
+ const logger = new Logger({ filePath: logPath, level: 'debug' });
635
+ const sm = new SessionManager(logger);
636
+ const sessions = sm.getAllSessions();
637
+ assert.equal(sessions.length, 0);
638
+ await logger.close();
639
+ });
640
+ it('/gsd-stop: filters to active sessions only', () => {
641
+ // Simulate the filter logic used in handleGsdStop
642
+ const allSessions = [
643
+ { sessionId: 's1', status: 'running', projectName: 'alpha' },
644
+ { sessionId: 's2', status: 'completed', projectName: 'beta' },
645
+ { sessionId: 's3', status: 'blocked', projectName: 'gamma' },
646
+ { sessionId: 's4', status: 'error', projectName: 'delta' },
647
+ { sessionId: 's5', status: 'starting', projectName: 'epsilon' },
648
+ { sessionId: 's6', status: 'cancelled', projectName: 'zeta' },
649
+ ];
650
+ const active = allSessions.filter((s) => s.status === 'running' || s.status === 'blocked' || s.status === 'starting');
651
+ assert.equal(active.length, 3);
652
+ assert.deepEqual(active.map((s) => s.projectName), ['alpha', 'gamma', 'epsilon']);
653
+ });
654
+ it('/gsd-start: >25 projects are truncated for select menu', () => {
655
+ // Simulate the truncation logic
656
+ const projects = Array.from({ length: 30 }, (_, i) => ({
657
+ name: `project-${i}`,
658
+ path: `/home/user/project-${i}`,
659
+ markers: [],
660
+ lastModified: Date.now(),
661
+ }));
662
+ const truncated = projects.slice(0, 25);
663
+ assert.equal(truncated.length, 25);
664
+ assert.equal(truncated[24].name, 'project-24');
665
+ });
666
+ });
667
+ //# sourceMappingURL=discord-bot.test.js.map