@tyvm/knowhow 0.0.108 → 0.0.109-dev.86123ed
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 +2 -4
- package/src/chat/CliChatService.ts +11 -2
- package/src/chat/modules/AgentModule.ts +61 -31
- package/src/chat/modules/SessionsModule.ts +47 -3
- package/src/chat/modules/SystemModule.ts +2 -2
- 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 +18 -17
- package/src/clients/index.ts +31 -11
- package/src/clients/openai.ts +8 -5
- package/src/clients/types.ts +48 -10
- package/src/clients/withRetry.ts +89 -0
- 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 +38 -1
- 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/MediaProcessorService.ts +3 -2
- 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/clients/AIClient.test.ts +446 -0
- package/tests/unit/clients/withRetry.test.ts +319 -0
- package/tests/unit/commands/github-credentials.test.ts +210 -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 +2 -4
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +14 -2
- 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/modules/SystemModule.js +2 -2
- package/ts_build/src/chat/modules/SystemModule.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 +18 -17
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/index.js +9 -10
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.js +4 -4
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +15 -8
- package/ts_build/src/clients/withRetry.d.ts +2 -0
- package/ts_build/src/clients/withRetry.js +60 -0
- package/ts_build/src/clients/withRetry.js.map +1 -0
- 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 +33 -1
- 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/MediaProcessorService.d.ts +2 -1
- package/ts_build/src/services/MediaProcessorService.js +2 -1
- package/ts_build/src/services/MediaProcessorService.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/clients/AIClient.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/AIClient.test.js +339 -0
- package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -0
- package/ts_build/tests/unit/clients/withRetry.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/withRetry.test.js +225 -0
- package/ts_build/tests/unit/clients/withRetry.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 +145 -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
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for withRetry — retry, timeout, and AbortSignal behaviour.
|
|
3
|
+
*
|
|
4
|
+
* These tests are isolated from AIClient: they exercise the withRetry helper
|
|
5
|
+
* directly using a mock async function.
|
|
6
|
+
*/
|
|
7
|
+
import { withRetry } from "../../../src/clients/withRetry";
|
|
8
|
+
|
|
9
|
+
describe("withRetry", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.useRealTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// ─── Success path ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("success path", () => {
|
|
17
|
+
it("returns the result immediately when fn succeeds on the first attempt", async () => {
|
|
18
|
+
const fn = jest.fn().mockResolvedValue({ answer: 42 });
|
|
19
|
+
const result = await withRetry(fn, { maxRetries: 2 });
|
|
20
|
+
expect(result).toEqual({ answer: 42 });
|
|
21
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("passes undefined signal to fn when no timeout or external signal is provided", async () => {
|
|
25
|
+
let receivedSignal: AbortSignal | undefined = "not-called" as any;
|
|
26
|
+
const fn = jest.fn().mockImplementation((signal: any) => {
|
|
27
|
+
receivedSignal = signal;
|
|
28
|
+
return Promise.resolve("ok");
|
|
29
|
+
});
|
|
30
|
+
await withRetry(fn, {});
|
|
31
|
+
expect(receivedSignal).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("passes a signal to fn when a timeout is provided", async () => {
|
|
35
|
+
let receivedSignal: AbortSignal | undefined;
|
|
36
|
+
const fn = jest.fn().mockImplementation((signal: any) => {
|
|
37
|
+
receivedSignal = signal;
|
|
38
|
+
return Promise.resolve("ok");
|
|
39
|
+
});
|
|
40
|
+
await withRetry(fn, { timeout: 5000 });
|
|
41
|
+
expect(receivedSignal).toBeInstanceOf(AbortSignal);
|
|
42
|
+
expect(receivedSignal!.aborted).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ─── Retry path ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("retry on retriable errors", () => {
|
|
49
|
+
it("retries on a 429 rate-limit error and eventually succeeds", async () => {
|
|
50
|
+
jest.useFakeTimers();
|
|
51
|
+
const fn = jest
|
|
52
|
+
.fn()
|
|
53
|
+
.mockRejectedValueOnce(new Error("429 Too Many Requests"))
|
|
54
|
+
.mockResolvedValueOnce({ answer: "retried" });
|
|
55
|
+
|
|
56
|
+
const promise = withRetry(fn, { maxRetries: 2, backoffMs: 100 });
|
|
57
|
+
await jest.runAllTimersAsync();
|
|
58
|
+
const result = await promise;
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual({ answer: "retried" });
|
|
61
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("retries on a 500 server error and eventually succeeds", async () => {
|
|
65
|
+
jest.useFakeTimers();
|
|
66
|
+
const fn = jest
|
|
67
|
+
.fn()
|
|
68
|
+
.mockRejectedValueOnce(new Error("500 Internal Server Error"))
|
|
69
|
+
.mockResolvedValueOnce({
|
|
70
|
+
choices: [{ message: { role: "assistant", content: "hi" } }],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const promise = withRetry(fn, { maxRetries: 2, backoffMs: 100 });
|
|
74
|
+
await jest.runAllTimersAsync();
|
|
75
|
+
const result = await promise;
|
|
76
|
+
|
|
77
|
+
expect(result).toMatchObject({
|
|
78
|
+
choices: [{ message: { content: "hi" } }],
|
|
79
|
+
});
|
|
80
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("retries on ECONNRESET and succeeds", async () => {
|
|
84
|
+
jest.useFakeTimers();
|
|
85
|
+
const fn = jest
|
|
86
|
+
.fn()
|
|
87
|
+
.mockRejectedValueOnce(new Error("ECONNRESET"))
|
|
88
|
+
.mockResolvedValueOnce("success");
|
|
89
|
+
|
|
90
|
+
const promise = withRetry(fn, { maxRetries: 2, backoffMs: 100 });
|
|
91
|
+
await jest.runAllTimersAsync();
|
|
92
|
+
const result = await promise;
|
|
93
|
+
|
|
94
|
+
expect(result).toBe("success");
|
|
95
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("retries up to maxRetries times then throws", async () => {
|
|
99
|
+
jest.useFakeTimers();
|
|
100
|
+
const fn = jest.fn().mockImplementation(() =>
|
|
101
|
+
Promise.reject(new Error("500 Server Error"))
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const resultPromise = withRetry(fn, { maxRetries: 2, backoffMs: 10 });
|
|
105
|
+
// Suppress unhandled rejection warning while timers run
|
|
106
|
+
resultPromise.catch(() => {});
|
|
107
|
+
await jest.runAllTimersAsync();
|
|
108
|
+
await expect(resultPromise).rejects.toThrow("500 Server Error");
|
|
109
|
+
// 1 initial attempt + 2 retries = 3 total calls
|
|
110
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does NOT retry on a non-retriable error (e.g. 400 Bad Request)", async () => {
|
|
114
|
+
const fn = jest
|
|
115
|
+
.fn()
|
|
116
|
+
.mockRejectedValue(new Error("400 Bad Request"));
|
|
117
|
+
await expect(withRetry(fn, { maxRetries: 2 })).rejects.toThrow(
|
|
118
|
+
"400 Bad Request"
|
|
119
|
+
);
|
|
120
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does NOT retry when maxRetries is 0", async () => {
|
|
124
|
+
const fn = jest
|
|
125
|
+
.fn()
|
|
126
|
+
.mockRejectedValue(new Error("500 Server Error"));
|
|
127
|
+
await expect(withRetry(fn, { maxRetries: 0 })).rejects.toThrow(
|
|
128
|
+
"500 Server Error"
|
|
129
|
+
);
|
|
130
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("uses exponential backoff between retries", async () => {
|
|
134
|
+
jest.useFakeTimers();
|
|
135
|
+
|
|
136
|
+
const error = new Error("500 err");
|
|
137
|
+
const fn = jest
|
|
138
|
+
.fn()
|
|
139
|
+
.mockRejectedValueOnce(error)
|
|
140
|
+
.mockRejectedValueOnce(error)
|
|
141
|
+
.mockResolvedValueOnce("done");
|
|
142
|
+
|
|
143
|
+
// Spy on setTimeout to capture backoff delay values
|
|
144
|
+
const observedDelays: number[] = [];
|
|
145
|
+
const origSetTimeout = global.setTimeout;
|
|
146
|
+
const spy = jest
|
|
147
|
+
.spyOn(global, "setTimeout")
|
|
148
|
+
.mockImplementation((cb: any, delay?: number, ...args: any[]) => {
|
|
149
|
+
if (delay === 100 || delay === 200) {
|
|
150
|
+
observedDelays.push(delay!);
|
|
151
|
+
}
|
|
152
|
+
// Always run immediately so the test doesn't stall
|
|
153
|
+
return origSetTimeout(cb, 0, ...args);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const promise = withRetry(fn, { maxRetries: 2, backoffMs: 100 });
|
|
157
|
+
await jest.runAllTimersAsync();
|
|
158
|
+
await promise;
|
|
159
|
+
|
|
160
|
+
spy.mockRestore();
|
|
161
|
+
|
|
162
|
+
// Attempt 0 backoff = 100 * 2^0 = 100ms; attempt 1 backoff = 100 * 2^1 = 200ms
|
|
163
|
+
expect(observedDelays).toContain(100);
|
|
164
|
+
expect(observedDelays).toContain(200);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── Timeout path ────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
describe("per-attempt timeout", () => {
|
|
171
|
+
it("aborts fn via signal when timeout fires and retries on the next attempt", async () => {
|
|
172
|
+
jest.useFakeTimers();
|
|
173
|
+
|
|
174
|
+
// First call: honours the signal abort (simulates slow network)
|
|
175
|
+
// Second call: resolves immediately
|
|
176
|
+
const fn = jest
|
|
177
|
+
.fn()
|
|
178
|
+
.mockImplementationOnce((signal: AbortSignal) => {
|
|
179
|
+
return new Promise<string>((_, reject) => {
|
|
180
|
+
signal.addEventListener("abort", () => reject(signal.reason));
|
|
181
|
+
});
|
|
182
|
+
})
|
|
183
|
+
.mockResolvedValueOnce("completed after timeout retry");
|
|
184
|
+
|
|
185
|
+
const promise = withRetry(fn, {
|
|
186
|
+
timeout: 1000,
|
|
187
|
+
maxRetries: 2,
|
|
188
|
+
backoffMs: 10,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Advance timers: fires the timeout on the first attempt, then the backoff
|
|
192
|
+
await jest.runAllTimersAsync();
|
|
193
|
+
|
|
194
|
+
const result = await promise;
|
|
195
|
+
expect(result).toBe("completed after timeout retry");
|
|
196
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("throws after all retries are exhausted by timeouts", async () => {
|
|
200
|
+
jest.useFakeTimers();
|
|
201
|
+
|
|
202
|
+
// Every call hangs waiting for the abort signal
|
|
203
|
+
const fn = jest.fn().mockImplementation((signal: AbortSignal) => {
|
|
204
|
+
return new Promise<string>((_, reject) => {
|
|
205
|
+
signal.addEventListener("abort", () => reject(signal.reason));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const promise = withRetry(fn, {
|
|
210
|
+
timeout: 500,
|
|
211
|
+
maxRetries: 1,
|
|
212
|
+
backoffMs: 10,
|
|
213
|
+
});
|
|
214
|
+
// Suppress unhandled rejection while timers run
|
|
215
|
+
promise.catch(() => {});
|
|
216
|
+
await jest.runAllTimersAsync();
|
|
217
|
+
|
|
218
|
+
await expect(promise).rejects.toMatchObject({ name: "TimeoutError" });
|
|
219
|
+
// 1 initial + 1 retry, both timed out
|
|
220
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ─── AbortSignal (external cancel) ───────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe("external AbortSignal", () => {
|
|
227
|
+
it("does not call fn when signal is already aborted before invocation", async () => {
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
controller.abort();
|
|
230
|
+
|
|
231
|
+
const fn = jest.fn().mockResolvedValue("should not reach");
|
|
232
|
+
await expect(
|
|
233
|
+
withRetry(fn, { signal: controller.signal, maxRetries: 2 })
|
|
234
|
+
).rejects.toMatchObject({ name: "AbortError" });
|
|
235
|
+
|
|
236
|
+
expect(fn).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("cancels an in-flight request when signal is aborted externally", async () => {
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
|
|
242
|
+
const fn = jest.fn().mockImplementation((signal: AbortSignal) => {
|
|
243
|
+
return new Promise<string>((_, reject) => {
|
|
244
|
+
signal.addEventListener("abort", () => reject(signal.reason));
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const promise = withRetry(fn, {
|
|
249
|
+
signal: controller.signal,
|
|
250
|
+
maxRetries: 2,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Abort from outside while in-flight
|
|
254
|
+
setImmediate(() =>
|
|
255
|
+
controller.abort(new DOMException("User cancelled", "AbortError"))
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await expect(promise).rejects.toMatchObject({ name: "AbortError" });
|
|
259
|
+
// Must NOT retry after external abort
|
|
260
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("does not retry after external abort even if error string looks retriable", async () => {
|
|
264
|
+
const controller = new AbortController();
|
|
265
|
+
controller.abort(new DOMException("Aborted", "AbortError"));
|
|
266
|
+
|
|
267
|
+
const fn = jest.fn().mockResolvedValue("ok");
|
|
268
|
+
await expect(
|
|
269
|
+
withRetry(fn, { signal: controller.signal, maxRetries: 3 })
|
|
270
|
+
).rejects.toMatchObject({ name: "AbortError" });
|
|
271
|
+
|
|
272
|
+
expect(fn).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("forwards the combined signal to fn and it is not aborted on success", async () => {
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
let receivedSignal: AbortSignal | undefined;
|
|
278
|
+
|
|
279
|
+
const fn = jest.fn().mockImplementation((signal: AbortSignal) => {
|
|
280
|
+
receivedSignal = signal;
|
|
281
|
+
return Promise.resolve("ok");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await withRetry(fn, { signal: controller.signal, timeout: 5000 });
|
|
285
|
+
|
|
286
|
+
expect(receivedSignal).toBeInstanceOf(AbortSignal);
|
|
287
|
+
expect(receivedSignal!.aborted).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ─── Combined timeout + external signal ──────────────────────────────────
|
|
292
|
+
|
|
293
|
+
describe("timeout + external signal combined", () => {
|
|
294
|
+
it("abort via external signal takes priority over pending timeout", async () => {
|
|
295
|
+
const controller = new AbortController();
|
|
296
|
+
|
|
297
|
+
const fn = jest.fn().mockImplementation((signal: AbortSignal) => {
|
|
298
|
+
return new Promise<string>((_, reject) => {
|
|
299
|
+
signal.addEventListener("abort", () => reject(signal.reason));
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const promise = withRetry(fn, {
|
|
304
|
+
signal: controller.signal,
|
|
305
|
+
timeout: 10_000, // long timeout — won't fire before external abort
|
|
306
|
+
maxRetries: 2,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Abort externally right away
|
|
310
|
+
setImmediate(() =>
|
|
311
|
+
controller.abort(new DOMException("User cancelled", "AbortError"))
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
await expect(promise).rejects.toMatchObject({ name: "AbortError" });
|
|
315
|
+
// No retry after external abort
|
|
316
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
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.log("some module loading noise that should be suppressed");
|
|
88
|
+
|
|
89
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
90
|
+
consoleSpy.mockRestore();
|
|
91
|
+
process.argv = originalArgv;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("does NOT silence for other commands", () => {
|
|
95
|
+
const originalArgv = process.argv;
|
|
96
|
+
process.argv = ["node", "knowhow", "chat"];
|
|
97
|
+
|
|
98
|
+
const rawArgs = process.argv.slice(2);
|
|
99
|
+
const SILENT_COMMANDS = ["github-credentials"];
|
|
100
|
+
if (rawArgs.some((a) => SILENT_COMMANDS.includes(a))) {
|
|
101
|
+
logger.silence();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
expect(logger.isSilenced()).toBe(false);
|
|
105
|
+
process.argv = originalArgv;
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let program: Command;
|
|
110
|
+
let stdoutSpy: jest.SpyInstance;
|
|
111
|
+
let writtenToStdout: string[];
|
|
112
|
+
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
jest.clearAllMocks();
|
|
115
|
+
|
|
116
|
+
// Reset logger silence state between tests
|
|
117
|
+
logger.unsilence();
|
|
118
|
+
|
|
119
|
+
// Capture process.stdout.write — this is what the credential helper uses
|
|
120
|
+
writtenToStdout = [];
|
|
121
|
+
stdoutSpy = jest
|
|
122
|
+
.spyOn(process.stdout, "write")
|
|
123
|
+
.mockImplementation((chunk: any) => {
|
|
124
|
+
writtenToStdout.push(typeof chunk === "string" ? chunk : chunk.toString());
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
program = new Command();
|
|
129
|
+
program.exitOverride(); // prevent process.exit during tests
|
|
130
|
+
addGithubCredentialsCommand(program);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
stdoutSpy.mockRestore();
|
|
135
|
+
logger.unsilence();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("outputs only credential lines to stdout for 'get' action", async () => {
|
|
139
|
+
await program.parseAsync([
|
|
140
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
expect(writtenToStdout).toHaveLength(1);
|
|
144
|
+
expect(writtenToStdout[0]).toBe(
|
|
145
|
+
"protocol=https\nhost=github.com\nusername=x-access-token\npassword=ghu_TESTTOKEN123\n"
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("silences the logger immediately so module logs don't pollute stdout", async () => {
|
|
150
|
+
await program.parseAsync([
|
|
151
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// The action must have called logger.silence() — state persists after action
|
|
155
|
+
expect(logger.isSilenced()).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("produces exactly 4 credential field lines and nothing else", async () => {
|
|
159
|
+
await program.parseAsync([
|
|
160
|
+
"node", "knowhow", "github-credentials", "get", "--repo", "myorg/myrepo",
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const allOutput = writtenToStdout.join("");
|
|
164
|
+
const lines = allOutput.trim().split("\n");
|
|
165
|
+
|
|
166
|
+
expect(lines).toHaveLength(4);
|
|
167
|
+
expect(lines[0]).toMatch(/^protocol=/);
|
|
168
|
+
expect(lines[1]).toMatch(/^host=/);
|
|
169
|
+
expect(lines[2]).toMatch(/^username=/);
|
|
170
|
+
expect(lines[3]).toMatch(/^password=/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("exits cleanly for 'store' action without writing credentials", async () => {
|
|
174
|
+
let exitCode: number | undefined;
|
|
175
|
+
// Throw to stop execution after exit() is called — otherwise the mock
|
|
176
|
+
// just sets a flag and the action continues to fetch credentials.
|
|
177
|
+
const exitSpy = jest
|
|
178
|
+
.spyOn(process, "exit")
|
|
179
|
+
.mockImplementation(((code?: number) => {
|
|
180
|
+
exitCode = code ?? 0;
|
|
181
|
+
throw new Error(`process.exit(${exitCode})`);
|
|
182
|
+
}) as any);
|
|
183
|
+
|
|
184
|
+
await expect(
|
|
185
|
+
program.parseAsync(["node", "knowhow", "github-credentials", "store"])
|
|
186
|
+
).rejects.toThrow("process.exit(0)");
|
|
187
|
+
|
|
188
|
+
expect(exitCode).toBe(0);
|
|
189
|
+
expect(writtenToStdout).toHaveLength(0);
|
|
190
|
+
exitSpy.mockRestore();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("exits cleanly for 'erase' action without writing credentials", async () => {
|
|
194
|
+
let exitCode: number | undefined;
|
|
195
|
+
const exitSpy = jest
|
|
196
|
+
.spyOn(process, "exit")
|
|
197
|
+
.mockImplementation(((code?: number) => {
|
|
198
|
+
exitCode = code ?? 0;
|
|
199
|
+
throw new Error(`process.exit(${exitCode})`);
|
|
200
|
+
}) as any);
|
|
201
|
+
|
|
202
|
+
await expect(
|
|
203
|
+
program.parseAsync(["node", "knowhow", "github-credentials", "erase"])
|
|
204
|
+
).rejects.toThrow("process.exit(0)");
|
|
205
|
+
|
|
206
|
+
expect(exitCode).toBe(0);
|
|
207
|
+
expect(writtenToStdout).toHaveLength(0);
|
|
208
|
+
exitSpy.mockRestore();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -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 [
|