@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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Orchestrator — LLM agent for #gsd-control channel.
|
|
3
|
+
*
|
|
4
|
+
* Uses a MockAnthropicClient that simulates messages.create() responses,
|
|
5
|
+
* allowing tool execution and conversation flow testing without real API calls.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, afterEach } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { Orchestrator } from './orchestrator.js';
|
|
14
|
+
import { Logger } from './logger.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function tmpDir() {
|
|
19
|
+
return mkdtempSync(join(tmpdir(), `orch-test-${randomUUID().slice(0, 8)}-`));
|
|
20
|
+
}
|
|
21
|
+
const cleanupDirs = [];
|
|
22
|
+
const activeLoggers = [];
|
|
23
|
+
async function cleanupAll() {
|
|
24
|
+
// Close all loggers first so write streams flush before dirs are removed
|
|
25
|
+
for (const logger of activeLoggers) {
|
|
26
|
+
try {
|
|
27
|
+
await logger.close();
|
|
28
|
+
}
|
|
29
|
+
catch { /* ignore */ }
|
|
30
|
+
}
|
|
31
|
+
activeLoggers.length = 0;
|
|
32
|
+
while (cleanupDirs.length) {
|
|
33
|
+
const d = cleanupDirs.pop();
|
|
34
|
+
if (existsSync(d))
|
|
35
|
+
rmSync(d, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class MockAnthropicClient {
|
|
39
|
+
createCallCount = 0;
|
|
40
|
+
lastCreateParams = null;
|
|
41
|
+
createHandler;
|
|
42
|
+
constructor(handler) {
|
|
43
|
+
this.createHandler = handler ?? MockAnthropicClient.defaultHandler;
|
|
44
|
+
}
|
|
45
|
+
/** Default handler: returns a simple text response */
|
|
46
|
+
static defaultHandler() {
|
|
47
|
+
return {
|
|
48
|
+
stop_reason: 'end_turn',
|
|
49
|
+
content: [{ type: 'text', text: 'Mock LLM response' }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Handler that simulates a tool call then end_turn */
|
|
53
|
+
static toolThenTextHandler(toolName, toolInput, finalText) {
|
|
54
|
+
let callCount = 0;
|
|
55
|
+
return () => {
|
|
56
|
+
callCount++;
|
|
57
|
+
if (callCount === 1) {
|
|
58
|
+
return {
|
|
59
|
+
stop_reason: 'tool_use',
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: 'tool_use',
|
|
63
|
+
id: `toolu_${randomUUID().slice(0, 8)}`,
|
|
64
|
+
name: toolName,
|
|
65
|
+
input: toolInput,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
stop_reason: 'end_turn',
|
|
72
|
+
content: [{ type: 'text', text: finalText }],
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Handler that throws an error */
|
|
77
|
+
static errorHandler(message) {
|
|
78
|
+
return () => {
|
|
79
|
+
throw new Error(message);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
messages = {
|
|
83
|
+
create: async (params) => {
|
|
84
|
+
this.createCallCount++;
|
|
85
|
+
this.lastCreateParams = params;
|
|
86
|
+
return this.createHandler(params);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Mock SessionManager
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
function makeMockSession(overrides = {}) {
|
|
94
|
+
return {
|
|
95
|
+
sessionId: overrides.sessionId ?? 'sess-123',
|
|
96
|
+
projectDir: overrides.projectDir ?? '/home/user/project',
|
|
97
|
+
projectName: overrides.projectName ?? 'my-project',
|
|
98
|
+
status: overrides.status ?? 'running',
|
|
99
|
+
client: {},
|
|
100
|
+
events: [],
|
|
101
|
+
pendingBlocker: null,
|
|
102
|
+
cost: overrides.cost ?? { totalCost: 0.1234, tokens: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0 } },
|
|
103
|
+
startTime: overrides.startTime ?? Date.now() - 300_000, // 5 min ago
|
|
104
|
+
...overrides,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
class MockSessionManager {
|
|
108
|
+
sessions = [];
|
|
109
|
+
startSessionCalls = [];
|
|
110
|
+
cancelSessionCalls = [];
|
|
111
|
+
getResultCalls = [];
|
|
112
|
+
async startSession(opts) {
|
|
113
|
+
this.startSessionCalls.push(opts);
|
|
114
|
+
return 'sess-new-123';
|
|
115
|
+
}
|
|
116
|
+
getSession(sessionId) {
|
|
117
|
+
return this.sessions.find((s) => s.sessionId === sessionId);
|
|
118
|
+
}
|
|
119
|
+
getAllSessions() {
|
|
120
|
+
return this.sessions;
|
|
121
|
+
}
|
|
122
|
+
async cancelSession(sessionId) {
|
|
123
|
+
this.cancelSessionCalls.push(sessionId);
|
|
124
|
+
}
|
|
125
|
+
getResult(sessionId) {
|
|
126
|
+
const session = this.sessions.find((s) => s.sessionId === sessionId);
|
|
127
|
+
if (!session)
|
|
128
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
129
|
+
return {
|
|
130
|
+
sessionId: session.sessionId,
|
|
131
|
+
projectDir: session.projectDir,
|
|
132
|
+
projectName: session.projectName,
|
|
133
|
+
status: session.status,
|
|
134
|
+
durationMs: 300_000,
|
|
135
|
+
cost: session.cost,
|
|
136
|
+
recentEvents: [],
|
|
137
|
+
pendingBlocker: null,
|
|
138
|
+
error: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Mock ChannelManager (unused by orchestrator directly, but required by deps)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
class MockChannelManager {
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Mock Discord Message
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
function makeMessage(overrides) {
|
|
151
|
+
const sentMessages = [];
|
|
152
|
+
return {
|
|
153
|
+
author: {
|
|
154
|
+
id: overrides.authorId ?? 'owner-123',
|
|
155
|
+
bot: overrides.bot ?? false,
|
|
156
|
+
},
|
|
157
|
+
channelId: overrides.channelId ?? 'control-channel-1',
|
|
158
|
+
content: overrides.content ?? 'hello',
|
|
159
|
+
channel: {
|
|
160
|
+
send: async (content) => {
|
|
161
|
+
sentMessages.push(content);
|
|
162
|
+
},
|
|
163
|
+
sendTyping: async () => { },
|
|
164
|
+
},
|
|
165
|
+
sentMessages,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Test Setup Factory
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
function makeOrchestrator(opts) {
|
|
172
|
+
const dir = tmpDir();
|
|
173
|
+
cleanupDirs.push(dir);
|
|
174
|
+
const logPath = join(dir, 'test.log');
|
|
175
|
+
const logger = new Logger({ filePath: logPath, level: 'debug' });
|
|
176
|
+
activeLoggers.push(logger);
|
|
177
|
+
const sessionManager = new MockSessionManager();
|
|
178
|
+
if (opts?.sessions)
|
|
179
|
+
sessionManager.sessions = opts.sessions;
|
|
180
|
+
const projects = opts?.projects ?? [
|
|
181
|
+
{ name: 'alpha', path: '/home/user/alpha', markers: ['git', 'node', 'gsd'], lastModified: Date.now() },
|
|
182
|
+
{ name: 'bravo', path: '/home/user/bravo', markers: ['git', 'rust'], lastModified: Date.now() },
|
|
183
|
+
];
|
|
184
|
+
const config = {
|
|
185
|
+
model: 'claude-sonnet-4-20250514',
|
|
186
|
+
max_tokens: 4096,
|
|
187
|
+
control_channel_id: 'control-channel-1',
|
|
188
|
+
};
|
|
189
|
+
const deps = {
|
|
190
|
+
sessionManager: sessionManager,
|
|
191
|
+
channelManager: new MockChannelManager(),
|
|
192
|
+
scanProjects: async () => projects,
|
|
193
|
+
config,
|
|
194
|
+
logger,
|
|
195
|
+
ownerId: 'owner-123',
|
|
196
|
+
};
|
|
197
|
+
const mockClient = opts?.client ?? new MockAnthropicClient();
|
|
198
|
+
const orchestrator = new Orchestrator(deps, mockClient);
|
|
199
|
+
return { orchestrator, mockClient, sessionManager, logger, logPath };
|
|
200
|
+
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Tests
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
describe('Orchestrator', () => {
|
|
205
|
+
// Clean up after each test so logger streams are flushed before dirs removed
|
|
206
|
+
afterEach(async () => {
|
|
207
|
+
await cleanupAll();
|
|
208
|
+
});
|
|
209
|
+
// ---- Tool definitions ----
|
|
210
|
+
describe('tool definitions', () => {
|
|
211
|
+
it('passes 5 tools to the Anthropic API', async () => {
|
|
212
|
+
const { orchestrator, mockClient } = makeOrchestrator();
|
|
213
|
+
const msg = makeMessage({ content: 'what can you do?' });
|
|
214
|
+
await orchestrator.handleMessage(msg);
|
|
215
|
+
assert.ok(mockClient.lastCreateParams);
|
|
216
|
+
const tools = mockClient.lastCreateParams.tools;
|
|
217
|
+
assert.equal(tools.length, 5);
|
|
218
|
+
const names = tools.map((t) => t.name).sort();
|
|
219
|
+
assert.deepEqual(names, [
|
|
220
|
+
'get_session_detail',
|
|
221
|
+
'get_status',
|
|
222
|
+
'list_projects',
|
|
223
|
+
'start_session',
|
|
224
|
+
'stop_session',
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
// ---- list_projects tool ----
|
|
229
|
+
describe('list_projects tool', () => {
|
|
230
|
+
it('returns project list from scanProjects', async () => {
|
|
231
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('list_projects', {}, 'Here are your projects'));
|
|
232
|
+
const { orchestrator } = makeOrchestrator({ client: mockClient });
|
|
233
|
+
const msg = makeMessage({ content: 'list my projects' });
|
|
234
|
+
await orchestrator.handleMessage(msg);
|
|
235
|
+
assert.equal(msg.sentMessages.length, 1);
|
|
236
|
+
assert.equal(msg.sentMessages[0], 'Here are your projects');
|
|
237
|
+
// The tool was called (2 create calls: tool_use + end_turn)
|
|
238
|
+
assert.equal(mockClient.createCallCount, 2);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
// ---- start_session tool ----
|
|
242
|
+
describe('start_session tool', () => {
|
|
243
|
+
it('calls sessionManager.startSession and returns confirmation', async () => {
|
|
244
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('start_session', { projectPath: '/home/user/alpha' }, 'Started session for alpha'));
|
|
245
|
+
const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient });
|
|
246
|
+
const msg = makeMessage({ content: 'start alpha' });
|
|
247
|
+
await orchestrator.handleMessage(msg);
|
|
248
|
+
assert.equal(sessionManager.startSessionCalls.length, 1);
|
|
249
|
+
assert.equal(sessionManager.startSessionCalls[0].projectDir, '/home/user/alpha');
|
|
250
|
+
assert.equal(msg.sentMessages[0], 'Started session for alpha');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
// ---- get_status tool ----
|
|
254
|
+
describe('get_status tool', () => {
|
|
255
|
+
it('returns formatted session status', async () => {
|
|
256
|
+
const session = makeMockSession({ projectName: 'alpha', status: 'running' });
|
|
257
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('get_status', {}, 'Status: alpha is running'));
|
|
258
|
+
const { orchestrator } = makeOrchestrator({ client: mockClient, sessions: [session] });
|
|
259
|
+
const msg = makeMessage({ content: 'status' });
|
|
260
|
+
await orchestrator.handleMessage(msg);
|
|
261
|
+
assert.equal(msg.sentMessages[0], 'Status: alpha is running');
|
|
262
|
+
});
|
|
263
|
+
it('handles empty session list', async () => {
|
|
264
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('get_status', {}, 'No sessions running'));
|
|
265
|
+
const { orchestrator } = makeOrchestrator({ client: mockClient, sessions: [] });
|
|
266
|
+
const msg = makeMessage({ content: 'status' });
|
|
267
|
+
await orchestrator.handleMessage(msg);
|
|
268
|
+
assert.equal(msg.sentMessages[0], 'No sessions running');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
// ---- stop_session tool ----
|
|
272
|
+
describe('stop_session tool', () => {
|
|
273
|
+
it('stops session matched by sessionId', async () => {
|
|
274
|
+
const session = makeMockSession({ sessionId: 'sess-abc', projectName: 'alpha' });
|
|
275
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('stop_session', { identifier: 'sess-abc' }, 'Stopped alpha'));
|
|
276
|
+
const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient, sessions: [session] });
|
|
277
|
+
const msg = makeMessage({ content: 'stop sess-abc' });
|
|
278
|
+
await orchestrator.handleMessage(msg);
|
|
279
|
+
assert.equal(sessionManager.cancelSessionCalls.length, 1);
|
|
280
|
+
assert.equal(sessionManager.cancelSessionCalls[0], 'sess-abc');
|
|
281
|
+
});
|
|
282
|
+
it('fuzzy matches by project name', async () => {
|
|
283
|
+
const session = makeMockSession({ sessionId: 'sess-xyz', projectName: 'my-big-project' });
|
|
284
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('stop_session', { identifier: 'big-project' }, 'Stopped my-big-project'));
|
|
285
|
+
const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient, sessions: [session] });
|
|
286
|
+
const msg = makeMessage({ content: 'stop big project' });
|
|
287
|
+
await orchestrator.handleMessage(msg);
|
|
288
|
+
assert.equal(sessionManager.cancelSessionCalls.length, 1);
|
|
289
|
+
assert.equal(sessionManager.cancelSessionCalls[0], 'sess-xyz');
|
|
290
|
+
});
|
|
291
|
+
it('returns not-found for unmatched identifier', async () => {
|
|
292
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('stop_session', { identifier: 'nonexistent' }, 'No session found'));
|
|
293
|
+
const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient, sessions: [] });
|
|
294
|
+
const msg = makeMessage({ content: 'stop nonexistent' });
|
|
295
|
+
await orchestrator.handleMessage(msg);
|
|
296
|
+
assert.equal(sessionManager.cancelSessionCalls.length, 0);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
// ---- get_session_detail tool ----
|
|
300
|
+
describe('get_session_detail tool', () => {
|
|
301
|
+
it('returns formatted session detail', async () => {
|
|
302
|
+
const session = makeMockSession({ sessionId: 'sess-detail' });
|
|
303
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('get_session_detail', { sessionId: 'sess-detail' }, 'Session details for my-project'));
|
|
304
|
+
const { orchestrator } = makeOrchestrator({ client: mockClient, sessions: [session] });
|
|
305
|
+
const msg = makeMessage({ content: 'detail sess-detail' });
|
|
306
|
+
await orchestrator.handleMessage(msg);
|
|
307
|
+
assert.equal(msg.sentMessages[0], 'Session details for my-project');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
// ---- Message routing / auth guards ----
|
|
311
|
+
describe('handleMessage routing', () => {
|
|
312
|
+
it('ignores bot messages', async () => {
|
|
313
|
+
const { orchestrator, mockClient } = makeOrchestrator();
|
|
314
|
+
const msg = makeMessage({ bot: true, content: 'hello from bot' });
|
|
315
|
+
await orchestrator.handleMessage(msg);
|
|
316
|
+
assert.equal(mockClient.createCallCount, 0);
|
|
317
|
+
assert.equal(msg.sentMessages.length, 0);
|
|
318
|
+
});
|
|
319
|
+
it('ignores non-owner messages', async () => {
|
|
320
|
+
const { orchestrator, mockClient } = makeOrchestrator();
|
|
321
|
+
const msg = makeMessage({ authorId: 'stranger-456', content: 'hack the planet' });
|
|
322
|
+
await orchestrator.handleMessage(msg);
|
|
323
|
+
assert.equal(mockClient.createCallCount, 0);
|
|
324
|
+
assert.equal(msg.sentMessages.length, 0);
|
|
325
|
+
});
|
|
326
|
+
it('ignores messages from non-control channels', async () => {
|
|
327
|
+
const { orchestrator, mockClient } = makeOrchestrator();
|
|
328
|
+
const msg = makeMessage({ channelId: 'random-channel', content: 'hello' });
|
|
329
|
+
await orchestrator.handleMessage(msg);
|
|
330
|
+
assert.equal(mockClient.createCallCount, 0);
|
|
331
|
+
assert.equal(msg.sentMessages.length, 0);
|
|
332
|
+
});
|
|
333
|
+
it('ignores empty message content', async () => {
|
|
334
|
+
const { orchestrator, mockClient } = makeOrchestrator();
|
|
335
|
+
const msg = makeMessage({ content: ' ' });
|
|
336
|
+
await orchestrator.handleMessage(msg);
|
|
337
|
+
assert.equal(mockClient.createCallCount, 0);
|
|
338
|
+
});
|
|
339
|
+
it('routes valid message through LLM and sends response', async () => {
|
|
340
|
+
const { orchestrator, mockClient } = makeOrchestrator();
|
|
341
|
+
const msg = makeMessage({ content: 'hello orchestrator' });
|
|
342
|
+
await orchestrator.handleMessage(msg);
|
|
343
|
+
assert.equal(mockClient.createCallCount, 1);
|
|
344
|
+
assert.equal(msg.sentMessages.length, 1);
|
|
345
|
+
assert.equal(msg.sentMessages[0], 'Mock LLM response');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
// ---- Conversation history ----
|
|
349
|
+
describe('conversation history', () => {
|
|
350
|
+
it('accumulates user and assistant entries', async () => {
|
|
351
|
+
const { orchestrator } = makeOrchestrator();
|
|
352
|
+
await orchestrator.handleMessage(makeMessage({ content: 'first' }));
|
|
353
|
+
await orchestrator.handleMessage(makeMessage({ content: 'second' }));
|
|
354
|
+
const history = orchestrator.getHistory();
|
|
355
|
+
assert.equal(history.length, 4); // 2 user + 2 assistant
|
|
356
|
+
assert.equal(history[0].role, 'user');
|
|
357
|
+
assert.equal(history[1].role, 'assistant');
|
|
358
|
+
assert.equal(history[2].role, 'user');
|
|
359
|
+
assert.equal(history[3].role, 'assistant');
|
|
360
|
+
});
|
|
361
|
+
it('trims to MAX_HISTORY (30) by removing oldest pairs', async () => {
|
|
362
|
+
const { orchestrator } = makeOrchestrator();
|
|
363
|
+
// Send 17 messages → 34 history entries (17 user + 17 assistant)
|
|
364
|
+
// After trimming: should be ≤30
|
|
365
|
+
for (let i = 0; i < 17; i++) {
|
|
366
|
+
await orchestrator.handleMessage(makeMessage({ content: `msg-${i}` }));
|
|
367
|
+
}
|
|
368
|
+
const history = orchestrator.getHistory();
|
|
369
|
+
assert.ok(history.length <= 30, `History length ${history.length} exceeds 30`);
|
|
370
|
+
// Should have trimmed from the front — oldest entries gone
|
|
371
|
+
// 34 entries → trim 2 at a time until ≤30 → 30 entries (trimmed 4)
|
|
372
|
+
assert.equal(history.length, 30);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
// ---- Error handling ----
|
|
376
|
+
describe('error handling', () => {
|
|
377
|
+
it('sends error message to Discord when LLM API throws', async () => {
|
|
378
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.errorHandler('API rate limit exceeded'));
|
|
379
|
+
const { orchestrator } = makeOrchestrator({ client: mockClient });
|
|
380
|
+
const msg = makeMessage({ content: 'hello' });
|
|
381
|
+
await orchestrator.handleMessage(msg);
|
|
382
|
+
assert.equal(msg.sentMessages.length, 1);
|
|
383
|
+
assert.ok(msg.sentMessages[0].includes('Something went wrong'));
|
|
384
|
+
});
|
|
385
|
+
it('appends error placeholder to history on LLM failure', async () => {
|
|
386
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.errorHandler('Network error'));
|
|
387
|
+
const { orchestrator } = makeOrchestrator({ client: mockClient });
|
|
388
|
+
await orchestrator.handleMessage(makeMessage({ content: 'fail' }));
|
|
389
|
+
const history = orchestrator.getHistory();
|
|
390
|
+
assert.equal(history.length, 2); // user + error assistant
|
|
391
|
+
assert.equal(history[1].role, 'assistant');
|
|
392
|
+
assert.equal(history[1].content, '[error — see logs]');
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
// ---- stop() ----
|
|
396
|
+
describe('stop()', () => {
|
|
397
|
+
it('clears conversation history and nulls client', async () => {
|
|
398
|
+
const { orchestrator } = makeOrchestrator();
|
|
399
|
+
await orchestrator.handleMessage(makeMessage({ content: 'hello' }));
|
|
400
|
+
assert.ok(orchestrator.getHistory().length > 0);
|
|
401
|
+
orchestrator.stop();
|
|
402
|
+
assert.equal(orchestrator.getHistory().length, 0);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
// ---- Tool execution direct tests ----
|
|
406
|
+
describe('tool execution (via agent loop)', () => {
|
|
407
|
+
it('list_projects returns empty message when no projects', async () => {
|
|
408
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('list_projects', {}, 'No projects'));
|
|
409
|
+
const { orchestrator } = makeOrchestrator({ client: mockClient, projects: [] });
|
|
410
|
+
const msg = makeMessage({ content: 'list' });
|
|
411
|
+
await orchestrator.handleMessage(msg);
|
|
412
|
+
// The second create call receives the tool result
|
|
413
|
+
assert.equal(mockClient.createCallCount, 2);
|
|
414
|
+
});
|
|
415
|
+
it('start_session with optional command passes through', async () => {
|
|
416
|
+
const mockClient = new MockAnthropicClient(MockAnthropicClient.toolThenTextHandler('start_session', { projectPath: '/p', command: '/gsd quick fix tests' }, 'Started'));
|
|
417
|
+
const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient });
|
|
418
|
+
const msg = makeMessage({ content: 'start with custom command' });
|
|
419
|
+
await orchestrator.handleMessage(msg);
|
|
420
|
+
assert.equal(sessionManager.startSessionCalls.length, 1);
|
|
421
|
+
assert.equal(sessionManager.startSessionCalls[0].command, '/gsd quick fix tests');
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
//# sourceMappingURL=orchestrator.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.test.js","sourceRoot":"","sources":["../src/orchestrator.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tests for Orchestrator — LLM agent for #gsd-control channel.\n *\n * Uses a MockAnthropicClient that simulates messages.create() responses,\n * allowing tool execution and conversation flow testing without real API calls.\n */\n\nimport { describe, it, afterEach } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { mkdtempSync, rmSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport { Orchestrator, type OrchestratorConfig, type OrchestratorDeps, type DiscordMessageLike } from './orchestrator.js';\nimport { Logger } from './logger.js';\nimport type { ManagedSession, ProjectInfo, SessionStatus, CostAccumulator } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction tmpDir(): string {\n return mkdtempSync(join(tmpdir(), `orch-test-${randomUUID().slice(0, 8)}-`));\n}\n\nconst cleanupDirs: string[] = [];\nconst activeLoggers: Logger[] = [];\n\nasync function cleanupAll(): Promise<void> {\n // Close all loggers first so write streams flush before dirs are removed\n for (const logger of activeLoggers) {\n try { await logger.close(); } catch { /* ignore */ }\n }\n activeLoggers.length = 0;\n\n while (cleanupDirs.length) {\n const d = cleanupDirs.pop()!;\n if (existsSync(d)) rmSync(d, { recursive: true, force: true });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Mock Anthropic Client\n// ---------------------------------------------------------------------------\n\ninterface MockCreateParams {\n model: string;\n max_tokens: number;\n system: string;\n tools: unknown[];\n messages: unknown[];\n}\n\ntype CreateHandler = (params: MockCreateParams) => {\n stop_reason: string;\n content: Array<{ type: string; text?: string; id?: string; name?: string; input?: unknown }>;\n};\n\nclass MockAnthropicClient {\n public createCallCount = 0;\n public lastCreateParams: MockCreateParams | null = null;\n private createHandler: CreateHandler;\n\n constructor(handler?: CreateHandler) {\n this.createHandler = handler ?? MockAnthropicClient.defaultHandler;\n }\n\n /** Default handler: returns a simple text response */\n static defaultHandler(): ReturnType<CreateHandler> {\n return {\n stop_reason: 'end_turn',\n content: [{ type: 'text', text: 'Mock LLM response' }],\n };\n }\n\n /** Handler that simulates a tool call then end_turn */\n static toolThenTextHandler(toolName: string, toolInput: unknown, finalText: string): CreateHandler {\n let callCount = 0;\n return () => {\n callCount++;\n if (callCount === 1) {\n return {\n stop_reason: 'tool_use',\n content: [\n {\n type: 'tool_use',\n id: `toolu_${randomUUID().slice(0, 8)}`,\n name: toolName,\n input: toolInput,\n },\n ],\n };\n }\n return {\n stop_reason: 'end_turn',\n content: [{ type: 'text', text: finalText }],\n };\n };\n }\n\n /** Handler that throws an error */\n static errorHandler(message: string): CreateHandler {\n return () => {\n throw new Error(message);\n };\n }\n\n messages = {\n create: async (params: MockCreateParams) => {\n this.createCallCount++;\n this.lastCreateParams = params;\n return this.createHandler(params);\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Mock SessionManager\n// ---------------------------------------------------------------------------\n\nfunction makeMockSession(overrides: Partial<ManagedSession> = {}): ManagedSession {\n return {\n sessionId: overrides.sessionId ?? 'sess-123',\n projectDir: overrides.projectDir ?? '/home/user/project',\n projectName: overrides.projectName ?? 'my-project',\n status: overrides.status ?? ('running' as SessionStatus),\n client: {} as ManagedSession['client'],\n events: [],\n pendingBlocker: null,\n cost: overrides.cost ?? { totalCost: 0.1234, tokens: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0 } },\n startTime: overrides.startTime ?? Date.now() - 300_000, // 5 min ago\n ...overrides,\n };\n}\n\nclass MockSessionManager {\n public sessions: ManagedSession[] = [];\n public startSessionCalls: Array<{ projectDir: string; command?: string }> = [];\n public cancelSessionCalls: string[] = [];\n public getResultCalls: string[] = [];\n\n async startSession(opts: { projectDir: string; command?: string }): Promise<string> {\n this.startSessionCalls.push(opts);\n return 'sess-new-123';\n }\n\n getSession(sessionId: string): ManagedSession | undefined {\n return this.sessions.find((s) => s.sessionId === sessionId);\n }\n\n getAllSessions(): ManagedSession[] {\n return this.sessions;\n }\n\n async cancelSession(sessionId: string): Promise<void> {\n this.cancelSessionCalls.push(sessionId);\n }\n\n getResult(sessionId: string): Record<string, unknown> {\n const session = this.sessions.find((s) => s.sessionId === sessionId);\n if (!session) throw new Error(`Session not found: ${sessionId}`);\n return {\n sessionId: session.sessionId,\n projectDir: session.projectDir,\n projectName: session.projectName,\n status: session.status,\n durationMs: 300_000,\n cost: session.cost,\n recentEvents: [],\n pendingBlocker: null,\n error: null,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Mock ChannelManager (unused by orchestrator directly, but required by deps)\n// ---------------------------------------------------------------------------\n\nclass MockChannelManager {}\n\n// ---------------------------------------------------------------------------\n// Mock Discord Message\n// ---------------------------------------------------------------------------\n\nfunction makeMessage(overrides: Partial<{\n authorId: string;\n bot: boolean;\n channelId: string;\n content: string;\n}>): DiscordMessageLike & { sentMessages: string[] } {\n const sentMessages: string[] = [];\n return {\n author: {\n id: overrides.authorId ?? 'owner-123',\n bot: overrides.bot ?? false,\n },\n channelId: overrides.channelId ?? 'control-channel-1',\n content: overrides.content ?? 'hello',\n channel: {\n send: async (content: string) => {\n sentMessages.push(content);\n },\n sendTyping: async () => {},\n },\n sentMessages,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Test Setup Factory\n// ---------------------------------------------------------------------------\n\nfunction makeOrchestrator(opts?: {\n client?: MockAnthropicClient;\n sessions?: ManagedSession[];\n projects?: ProjectInfo[];\n}) {\n const dir = tmpDir();\n cleanupDirs.push(dir);\n const logPath = join(dir, 'test.log');\n const logger = new Logger({ filePath: logPath, level: 'debug' });\n activeLoggers.push(logger);\n\n const sessionManager = new MockSessionManager();\n if (opts?.sessions) sessionManager.sessions = opts.sessions;\n\n const projects: ProjectInfo[] = opts?.projects ?? [\n { name: 'alpha', path: '/home/user/alpha', markers: ['git', 'node', 'gsd'], lastModified: Date.now() },\n { name: 'bravo', path: '/home/user/bravo', markers: ['git', 'rust'], lastModified: Date.now() },\n ];\n\n const config: OrchestratorConfig = {\n model: 'claude-sonnet-4-20250514',\n max_tokens: 4096,\n control_channel_id: 'control-channel-1',\n };\n\n const deps: OrchestratorDeps = {\n sessionManager: sessionManager as unknown as OrchestratorDeps['sessionManager'],\n channelManager: new MockChannelManager() as unknown as OrchestratorDeps['channelManager'],\n scanProjects: async () => projects,\n config,\n logger,\n ownerId: 'owner-123',\n };\n\n const mockClient = opts?.client ?? new MockAnthropicClient();\n const orchestrator = new Orchestrator(deps, mockClient as unknown as import('@anthropic-ai/sdk').default);\n\n return { orchestrator, mockClient, sessionManager, logger, logPath };\n}\n\n// ---------------------------------------------------------------------------\n// Tests\n// ---------------------------------------------------------------------------\n\ndescribe('Orchestrator', () => {\n // Clean up after each test so logger streams are flushed before dirs removed\n afterEach(async () => {\n await cleanupAll();\n });\n\n // ---- Tool definitions ----\n\n describe('tool definitions', () => {\n it('passes 5 tools to the Anthropic API', async () => {\n const { orchestrator, mockClient } = makeOrchestrator();\n const msg = makeMessage({ content: 'what can you do?' });\n await orchestrator.handleMessage(msg);\n\n assert.ok(mockClient.lastCreateParams);\n const tools = mockClient.lastCreateParams.tools as Array<{ name: string }>;\n assert.equal(tools.length, 5);\n\n const names = tools.map((t) => t.name).sort();\n assert.deepEqual(names, [\n 'get_session_detail',\n 'get_status',\n 'list_projects',\n 'start_session',\n 'stop_session',\n ]);\n });\n });\n\n // ---- list_projects tool ----\n\n describe('list_projects tool', () => {\n it('returns project list from scanProjects', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler('list_projects', {}, 'Here are your projects'),\n );\n const { orchestrator } = makeOrchestrator({ client: mockClient });\n const msg = makeMessage({ content: 'list my projects' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(msg.sentMessages.length, 1);\n assert.equal(msg.sentMessages[0], 'Here are your projects');\n // The tool was called (2 create calls: tool_use + end_turn)\n assert.equal(mockClient.createCallCount, 2);\n });\n });\n\n // ---- start_session tool ----\n\n describe('start_session tool', () => {\n it('calls sessionManager.startSession and returns confirmation', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler(\n 'start_session',\n { projectPath: '/home/user/alpha' },\n 'Started session for alpha',\n ),\n );\n const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient });\n const msg = makeMessage({ content: 'start alpha' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(sessionManager.startSessionCalls.length, 1);\n assert.equal(sessionManager.startSessionCalls[0]!.projectDir, '/home/user/alpha');\n assert.equal(msg.sentMessages[0], 'Started session for alpha');\n });\n });\n\n // ---- get_status tool ----\n\n describe('get_status tool', () => {\n it('returns formatted session status', async () => {\n const session = makeMockSession({ projectName: 'alpha', status: 'running' as SessionStatus });\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler('get_status', {}, 'Status: alpha is running'),\n );\n const { orchestrator } = makeOrchestrator({ client: mockClient, sessions: [session] });\n const msg = makeMessage({ content: 'status' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(msg.sentMessages[0], 'Status: alpha is running');\n });\n\n it('handles empty session list', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler('get_status', {}, 'No sessions running'),\n );\n const { orchestrator } = makeOrchestrator({ client: mockClient, sessions: [] });\n const msg = makeMessage({ content: 'status' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(msg.sentMessages[0], 'No sessions running');\n });\n });\n\n // ---- stop_session tool ----\n\n describe('stop_session tool', () => {\n it('stops session matched by sessionId', async () => {\n const session = makeMockSession({ sessionId: 'sess-abc', projectName: 'alpha' });\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler(\n 'stop_session',\n { identifier: 'sess-abc' },\n 'Stopped alpha',\n ),\n );\n const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient, sessions: [session] });\n const msg = makeMessage({ content: 'stop sess-abc' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(sessionManager.cancelSessionCalls.length, 1);\n assert.equal(sessionManager.cancelSessionCalls[0], 'sess-abc');\n });\n\n it('fuzzy matches by project name', async () => {\n const session = makeMockSession({ sessionId: 'sess-xyz', projectName: 'my-big-project' });\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler(\n 'stop_session',\n { identifier: 'big-project' },\n 'Stopped my-big-project',\n ),\n );\n const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient, sessions: [session] });\n const msg = makeMessage({ content: 'stop big project' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(sessionManager.cancelSessionCalls.length, 1);\n assert.equal(sessionManager.cancelSessionCalls[0], 'sess-xyz');\n });\n\n it('returns not-found for unmatched identifier', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler(\n 'stop_session',\n { identifier: 'nonexistent' },\n 'No session found',\n ),\n );\n const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient, sessions: [] });\n const msg = makeMessage({ content: 'stop nonexistent' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(sessionManager.cancelSessionCalls.length, 0);\n });\n });\n\n // ---- get_session_detail tool ----\n\n describe('get_session_detail tool', () => {\n it('returns formatted session detail', async () => {\n const session = makeMockSession({ sessionId: 'sess-detail' });\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler(\n 'get_session_detail',\n { sessionId: 'sess-detail' },\n 'Session details for my-project',\n ),\n );\n const { orchestrator } = makeOrchestrator({ client: mockClient, sessions: [session] });\n const msg = makeMessage({ content: 'detail sess-detail' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(msg.sentMessages[0], 'Session details for my-project');\n });\n });\n\n // ---- Message routing / auth guards ----\n\n describe('handleMessage routing', () => {\n it('ignores bot messages', async () => {\n const { orchestrator, mockClient } = makeOrchestrator();\n const msg = makeMessage({ bot: true, content: 'hello from bot' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(mockClient.createCallCount, 0);\n assert.equal(msg.sentMessages.length, 0);\n });\n\n it('ignores non-owner messages', async () => {\n const { orchestrator, mockClient } = makeOrchestrator();\n const msg = makeMessage({ authorId: 'stranger-456', content: 'hack the planet' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(mockClient.createCallCount, 0);\n assert.equal(msg.sentMessages.length, 0);\n });\n\n it('ignores messages from non-control channels', async () => {\n const { orchestrator, mockClient } = makeOrchestrator();\n const msg = makeMessage({ channelId: 'random-channel', content: 'hello' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(mockClient.createCallCount, 0);\n assert.equal(msg.sentMessages.length, 0);\n });\n\n it('ignores empty message content', async () => {\n const { orchestrator, mockClient } = makeOrchestrator();\n const msg = makeMessage({ content: ' ' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(mockClient.createCallCount, 0);\n });\n\n it('routes valid message through LLM and sends response', async () => {\n const { orchestrator, mockClient } = makeOrchestrator();\n const msg = makeMessage({ content: 'hello orchestrator' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(mockClient.createCallCount, 1);\n assert.equal(msg.sentMessages.length, 1);\n assert.equal(msg.sentMessages[0], 'Mock LLM response');\n });\n });\n\n // ---- Conversation history ----\n\n describe('conversation history', () => {\n it('accumulates user and assistant entries', async () => {\n const { orchestrator } = makeOrchestrator();\n\n await orchestrator.handleMessage(makeMessage({ content: 'first' }));\n await orchestrator.handleMessage(makeMessage({ content: 'second' }));\n\n const history = orchestrator.getHistory();\n assert.equal(history.length, 4); // 2 user + 2 assistant\n assert.equal(history[0]!.role, 'user');\n assert.equal(history[1]!.role, 'assistant');\n assert.equal(history[2]!.role, 'user');\n assert.equal(history[3]!.role, 'assistant');\n });\n\n it('trims to MAX_HISTORY (30) by removing oldest pairs', async () => {\n const { orchestrator } = makeOrchestrator();\n\n // Send 17 messages → 34 history entries (17 user + 17 assistant)\n // After trimming: should be ≤30\n for (let i = 0; i < 17; i++) {\n await orchestrator.handleMessage(makeMessage({ content: `msg-${i}` }));\n }\n\n const history = orchestrator.getHistory();\n assert.ok(history.length <= 30, `History length ${history.length} exceeds 30`);\n // Should have trimmed from the front — oldest entries gone\n // 34 entries → trim 2 at a time until ≤30 → 30 entries (trimmed 4)\n assert.equal(history.length, 30);\n });\n });\n\n // ---- Error handling ----\n\n describe('error handling', () => {\n it('sends error message to Discord when LLM API throws', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.errorHandler('API rate limit exceeded'),\n );\n const { orchestrator } = makeOrchestrator({ client: mockClient });\n const msg = makeMessage({ content: 'hello' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(msg.sentMessages.length, 1);\n assert.ok(msg.sentMessages[0]!.includes('Something went wrong'));\n });\n\n it('appends error placeholder to history on LLM failure', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.errorHandler('Network error'),\n );\n const { orchestrator } = makeOrchestrator({ client: mockClient });\n await orchestrator.handleMessage(makeMessage({ content: 'fail' }));\n\n const history = orchestrator.getHistory();\n assert.equal(history.length, 2); // user + error assistant\n assert.equal(history[1]!.role, 'assistant');\n assert.equal(history[1]!.content, '[error — see logs]');\n });\n });\n\n // ---- stop() ----\n\n describe('stop()', () => {\n it('clears conversation history and nulls client', async () => {\n const { orchestrator } = makeOrchestrator();\n\n await orchestrator.handleMessage(makeMessage({ content: 'hello' }));\n assert.ok(orchestrator.getHistory().length > 0);\n\n orchestrator.stop();\n assert.equal(orchestrator.getHistory().length, 0);\n });\n });\n\n // ---- Tool execution direct tests ----\n\n describe('tool execution (via agent loop)', () => {\n it('list_projects returns empty message when no projects', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler('list_projects', {}, 'No projects'),\n );\n const { orchestrator } = makeOrchestrator({ client: mockClient, projects: [] });\n const msg = makeMessage({ content: 'list' });\n await orchestrator.handleMessage(msg);\n\n // The second create call receives the tool result\n assert.equal(mockClient.createCallCount, 2);\n });\n\n it('start_session with optional command passes through', async () => {\n const mockClient = new MockAnthropicClient(\n MockAnthropicClient.toolThenTextHandler(\n 'start_session',\n { projectPath: '/p', command: '/gsd quick fix tests' },\n 'Started',\n ),\n );\n const { orchestrator, sessionManager } = makeOrchestrator({ client: mockClient });\n const msg = makeMessage({ content: 'start with custom command' });\n await orchestrator.handleMessage(msg);\n\n assert.equal(sessionManager.startSessionCalls.length, 1);\n assert.equal(sessionManager.startSessionCalls[0]!.command, '/gsd quick fix tests');\n });\n });\n\n});\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project scanner — discovers projects in configured scan_roots by detecting
|
|
3
|
+
* marker files/directories. Reads one level deep (immediate children only).
|
|
4
|
+
*/
|
|
5
|
+
import type { ProjectInfo } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Scan configured roots for project directories.
|
|
8
|
+
*
|
|
9
|
+
* Behaviour:
|
|
10
|
+
* - Reads immediate children of each root (1 level deep, not recursive)
|
|
11
|
+
* - Skips hidden directories (starting with `.`) and `node_modules`
|
|
12
|
+
* - Skips missing roots and permission-denied entries gracefully
|
|
13
|
+
* - Detects markers via MARKER_MAP; directories with no markers are excluded
|
|
14
|
+
* - Results are sorted alphabetically by name
|
|
15
|
+
* - lastModified is the most recent mtime among detected marker files/dirs
|
|
16
|
+
*/
|
|
17
|
+
export declare function scanForProjects(scanRoots: string[]): Promise<ProjectInfo[]>;
|
|
18
|
+
//# sourceMappingURL=project-scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-scanner.d.ts","sourceRoot":"","sources":["../src/project-scanner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,YAAY,CAAC;AAmB7D;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA6DjF"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project scanner — discovers projects in configured scan_roots by detecting
|
|
3
|
+
* marker files/directories. Reads one level deep (immediate children only).
|
|
4
|
+
*/
|
|
5
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
6
|
+
import { join, basename } from 'node:path';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Marker file → project type mapping
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const MARKER_MAP = new Map([
|
|
11
|
+
['.git', 'git'],
|
|
12
|
+
['package.json', 'node'],
|
|
13
|
+
['.gsd', 'gsd'],
|
|
14
|
+
['Cargo.toml', 'rust'],
|
|
15
|
+
['pyproject.toml', 'python'],
|
|
16
|
+
['go.mod', 'go'],
|
|
17
|
+
]);
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Public API
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Scan configured roots for project directories.
|
|
23
|
+
*
|
|
24
|
+
* Behaviour:
|
|
25
|
+
* - Reads immediate children of each root (1 level deep, not recursive)
|
|
26
|
+
* - Skips hidden directories (starting with `.`) and `node_modules`
|
|
27
|
+
* - Skips missing roots and permission-denied entries gracefully
|
|
28
|
+
* - Detects markers via MARKER_MAP; directories with no markers are excluded
|
|
29
|
+
* - Results are sorted alphabetically by name
|
|
30
|
+
* - lastModified is the most recent mtime among detected marker files/dirs
|
|
31
|
+
*/
|
|
32
|
+
export async function scanForProjects(scanRoots) {
|
|
33
|
+
const results = [];
|
|
34
|
+
for (const root of scanRoots) {
|
|
35
|
+
let entries;
|
|
36
|
+
try {
|
|
37
|
+
entries = await readdir(root);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Missing root or permission error — skip gracefully
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
// Skip hidden directories and node_modules
|
|
45
|
+
if (entry.startsWith('.') || entry === 'node_modules')
|
|
46
|
+
continue;
|
|
47
|
+
const entryPath = join(root, entry);
|
|
48
|
+
// Must be a directory
|
|
49
|
+
let entryStat;
|
|
50
|
+
try {
|
|
51
|
+
entryStat = await stat(entryPath);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Permission error or disappeared entry — skip
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!entryStat.isDirectory())
|
|
58
|
+
continue;
|
|
59
|
+
// Detect markers
|
|
60
|
+
const markers = [];
|
|
61
|
+
let latestMtime = 0;
|
|
62
|
+
for (const [markerFile, markerType] of MARKER_MAP) {
|
|
63
|
+
const markerPath = join(entryPath, markerFile);
|
|
64
|
+
try {
|
|
65
|
+
const markerStat = await stat(markerPath);
|
|
66
|
+
markers.push(markerType);
|
|
67
|
+
if (markerStat.mtimeMs > latestMtime) {
|
|
68
|
+
latestMtime = markerStat.mtimeMs;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Marker doesn't exist — not an error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Only include directories with at least one marker
|
|
76
|
+
if (markers.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
results.push({
|
|
79
|
+
name: basename(entryPath),
|
|
80
|
+
path: entryPath,
|
|
81
|
+
markers,
|
|
82
|
+
lastModified: latestMtime,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Sort alphabetically by name
|
|
87
|
+
results.sort((a, b) => a.name.localeCompare(b.name));
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=project-scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-scanner.js","sourceRoot":"","sources":["../src/project-scanner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAU,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAG3C,8EAA8E;AAC9E,qCAAqC;AACrC,8EAA8E;AAE9E,MAAM,UAAU,GAAuC,IAAI,GAAG,CAAC;IAC7D,CAAC,MAAM,EAAE,KAAK,CAAC;IACf,CAAC,cAAc,EAAE,MAAM,CAAC;IACxB,CAAC,MAAM,EAAE,KAAK,CAAC;IACf,CAAC,YAAY,EAAE,MAAM,CAAC;IACtB,CAAC,gBAAgB,EAAE,QAAQ,CAAC;IAC5B,CAAC,QAAQ,EAAE,IAAI,CAAC;CACjB,CAAC,CAAC;AAEH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAmB;IACvD,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,qDAAqD;YACrD,SAAS;QACX,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,2CAA2C;YAC3C,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,KAAK,cAAc;gBAAE,SAAS;YAEhE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAEpC,sBAAsB;YACtB,IAAI,SAAS,CAAC;YACd,IAAI,CAAC;gBACH,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,+CAA+C;gBAC/C,SAAS;YACX,CAAC;YACD,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;gBAAE,SAAS;YAEvC,iBAAiB;YACjB,MAAM,OAAO,GAAoB,EAAE,CAAC;YACpC,IAAI,WAAW,GAAG,CAAC,CAAC;YAEpB,KAAK,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,IAAI,UAAU,EAAE,CAAC;gBAClD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;gBAC/C,IAAI,CAAC;oBACH,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,CAAC;oBAC1C,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;oBACzB,IAAI,UAAU,CAAC,OAAO,GAAG,WAAW,EAAE,CAAC;wBACrC,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC;oBACnC,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,sCAAsC;gBACxC,CAAC;YACH,CAAC;YAED,oDAAoD;YACpD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAEnC,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC;gBACzB,IAAI,EAAE,SAAS;gBACf,OAAO;gBACP,YAAY,EAAE,WAAW;aAC1B,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAErD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * Project scanner — discovers projects in configured scan_roots by detecting\n * marker files/directories. Reads one level deep (immediate children only).\n */\n\nimport { readdir, stat, access } from 'node:fs/promises';\nimport { join, basename } from 'node:path';\nimport type { ProjectInfo, ProjectMarker } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Marker file → project type mapping\n// ---------------------------------------------------------------------------\n\nconst MARKER_MAP: ReadonlyMap<string, ProjectMarker> = new Map([\n ['.git', 'git'],\n ['package.json', 'node'],\n ['.gsd', 'gsd'],\n ['Cargo.toml', 'rust'],\n ['pyproject.toml', 'python'],\n ['go.mod', 'go'],\n]);\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Scan configured roots for project directories.\n *\n * Behaviour:\n * - Reads immediate children of each root (1 level deep, not recursive)\n * - Skips hidden directories (starting with `.`) and `node_modules`\n * - Skips missing roots and permission-denied entries gracefully\n * - Detects markers via MARKER_MAP; directories with no markers are excluded\n * - Results are sorted alphabetically by name\n * - lastModified is the most recent mtime among detected marker files/dirs\n */\nexport async function scanForProjects(scanRoots: string[]): Promise<ProjectInfo[]> {\n const results: ProjectInfo[] = [];\n\n for (const root of scanRoots) {\n let entries: string[];\n try {\n entries = await readdir(root);\n } catch {\n // Missing root or permission error — skip gracefully\n continue;\n }\n\n for (const entry of entries) {\n // Skip hidden directories and node_modules\n if (entry.startsWith('.') || entry === 'node_modules') continue;\n\n const entryPath = join(root, entry);\n\n // Must be a directory\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch {\n // Permission error or disappeared entry — skip\n continue;\n }\n if (!entryStat.isDirectory()) continue;\n\n // Detect markers\n const markers: ProjectMarker[] = [];\n let latestMtime = 0;\n\n for (const [markerFile, markerType] of MARKER_MAP) {\n const markerPath = join(entryPath, markerFile);\n try {\n const markerStat = await stat(markerPath);\n markers.push(markerType);\n if (markerStat.mtimeMs > latestMtime) {\n latestMtime = markerStat.mtimeMs;\n }\n } catch {\n // Marker doesn't exist — not an error\n }\n }\n\n // Only include directories with at least one marker\n if (markers.length === 0) continue;\n\n results.push({\n name: basename(entryPath),\n path: entryPath,\n markers,\n lastModified: latestMtime,\n });\n }\n }\n\n // Sort alphabetically by name\n results.sort((a, b) => a.name.localeCompare(b.name));\n\n return results;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-scanner.test.d.ts","sourceRoot":"","sources":["../src/project-scanner.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|