@tyvm/knowhow 0.0.108 → 0.0.109-dev.e88af1e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/package.json +9 -4
- package/scripts/build-for-node.sh +10 -24
- package/scripts/publish.sh +86 -0
- package/src/agents/base/base.ts +10 -0
- package/src/agents/tools/execCommand.ts +49 -6
- package/src/agents/tools/index.ts +0 -1
- package/src/agents/tools/list.ts +0 -2
- package/src/chat/CliChatService.ts +10 -1
- package/src/chat/modules/AgentModule.ts +61 -31
- package/src/chat/modules/SessionsModule.ts +47 -3
- package/src/chat/renderer/CompactRenderer.ts +20 -0
- package/src/chat/renderer/ConsoleRenderer.ts +19 -0
- package/src/chat/renderer/FancyRenderer.ts +19 -0
- package/src/chat/renderer/types.ts +11 -0
- package/src/cli.ts +91 -659
- package/src/clients/anthropic.ts +17 -16
- package/src/clients/index.ts +6 -5
- package/src/clients/types.ts +19 -4
- package/src/cloudWorker.ts +175 -113
- package/src/commands/agent.ts +246 -0
- package/src/commands/misc.ts +174 -0
- package/src/commands/modules.ts +217 -0
- package/src/commands/services.ts +77 -0
- package/src/commands/workers.ts +168 -0
- package/src/config.ts +37 -0
- package/src/fileSync.ts +70 -29
- package/src/hashes.ts +35 -13
- package/src/index.ts +18 -0
- package/src/logger.ts +197 -0
- package/src/plugins/embedding.ts +11 -6
- package/src/plugins/plugins.ts +0 -21
- package/src/plugins/vim.ts +5 -16
- package/src/processors/JsonCompressor.ts +6 -6
- package/src/services/EventService.ts +61 -1
- package/src/services/KnowhowClient.ts +34 -4
- package/src/services/modules/index.ts +95 -51
- package/src/services/modules/types.ts +6 -0
- package/src/tunnel.ts +216 -0
- package/src/types.ts +0 -1
- package/src/worker.ts +105 -312
- package/src/workers/auth/WsMiddleware.ts +99 -0
- package/src/workers/auth/authMiddleware.ts +104 -0
- package/src/workers/auth/types.ts +14 -2
- package/src/workers/tools/index.ts +2 -0
- package/src/workers/tools/reloadConfig.ts +84 -0
- package/tests/services/WorkerReloadConfig.test.ts +141 -0
- package/tests/unit/commands/github-credentials.test.ts +211 -0
- package/tests/unit/modules/moduleLoading.test.ts +39 -37
- package/tests/unit/plugins/pluginLoading.test.ts +0 -85
- package/ts_build/package.json +9 -4
- package/ts_build/src/agents/base/base.js +11 -0
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
- package/ts_build/src/agents/tools/execCommand.js +39 -5
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/index.d.ts +0 -1
- package/ts_build/src/agents/tools/index.js +0 -1
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +0 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +13 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +43 -20
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +37 -3
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/types.d.ts +2 -0
- package/ts_build/src/cli.js +47 -519
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +5 -5
- package/ts_build/src/clients/anthropic.js +17 -16
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/index.js +2 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +3 -2
- package/ts_build/src/cloudWorker.d.ts +14 -0
- package/ts_build/src/cloudWorker.js +105 -66
- package/ts_build/src/cloudWorker.js.map +1 -1
- package/ts_build/src/commands/agent.d.ts +6 -0
- package/ts_build/src/commands/agent.js +229 -0
- package/ts_build/src/commands/agent.js.map +1 -0
- package/ts_build/src/commands/misc.d.ts +10 -0
- package/ts_build/src/commands/misc.js +197 -0
- package/ts_build/src/commands/misc.js.map +1 -0
- package/ts_build/src/commands/modules.d.ts +3 -0
- package/ts_build/src/commands/modules.js +207 -0
- package/ts_build/src/commands/modules.js.map +1 -0
- package/ts_build/src/commands/services.d.ts +5 -0
- package/ts_build/src/commands/services.js +87 -0
- package/ts_build/src/commands/services.js.map +1 -0
- package/ts_build/src/commands/workers.d.ts +6 -0
- package/ts_build/src/commands/workers.js +168 -0
- package/ts_build/src/commands/workers.js.map +1 -0
- package/ts_build/src/config.d.ts +1 -0
- package/ts_build/src/config.js +32 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/fileSync.d.ts +6 -0
- package/ts_build/src/fileSync.js +50 -23
- package/ts_build/src/fileSync.js.map +1 -1
- package/ts_build/src/hashes.d.ts +2 -2
- package/ts_build/src/hashes.js +35 -9
- package/ts_build/src/hashes.js.map +1 -1
- package/ts_build/src/index.d.ts +1 -0
- package/ts_build/src/index.js +17 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/logger.d.ts +21 -0
- package/ts_build/src/logger.js +106 -0
- package/ts_build/src/logger.js.map +1 -0
- package/ts_build/src/plugins/embedding.js +4 -3
- package/ts_build/src/plugins/embedding.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +0 -2
- package/ts_build/src/plugins/plugins.js +0 -11
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/plugins/vim.js +3 -9
- package/ts_build/src/plugins/vim.js.map +1 -1
- package/ts_build/src/processors/JsonCompressor.js +4 -4
- package/ts_build/src/processors/JsonCompressor.js.map +1 -1
- package/ts_build/src/services/EventService.d.ts +6 -1
- package/ts_build/src/services/EventService.js +29 -0
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +13 -1
- package/ts_build/src/services/KnowhowClient.js +19 -2
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +33 -0
- package/ts_build/src/services/modules/index.js +67 -47
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +6 -0
- package/ts_build/src/tunnel.d.ts +27 -0
- package/ts_build/src/tunnel.js +112 -0
- package/ts_build/src/tunnel.js.map +1 -0
- package/ts_build/src/types.d.ts +0 -1
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker.d.ts +1 -4
- package/ts_build/src/worker.js +59 -227
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
- package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
- package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
- package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
- package/ts_build/src/workers/auth/authMiddleware.js +60 -0
- package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
- package/ts_build/src/workers/auth/types.d.ts +8 -1
- package/ts_build/src/workers/tools/index.d.ts +2 -0
- package/ts_build/src/workers/tools/index.js +4 -1
- package/ts_build/src/workers/tools/index.js.map +1 -1
- package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
- package/ts_build/src/workers/tools/reloadConfig.js +48 -0
- package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
- package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
- package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
- package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
- package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
- package/src/agents/tools/executeScript/README.md +0 -94
- package/src/agents/tools/executeScript/definition.ts +0 -79
- package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
- package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
- package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
- package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
- package/src/agents/tools/executeScript/index.ts +0 -98
- package/src/services/script-execution/SandboxContext.ts +0 -282
- package/src/services/script-execution/ScriptExecutor.ts +0 -441
- package/src/services/script-execution/ScriptPolicy.ts +0 -194
- package/src/services/script-execution/ScriptTracer.ts +0 -249
- package/src/services/script-execution/types.ts +0 -134
- package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
- package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
- package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
- package/ts_build/src/agents/tools/executeScript/index.js +0 -72
- package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
- package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
- package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
- package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
- package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
- package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
- package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
- package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
- package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
- package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
- package/ts_build/src/services/script-execution/types.d.ts +0 -108
- package/ts_build/src/services/script-execution/types.js +0 -3
- package/ts_build/src/services/script-execution/types.js.map +0 -1
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* Auth message types for the worker WebSocket authentication protocol.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// Client → Worker: request a challenge
|
|
6
|
+
export interface AuthGetChallengeMessage {
|
|
7
|
+
type: "auth:getChallenge";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Worker → Client: sent when a non-auth message is received while locked
|
|
11
|
+
export interface AuthLockedMessage {
|
|
12
|
+
type: "auth:locked";
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
// Worker → Client: challenge
|
|
6
17
|
export interface AuthChallengeMessage {
|
|
7
18
|
type: "auth:challenge";
|
|
@@ -31,12 +42,13 @@ export interface AuthFailureMessage {
|
|
|
31
42
|
type: "auth:failure";
|
|
32
43
|
reason: "invalid_signature" | "expired" | "unknown_credential";
|
|
33
44
|
}
|
|
34
|
-
|
|
35
45
|
export type AuthMessage =
|
|
46
|
+
| AuthGetChallengeMessage
|
|
36
47
|
| AuthChallengeMessage
|
|
37
48
|
| AuthResponseMessage
|
|
38
49
|
| AuthSuccessMessage
|
|
39
|
-
| AuthFailureMessage
|
|
50
|
+
| AuthFailureMessage
|
|
51
|
+
| AuthLockedMessage;
|
|
40
52
|
|
|
41
53
|
// Passkey credential stored in config
|
|
42
54
|
export interface PasskeyCredential {
|
|
@@ -2,6 +2,7 @@ export * from "./listAllowedPorts";
|
|
|
2
2
|
export * from "./getChallenge";
|
|
3
3
|
export * from "./unlock";
|
|
4
4
|
export * from "./lock";
|
|
5
|
+
export * from "./reloadConfig";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
listAllowedPorts,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
export { makeGetChallengeTool } from "./getChallenge";
|
|
12
13
|
export { makeUnlockTool } from "./unlock";
|
|
13
14
|
export { makeLockTool } from "./lock";
|
|
15
|
+
export { makeReloadConfigTool } from "./reloadConfig";
|
|
14
16
|
|
|
15
17
|
export default {
|
|
16
18
|
tools: { listAllowedPorts },
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Tool } from "../../clients/types";
|
|
2
|
+
import { getConfig } from "../../config";
|
|
3
|
+
import { McpServerService } from "../../services/McpServer";
|
|
4
|
+
import { McpService } from "../../services/Mcp";
|
|
5
|
+
import { ToolsService } from "../../services/Tools";
|
|
6
|
+
|
|
7
|
+
export interface ReloadConfigResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
toolCount: number;
|
|
10
|
+
mcpCount: number;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Factory that creates the reloadConfig tool with access to runtime services.
|
|
16
|
+
* The tool re-reads the config from disk, reconnects all MCPs, and rebuilds
|
|
17
|
+
* the active tool list — the same logic as the WebSocket reloadConfig handler.
|
|
18
|
+
*
|
|
19
|
+
* Typical usage after pulling updated config from the cloud worker API:
|
|
20
|
+
* 1. execCommand("knowhow cloudworker --pull <cloudWorkerId>")
|
|
21
|
+
* 2. reloadConfig()
|
|
22
|
+
*/
|
|
23
|
+
export function makeReloadConfigTool(
|
|
24
|
+
Mcp: McpService,
|
|
25
|
+
Tools: ToolsService,
|
|
26
|
+
mcpServer: McpServerService,
|
|
27
|
+
setToolsToUse: (tools: ReturnType<typeof Tools.getToolsByNames>) => void
|
|
28
|
+
) {
|
|
29
|
+
const reloadConfig = async (): Promise<ReloadConfigResult> => {
|
|
30
|
+
try {
|
|
31
|
+
// Re-read fresh config from disk
|
|
32
|
+
const freshConfig = await getConfig();
|
|
33
|
+
|
|
34
|
+
// Close all existing MCP connections
|
|
35
|
+
await Mcp.closeAll();
|
|
36
|
+
|
|
37
|
+
// Reconnect from fresh config and re-register tools
|
|
38
|
+
await Mcp.connectToConfigured(Tools);
|
|
39
|
+
|
|
40
|
+
// Rebuild the allowed tools list from fresh config
|
|
41
|
+
const allowedToolNames =
|
|
42
|
+
freshConfig.worker?.allowedTools ?? Tools.getToolNames();
|
|
43
|
+
const newToolsToUse = Tools.getToolsByNames(allowedToolNames);
|
|
44
|
+
setToolsToUse(newToolsToUse);
|
|
45
|
+
|
|
46
|
+
// Update the MCP server with the new tool list
|
|
47
|
+
mcpServer.withTools(newToolsToUse);
|
|
48
|
+
|
|
49
|
+
const mcpCount = freshConfig.mcps?.length ?? 0;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
toolCount: newToolsToUse.length,
|
|
54
|
+
mcpCount,
|
|
55
|
+
message: `Config reloaded: ${newToolsToUse.length} tools active, ${mcpCount} MCP(s) configured`,
|
|
56
|
+
};
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
toolCount: 0,
|
|
62
|
+
mcpCount: 0,
|
|
63
|
+
message: `Failed to reload config: ${message}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const reloadConfigDefinition: Tool = {
|
|
69
|
+
type: "function" as const,
|
|
70
|
+
function: {
|
|
71
|
+
name: "reloadConfig",
|
|
72
|
+
description:
|
|
73
|
+
"Reload the worker config from disk, reconnect all MCPs, and rebuild the active tool list. " +
|
|
74
|
+
"Call this after running `knowhow cloudworker --pull <id>` to apply updated MCPs without restarting the worker.",
|
|
75
|
+
parameters: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {},
|
|
78
|
+
required: [],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return { reloadConfig, reloadConfigDefinition };
|
|
84
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the worker reloadConfig message handler.
|
|
3
|
+
*
|
|
4
|
+
* We test the core logic in isolation — simulating the "reloadConfig" message
|
|
5
|
+
* arriving on the WebSocket and verifying that MCPs are torn down and
|
|
6
|
+
* re-connected, and that the tool list is rebuilt from fresh config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { McpService } from "../../src/services/Mcp";
|
|
10
|
+
import { ToolsService } from "../../src/services/Tools";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers: build the same reload logic that lives in worker.ts so we can
|
|
14
|
+
// test it in isolation without spinning up a real WebSocket server.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
async function simulateReloadConfig(
|
|
18
|
+
Mcp: McpService,
|
|
19
|
+
Tools: ToolsService,
|
|
20
|
+
mcpServer: { withTools: (tools: unknown[]) => void },
|
|
21
|
+
getConfig: () => Promise<{ worker?: { allowedTools?: string[] } }>,
|
|
22
|
+
toolsToUseRef: { value: unknown[] }
|
|
23
|
+
) {
|
|
24
|
+
// This mirrors the handler in worker.ts
|
|
25
|
+
const freshConfig = await getConfig();
|
|
26
|
+
await Mcp.closeAll();
|
|
27
|
+
await Mcp.connectToConfigured(Tools);
|
|
28
|
+
const allowedToolNames =
|
|
29
|
+
freshConfig.worker?.allowedTools ?? Tools.getToolNames();
|
|
30
|
+
toolsToUseRef.value = Tools.getToolsByNames(allowedToolNames);
|
|
31
|
+
mcpServer.withTools(toolsToUseRef.value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Tests
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe("Worker reloadConfig handler", () => {
|
|
39
|
+
let Mcp: McpService;
|
|
40
|
+
let Tools: ToolsService;
|
|
41
|
+
let mcpServer: { withTools: jest.Mock };
|
|
42
|
+
let toolsToUseRef: { value: unknown[] };
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
Mcp = new McpService();
|
|
46
|
+
Tools = new ToolsService();
|
|
47
|
+
mcpServer = { withTools: jest.fn() };
|
|
48
|
+
toolsToUseRef = { value: [] };
|
|
49
|
+
|
|
50
|
+
// Spy on MCP methods
|
|
51
|
+
jest.spyOn(Mcp, "closeAll").mockResolvedValue(undefined);
|
|
52
|
+
jest.spyOn(Mcp, "connectToConfigured").mockResolvedValue(undefined);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should call closeAll() to tear down existing MCP connections", async () => {
|
|
60
|
+
const getConfig = jest
|
|
61
|
+
.fn()
|
|
62
|
+
.mockResolvedValue({ worker: { allowedTools: [] } });
|
|
63
|
+
|
|
64
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
65
|
+
|
|
66
|
+
expect(Mcp.closeAll).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should call connectToConfigured() to reconnect MCPs from fresh config", async () => {
|
|
70
|
+
const getConfig = jest
|
|
71
|
+
.fn()
|
|
72
|
+
.mockResolvedValue({ worker: { allowedTools: [] } });
|
|
73
|
+
|
|
74
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
75
|
+
|
|
76
|
+
expect(Mcp.connectToConfigured).toHaveBeenCalledWith(Tools);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should rebuild the tool list from allowedTools in fresh config", async () => {
|
|
80
|
+
// Spy on getToolsByNames so we can track what names were requested
|
|
81
|
+
const toolsByNamesSpy = jest
|
|
82
|
+
.spyOn(Tools, "getToolsByNames")
|
|
83
|
+
.mockReturnValue([{ function: { name: "execCommand" } }] as ReturnType<ToolsService["getToolsByNames"]>);
|
|
84
|
+
|
|
85
|
+
const getConfig = jest
|
|
86
|
+
.fn()
|
|
87
|
+
.mockResolvedValue({ worker: { allowedTools: ["execCommand"] } });
|
|
88
|
+
|
|
89
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
90
|
+
|
|
91
|
+
expect(toolsByNamesSpy).toHaveBeenCalledWith(["execCommand"]);
|
|
92
|
+
expect(toolsToUseRef.value).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should fall back to all tool names when allowedTools is not set", async () => {
|
|
96
|
+
const allNames = ["execCommand", "readFile", "writeFileChunk"];
|
|
97
|
+
jest.spyOn(Tools, "getToolNames").mockReturnValue(allNames);
|
|
98
|
+
const toolsByNamesSpy = jest
|
|
99
|
+
.spyOn(Tools, "getToolsByNames")
|
|
100
|
+
.mockReturnValue([] as ReturnType<ToolsService["getToolsByNames"]>);
|
|
101
|
+
|
|
102
|
+
// Config has no worker.allowedTools
|
|
103
|
+
const getConfig = jest.fn().mockResolvedValue({});
|
|
104
|
+
|
|
105
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
106
|
+
|
|
107
|
+
expect(toolsByNamesSpy).toHaveBeenCalledWith(allNames);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should call mcpServer.withTools() with the rebuilt tool list", async () => {
|
|
111
|
+
const fakeTools = [{ function: { name: "readFile" } }] as ReturnType<ToolsService["getToolsByNames"]>;
|
|
112
|
+
jest.spyOn(Tools, "getToolsByNames").mockReturnValue(fakeTools);
|
|
113
|
+
|
|
114
|
+
const getConfig = jest
|
|
115
|
+
.fn()
|
|
116
|
+
.mockResolvedValue({ worker: { allowedTools: ["readFile"] } });
|
|
117
|
+
|
|
118
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
119
|
+
|
|
120
|
+
expect(mcpServer.withTools).toHaveBeenCalledWith(fakeTools);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should re-read the config on every reload (not use stale config)", async () => {
|
|
124
|
+
const getConfig = jest
|
|
125
|
+
.fn()
|
|
126
|
+
.mockResolvedValueOnce({ worker: { allowedTools: ["execCommand"] } })
|
|
127
|
+
.mockResolvedValueOnce({ worker: { allowedTools: ["readFile", "writeFileChunk"] } });
|
|
128
|
+
|
|
129
|
+
jest.spyOn(Tools, "getToolsByNames").mockReturnValue([]);
|
|
130
|
+
|
|
131
|
+
// First reload
|
|
132
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
133
|
+
// Second reload
|
|
134
|
+
await simulateReloadConfig(Mcp, Tools, mcpServer, getConfig, toolsToUseRef);
|
|
135
|
+
|
|
136
|
+
expect(getConfig).toHaveBeenCalledTimes(2);
|
|
137
|
+
// Each reload should tear down and reconnect
|
|
138
|
+
expect(Mcp.closeAll).toHaveBeenCalledTimes(2);
|
|
139
|
+
expect(Mcp.connectToConfigured).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the github-credentials command.
|
|
3
|
+
*
|
|
4
|
+
* Key invariant: running `github-credentials` must NEVER write anything other than
|
|
5
|
+
* the credential lines to stdout. Module loading logs, warnings, etc. must be
|
|
6
|
+
* silenced so the git credential helper protocol is not corrupted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Mock config before any imports that depend on it
|
|
10
|
+
jest.mock("../../../src/config", () => ({
|
|
11
|
+
getConfig: jest.fn().mockResolvedValue({ modules: [] }),
|
|
12
|
+
getGlobalConfig: jest.fn().mockResolvedValue({ modules: [] }),
|
|
13
|
+
getConfigSync: jest.fn().mockReturnValue({}),
|
|
14
|
+
migrateConfig: jest.fn().mockResolvedValue(undefined),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock clients to avoid openai.ts side-effects
|
|
18
|
+
jest.mock("../../../src/clients", () => ({
|
|
19
|
+
AIClient: jest.fn(),
|
|
20
|
+
Clients: { registerClient: jest.fn(), registerModels: jest.fn() },
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock KnowhowSimpleClient so we control what getGitCredential returns
|
|
24
|
+
// without needing a real JWT or network connection
|
|
25
|
+
jest.mock("../../../src/services/KnowhowClient", () => ({
|
|
26
|
+
KnowhowSimpleClient: jest.fn().mockImplementation(() => ({
|
|
27
|
+
getGitCredential: jest.fn().mockResolvedValue({
|
|
28
|
+
protocol: "https",
|
|
29
|
+
host: "github.com",
|
|
30
|
+
username: "x-access-token",
|
|
31
|
+
password: "ghu_TESTTOKEN123",
|
|
32
|
+
}),
|
|
33
|
+
})),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Mock readline so the 'get' action doesn't hang waiting for stdin
|
|
37
|
+
jest.mock("readline", () => ({
|
|
38
|
+
createInterface: jest.fn().mockReturnValue({
|
|
39
|
+
on: jest.fn().mockImplementation(function (event: string, cb: Function) {
|
|
40
|
+
// Immediately fire 'close' so the readline promise resolves
|
|
41
|
+
if (event === "close") {
|
|
42
|
+
setImmediate(() => cb());
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
import { Command } from "commander";
|
|
50
|
+
import { addGithubCredentialsCommand } from "../../../src/commands/misc";
|
|
51
|
+
import { logger } from "../../../src/logger";
|
|
52
|
+
|
|
53
|
+
describe("github-credentials command", () => {
|
|
54
|
+
/**
|
|
55
|
+
* This test verifies the EARLY silencing logic in cli.ts main().
|
|
56
|
+
* The problem: modules load BEFORE parseAsync, so any module that emits
|
|
57
|
+
* warnings (e.g. Terminal module: no TunnelHandler) does so before the
|
|
58
|
+
* action's logger.silence() call can stop it.
|
|
59
|
+
*
|
|
60
|
+
* The fix: cli.ts checks process.argv before module loading and silences early.
|
|
61
|
+
* This test simulates that logic directly.
|
|
62
|
+
*/
|
|
63
|
+
describe("early silencing (pre-module-load)", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
logger.unsilence();
|
|
66
|
+
logger.installConsoleOverload();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
logger.unsilence();
|
|
71
|
+
logger.uninstallConsoleOverload();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("silences before module loading when github-credentials is in argv", () => {
|
|
75
|
+
const originalArgv = process.argv;
|
|
76
|
+
process.argv = ["node", "knowhow", "github-credentials", "get"];
|
|
77
|
+
|
|
78
|
+
// Simulate the exact early-detection logic from cli.ts main()
|
|
79
|
+
const rawArgs = process.argv.slice(2);
|
|
80
|
+
const SILENT_COMMANDS = ["github-credentials"];
|
|
81
|
+
if (rawArgs.some((a) => SILENT_COMMANDS.includes(a))) {
|
|
82
|
+
logger.silence();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Now any module-load-time console.log/warn should be suppressed
|
|
86
|
+
const consoleSpy = jest.spyOn(process.stdout, "write");
|
|
87
|
+
console.warn("⚠️ Terminal module: no TunnelHandler in context — terminal addon not registered");
|
|
88
|
+
console.log("some other module loading noise");
|
|
89
|
+
|
|
90
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
91
|
+
consoleSpy.mockRestore();
|
|
92
|
+
process.argv = originalArgv;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("does NOT silence for other commands", () => {
|
|
96
|
+
const originalArgv = process.argv;
|
|
97
|
+
process.argv = ["node", "knowhow", "chat"];
|
|
98
|
+
|
|
99
|
+
const rawArgs = process.argv.slice(2);
|
|
100
|
+
const SILENT_COMMANDS = ["github-credentials"];
|
|
101
|
+
if (rawArgs.some((a) => SILENT_COMMANDS.includes(a))) {
|
|
102
|
+
logger.silence();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
expect(logger.isSilenced()).toBe(false);
|
|
106
|
+
process.argv = originalArgv;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let program: Command;
|
|
111
|
+
let stdoutSpy: jest.SpyInstance;
|
|
112
|
+
let writtenToStdout: string[];
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
jest.clearAllMocks();
|
|
116
|
+
|
|
117
|
+
// Reset logger silence state between tests
|
|
118
|
+
logger.unsilence();
|
|
119
|
+
|
|
120
|
+
// Capture process.stdout.write — this is what the credential helper uses
|
|
121
|
+
writtenToStdout = [];
|
|
122
|
+
stdoutSpy = jest
|
|
123
|
+
.spyOn(process.stdout, "write")
|
|
124
|
+
.mockImplementation((chunk: any) => {
|
|
125
|
+
writtenToStdout.push(typeof chunk === "string" ? chunk : chunk.toString());
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
program = new Command();
|
|
130
|
+
program.exitOverride(); // prevent process.exit during tests
|
|
131
|
+
addGithubCredentialsCommand(program);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
stdoutSpy.mockRestore();
|
|
136
|
+
logger.unsilence();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("outputs only credential lines to stdout for 'get' action", async () => {
|
|
140
|
+
await program.parseAsync([
|
|
141
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
expect(writtenToStdout).toHaveLength(1);
|
|
145
|
+
expect(writtenToStdout[0]).toBe(
|
|
146
|
+
"protocol=https\nhost=github.com\nusername=x-access-token\npassword=ghu_TESTTOKEN123\n"
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("silences the logger immediately so module logs don't pollute stdout", async () => {
|
|
151
|
+
await program.parseAsync([
|
|
152
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// The action must have called logger.silence() — state persists after action
|
|
156
|
+
expect(logger.isSilenced()).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("produces exactly 4 credential field lines and nothing else", async () => {
|
|
160
|
+
await program.parseAsync([
|
|
161
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
const allOutput = writtenToStdout.join("");
|
|
165
|
+
const lines = allOutput.trim().split("\n");
|
|
166
|
+
|
|
167
|
+
expect(lines).toHaveLength(4);
|
|
168
|
+
expect(lines[0]).toMatch(/^protocol=/);
|
|
169
|
+
expect(lines[1]).toMatch(/^host=/);
|
|
170
|
+
expect(lines[2]).toMatch(/^username=/);
|
|
171
|
+
expect(lines[3]).toMatch(/^password=/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("exits cleanly for 'store' action without writing credentials", async () => {
|
|
175
|
+
let exitCode: number | undefined;
|
|
176
|
+
// Throw to stop execution after exit() is called — otherwise the mock
|
|
177
|
+
// just sets a flag and the action continues to fetch credentials.
|
|
178
|
+
const exitSpy = jest
|
|
179
|
+
.spyOn(process, "exit")
|
|
180
|
+
.mockImplementation(((code?: number) => {
|
|
181
|
+
exitCode = code ?? 0;
|
|
182
|
+
throw new Error(`process.exit(${exitCode})`);
|
|
183
|
+
}) as any);
|
|
184
|
+
|
|
185
|
+
await expect(
|
|
186
|
+
program.parseAsync(["node", "knowhow", "github-credentials", "store"])
|
|
187
|
+
).rejects.toThrow("process.exit(0)");
|
|
188
|
+
|
|
189
|
+
expect(exitCode).toBe(0);
|
|
190
|
+
expect(writtenToStdout).toHaveLength(0);
|
|
191
|
+
exitSpy.mockRestore();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("exits cleanly for 'erase' action without writing credentials", async () => {
|
|
195
|
+
let exitCode: number | undefined;
|
|
196
|
+
const exitSpy = jest
|
|
197
|
+
.spyOn(process, "exit")
|
|
198
|
+
.mockImplementation(((code?: number) => {
|
|
199
|
+
exitCode = code ?? 0;
|
|
200
|
+
throw new Error(`process.exit(${exitCode})`);
|
|
201
|
+
}) as any);
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
program.parseAsync(["node", "knowhow", "github-credentials", "erase"])
|
|
205
|
+
).rejects.toThrow("process.exit(0)");
|
|
206
|
+
|
|
207
|
+
expect(exitCode).toBe(0);
|
|
208
|
+
expect(writtenToStdout).toHaveLength(0);
|
|
209
|
+
exitSpy.mockRestore();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -24,20 +24,27 @@ jest.mock("../../../src/services", () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
import { ModulesService } from "../../../src/services/modules";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
ModuleContext,
|
|
29
|
+
KnowhowModule,
|
|
30
|
+
} from "../../../src/services/modules/types";
|
|
28
31
|
import { getConfig, getGlobalConfig } from "../../../src/config";
|
|
29
32
|
|
|
30
33
|
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
|
|
31
|
-
const mockGetGlobalConfig = getGlobalConfig as jest.MockedFunction<
|
|
34
|
+
const mockGetGlobalConfig = getGlobalConfig as jest.MockedFunction<
|
|
35
|
+
typeof getGlobalConfig
|
|
36
|
+
>;
|
|
32
37
|
|
|
33
38
|
function makeContext(overrides?: Partial<ModuleContext>): ModuleContext {
|
|
34
39
|
return {
|
|
40
|
+
Events: {
|
|
41
|
+
log: jest.fn(),
|
|
42
|
+
} as any,
|
|
35
43
|
Agents: {
|
|
36
44
|
registerAgent: jest.fn(),
|
|
37
45
|
} as any,
|
|
38
46
|
Plugins: {
|
|
39
47
|
registerPlugin: jest.fn(),
|
|
40
|
-
loadPluginsFromConfig: jest.fn().mockResolvedValue(undefined),
|
|
41
48
|
} as any,
|
|
42
49
|
Clients: {
|
|
43
50
|
registerClient: jest.fn(),
|
|
@@ -87,7 +94,8 @@ describe("ModulesService.loadModulesFromConfig", () => {
|
|
|
87
94
|
const context = makeContext();
|
|
88
95
|
|
|
89
96
|
// Mock require used inside loadModulesFromConfig
|
|
90
|
-
const requireSpy = jest
|
|
97
|
+
const requireSpy = jest
|
|
98
|
+
.spyOn(service as any, "loadModulesFromConfig")
|
|
91
99
|
.mockImplementation(async (ctx?: ModuleContext) => {
|
|
92
100
|
const resolvedCtx = ctx || context;
|
|
93
101
|
await mockModule.init({ config: {} as Config, cwd: process.cwd() });
|
|
@@ -113,26 +121,35 @@ describe("ModulesService.loadModulesFromConfig", () => {
|
|
|
113
121
|
};
|
|
114
122
|
const mockToolHandler = jest.fn();
|
|
115
123
|
const mockModule = makeModule({
|
|
116
|
-
tools: [
|
|
124
|
+
tools: [
|
|
125
|
+
{ name: "myTool", handler: mockToolHandler, definition: mockToolDef },
|
|
126
|
+
],
|
|
117
127
|
});
|
|
118
128
|
|
|
119
129
|
const service = new ModulesService();
|
|
120
130
|
const context = makeContext();
|
|
121
131
|
|
|
122
|
-
const spy = jest
|
|
132
|
+
const spy = jest
|
|
133
|
+
.spyOn(service as any, "loadModulesFromConfig")
|
|
123
134
|
.mockImplementation(async (ctx?: ModuleContext) => {
|
|
124
135
|
const resolvedCtx = ctx || context;
|
|
125
136
|
await mockModule.init({ config: {} as Config, cwd: process.cwd() });
|
|
126
137
|
for (const tool of mockModule.tools) {
|
|
127
138
|
resolvedCtx.Tools.addTool(tool.definition);
|
|
128
|
-
resolvedCtx.Tools.setFunction(
|
|
139
|
+
resolvedCtx.Tools.setFunction(
|
|
140
|
+
tool.definition.function.name,
|
|
141
|
+
tool.handler
|
|
142
|
+
);
|
|
129
143
|
}
|
|
130
144
|
});
|
|
131
145
|
|
|
132
146
|
await service.loadModulesFromConfig(context);
|
|
133
147
|
|
|
134
148
|
expect(context.Tools.addTool).toHaveBeenCalledWith(mockToolDef);
|
|
135
|
-
expect(context.Tools.setFunction).toHaveBeenCalledWith(
|
|
149
|
+
expect(context.Tools.setFunction).toHaveBeenCalledWith(
|
|
150
|
+
"myTool",
|
|
151
|
+
mockToolHandler
|
|
152
|
+
);
|
|
136
153
|
spy.mockRestore();
|
|
137
154
|
});
|
|
138
155
|
|
|
@@ -147,7 +164,9 @@ describe("ModulesService.loadModulesFromConfig", () => {
|
|
|
147
164
|
embed: () => Promise.resolve([]),
|
|
148
165
|
};
|
|
149
166
|
// ModulePlugin expects a constructor (class), not an instance
|
|
150
|
-
const MockPluginClass = jest
|
|
167
|
+
const MockPluginClass = jest
|
|
168
|
+
.fn()
|
|
169
|
+
.mockImplementation(() => mockPluginInstance);
|
|
151
170
|
const mockModule = makeModule({
|
|
152
171
|
plugins: [{ name: "test-plugin", plugin: MockPluginClass as any }],
|
|
153
172
|
});
|
|
@@ -155,7 +174,8 @@ describe("ModulesService.loadModulesFromConfig", () => {
|
|
|
155
174
|
const service = new ModulesService();
|
|
156
175
|
const context = makeContext();
|
|
157
176
|
|
|
158
|
-
const spy = jest
|
|
177
|
+
const spy = jest
|
|
178
|
+
.spyOn(service as any, "loadModulesFromConfig")
|
|
159
179
|
.mockImplementation(async (ctx?: ModuleContext) => {
|
|
160
180
|
const resolvedCtx = ctx || context;
|
|
161
181
|
await mockModule.init({ config: {} as Config, cwd: process.cwd() });
|
|
@@ -167,36 +187,17 @@ describe("ModulesService.loadModulesFromConfig", () => {
|
|
|
167
187
|
|
|
168
188
|
await service.loadModulesFromConfig(context);
|
|
169
189
|
|
|
170
|
-
expect(context.Plugins.registerPlugin).toHaveBeenCalledWith(
|
|
190
|
+
expect(context.Plugins.registerPlugin).toHaveBeenCalledWith(
|
|
191
|
+
"test-plugin",
|
|
192
|
+
mockPluginInstance
|
|
193
|
+
);
|
|
171
194
|
spy.mockRestore();
|
|
172
195
|
});
|
|
173
196
|
|
|
174
|
-
it("should call loadPluginsFromConfig with both global and local configs", async () => {
|
|
175
|
-
const localConfig = {
|
|
176
|
-
modules: [],
|
|
177
|
-
pluginPackages: { asana: "@knowhow/plugin-asana" },
|
|
178
|
-
} as unknown as Config;
|
|
179
|
-
const globalConfig = {
|
|
180
|
-
modules: [],
|
|
181
|
-
pluginPackages: { linear: "@knowhow/plugin-linear" },
|
|
182
|
-
} as unknown as Config;
|
|
183
|
-
|
|
184
|
-
mockGetConfig.mockResolvedValue(localConfig);
|
|
185
|
-
mockGetGlobalConfig.mockResolvedValue(globalConfig);
|
|
186
|
-
|
|
187
|
-
const service = new ModulesService();
|
|
188
|
-
const context = makeContext();
|
|
189
|
-
|
|
190
|
-
await service.loadModulesFromConfig(context);
|
|
191
|
-
|
|
192
|
-
// pluginService.loadPluginsFromConfig should be called twice: once for local, once for global
|
|
193
|
-
expect(context.Plugins.loadPluginsFromConfig).toHaveBeenCalledTimes(2);
|
|
194
|
-
expect(context.Plugins.loadPluginsFromConfig).toHaveBeenCalledWith(localConfig);
|
|
195
|
-
expect(context.Plugins.loadPluginsFromConfig).toHaveBeenCalledWith(globalConfig);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
197
|
it("should load modules from both global and local config paths", async () => {
|
|
199
|
-
const globalModule = makeModule({
|
|
198
|
+
const globalModule = makeModule({
|
|
199
|
+
agents: [{ name: "GlobalAgent" } as any],
|
|
200
|
+
});
|
|
200
201
|
const localModule = makeModule({ agents: [{ name: "LocalAgent" } as any] });
|
|
201
202
|
|
|
202
203
|
mockGetConfig.mockResolvedValue({
|
|
@@ -210,7 +211,8 @@ describe("ModulesService.loadModulesFromConfig", () => {
|
|
|
210
211
|
const context = makeContext();
|
|
211
212
|
|
|
212
213
|
const loadedPaths: string[] = [];
|
|
213
|
-
const spy = jest
|
|
214
|
+
const spy = jest
|
|
215
|
+
.spyOn(service as any, "loadModulesFromConfig")
|
|
214
216
|
.mockImplementation(async (ctx?: ModuleContext) => {
|
|
215
217
|
const resolvedCtx = ctx || context;
|
|
216
218
|
for (const [path, mod] of [
|