@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.
- package/dist/resource-loader.d.ts +5 -0
- package/dist/resource-loader.js +24 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +19 -0
- package/dist/resources/extensions/gsd/auto/phases.js +1 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
- package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/package.json +0 -1
- package/dist/worktree-cli.d.ts +0 -2
- package/dist/worktree-cli.js +21 -9
- package/package.json +9 -4
- package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
- package/packages/cloud-mcp-gateway/package.json +5 -4
- package/packages/contracts/package.json +2 -2
- package/packages/daemon/bin/gsd-daemon.js +14 -0
- package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
- package/packages/daemon/bin/gsd-mcp.js +14 -0
- package/packages/daemon/dist/channel-manager.d.ts +53 -0
- package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
- package/packages/daemon/dist/channel-manager.js +167 -0
- package/packages/daemon/dist/channel-manager.js.map +1 -0
- package/packages/daemon/dist/cli.d.ts +3 -0
- package/packages/daemon/dist/cli.d.ts.map +1 -0
- package/packages/daemon/dist/cli.js +94 -0
- package/packages/daemon/dist/cli.js.map +1 -0
- package/packages/daemon/dist/cloud-cli.d.ts +7 -0
- package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-cli.js +96 -0
- package/packages/daemon/dist/cloud-cli.js.map +1 -0
- package/packages/daemon/dist/cloud-config.d.ts +18 -0
- package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.js +209 -0
- package/packages/daemon/dist/cloud-config.js.map +1 -0
- package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-config.test.js +132 -0
- package/packages/daemon/dist/cloud-config.test.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
- package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.js +180 -0
- package/packages/daemon/dist/cloud-runtime.js.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
- package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-runtime.test.js +28 -0
- package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
- package/packages/daemon/dist/cloud-token.d.ts +3 -0
- package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
- package/packages/daemon/dist/cloud-token.js +37 -0
- package/packages/daemon/dist/cloud-token.js.map +1 -0
- package/packages/daemon/dist/commands.d.ts +25 -0
- package/packages/daemon/dist/commands.d.ts.map +1 -0
- package/packages/daemon/dist/commands.js +81 -0
- package/packages/daemon/dist/commands.js.map +1 -0
- package/packages/daemon/dist/config.d.ts +17 -0
- package/packages/daemon/dist/config.d.ts.map +1 -0
- package/packages/daemon/dist/config.js +146 -0
- package/packages/daemon/dist/config.js.map +1 -0
- package/packages/daemon/dist/daemon.d.ts +38 -0
- package/packages/daemon/dist/daemon.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.js +194 -0
- package/packages/daemon/dist/daemon.js.map +1 -0
- package/packages/daemon/dist/daemon.test.d.ts +2 -0
- package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
- package/packages/daemon/dist/daemon.test.js +692 -0
- package/packages/daemon/dist/daemon.test.js.map +1 -0
- package/packages/daemon/dist/discord-bot.d.ts +70 -0
- package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.js +433 -0
- package/packages/daemon/dist/discord-bot.js.map +1 -0
- package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
- package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
- package/packages/daemon/dist/discord-bot.test.js +667 -0
- package/packages/daemon/dist/discord-bot.test.js.map +1 -0
- package/packages/daemon/dist/event-bridge.d.ts +72 -0
- package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.js +366 -0
- package/packages/daemon/dist/event-bridge.js.map +1 -0
- package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
- package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-bridge.test.js +528 -0
- package/packages/daemon/dist/event-bridge.test.js.map +1 -0
- package/packages/daemon/dist/event-formatter.d.ts +34 -0
- package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.js +355 -0
- package/packages/daemon/dist/event-formatter.js.map +1 -0
- package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
- package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
- package/packages/daemon/dist/event-formatter.test.js +333 -0
- package/packages/daemon/dist/event-formatter.test.js.map +1 -0
- package/packages/daemon/dist/index.d.ts +25 -0
- package/packages/daemon/dist/index.d.ts.map +1 -0
- package/packages/daemon/dist/index.js +17 -0
- package/packages/daemon/dist/index.js.map +1 -0
- package/packages/daemon/dist/launchd.d.ts +49 -0
- package/packages/daemon/dist/launchd.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.js +188 -0
- package/packages/daemon/dist/launchd.js.map +1 -0
- package/packages/daemon/dist/launchd.test.d.ts +2 -0
- package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
- package/packages/daemon/dist/launchd.test.js +296 -0
- package/packages/daemon/dist/launchd.test.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
- package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.js +307 -0
- package/packages/daemon/dist/local-tool-executor.js.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
- package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
- package/packages/daemon/dist/local-tool-executor.test.js +111 -0
- package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
- package/packages/daemon/dist/logger.d.ts +25 -0
- package/packages/daemon/dist/logger.d.ts.map +1 -0
- package/packages/daemon/dist/logger.js +72 -0
- package/packages/daemon/dist/logger.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.js +8 -0
- package/packages/daemon/dist/mcp-cli.js.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
- package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-cli.test.js +13 -0
- package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
- package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
- package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
- package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
- package/packages/daemon/dist/message-batcher.d.ts +78 -0
- package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.js +173 -0
- package/packages/daemon/dist/message-batcher.js.map +1 -0
- package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
- package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
- package/packages/daemon/dist/message-batcher.test.js +242 -0
- package/packages/daemon/dist/message-batcher.test.js.map +1 -0
- package/packages/daemon/dist/orchestrator.d.ts +98 -0
- package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.js +359 -0
- package/packages/daemon/dist/orchestrator.js.map +1 -0
- package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
- package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
- package/packages/daemon/dist/orchestrator.test.js +425 -0
- package/packages/daemon/dist/orchestrator.test.js.map +1 -0
- package/packages/daemon/dist/project-scanner.d.ts +18 -0
- package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.js +90 -0
- package/packages/daemon/dist/project-scanner.js.map +1 -0
- package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
- package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
- package/packages/daemon/dist/project-scanner.test.js +183 -0
- package/packages/daemon/dist/project-scanner.test.js.map +1 -0
- package/packages/daemon/dist/session-manager.d.ts +70 -0
- package/packages/daemon/dist/session-manager.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.js +358 -0
- package/packages/daemon/dist/session-manager.js.map +1 -0
- package/packages/daemon/dist/session-manager.test.d.ts +9 -0
- package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
- package/packages/daemon/dist/session-manager.test.js +616 -0
- package/packages/daemon/dist/session-manager.test.js.map +1 -0
- package/packages/daemon/dist/types.d.ts +133 -0
- package/packages/daemon/dist/types.d.ts.map +1 -0
- package/packages/daemon/dist/types.js +8 -0
- package/packages/daemon/dist/types.js.map +1 -0
- package/packages/daemon/dist/verbosity.d.ts +27 -0
- package/packages/daemon/dist/verbosity.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.js +86 -0
- package/packages/daemon/dist/verbosity.js.map +1 -0
- package/packages/daemon/dist/verbosity.test.d.ts +2 -0
- package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
- package/packages/daemon/dist/verbosity.test.js +136 -0
- package/packages/daemon/dist/verbosity.test.js.map +1 -0
- package/packages/daemon/package.json +9 -8
- package/packages/gsd-agent-core/package.json +6 -6
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +8 -8
- package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
- package/packages/mcp-server/package.json +6 -5
- package/packages/native/package.json +3 -3
- package/packages/pi-agent-core/package.json +4 -4
- package/packages/pi-ai/bin/pi-ai.js +14 -0
- package/packages/pi-ai/dist/models.generated.d.ts +0 -17
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +18 -35
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +5 -4
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
- package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
- package/packages/pi-coding-agent/package.json +9 -9
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +3 -3
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +4 -4
- package/scripts/install/deps.js +10 -0
- package/src/resources/extensions/gsd/auto/loop.ts +22 -0
- package/src/resources/extensions/gsd/auto/phases.ts +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
- package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
- package/dist/tsconfig.extensions.tsbuildinfo +0 -1
- /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
- /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
|