@herdctl/core 0.0.2 → 0.2.0
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/LICENSE +21 -0
- package/dist/config/__tests__/agent.test.js +30 -0
- package/dist/config/__tests__/agent.test.js.map +1 -1
- package/dist/config/__tests__/merge.test.js +1 -1
- package/dist/config/__tests__/merge.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +3 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +1005 -3
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +87 -4
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet-manager/__tests__/coverage.test.js +6 -2
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/integration.test.js +5 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/job-control.test.js +13 -14
- package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/reload.test.js +13 -3
- package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/status-queries.test.js +6 -0
- package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/trigger.test.js +10 -2
- package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -1
- package/dist/fleet-manager/config-reload.d.ts +1 -1
- package/dist/fleet-manager/config-reload.js +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts +1 -0
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +1 -0
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/fleet-manager/job-control.d.ts +41 -0
- package/dist/fleet-manager/job-control.d.ts.map +1 -1
- package/dist/fleet-manager/job-control.js +243 -20
- package/dist/fleet-manager/job-control.js.map +1 -1
- package/dist/fleet-manager/schedule-executor.d.ts +20 -0
- package/dist/fleet-manager/schedule-executor.d.ts.map +1 -1
- package/dist/fleet-manager/schedule-executor.js +113 -3
- package/dist/fleet-manager/schedule-executor.js.map +1 -1
- package/dist/fleet-manager/types.d.ts +18 -0
- package/dist/fleet-manager/types.d.ts.map +1 -1
- package/dist/hooks/__tests__/discord-runner.test.d.ts +5 -0
- package/dist/hooks/__tests__/discord-runner.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/discord-runner.test.js +606 -0
- package/dist/hooks/__tests__/discord-runner.test.js.map +1 -0
- package/dist/hooks/__tests__/hook-executor.test.d.ts +5 -0
- package/dist/hooks/__tests__/hook-executor.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/hook-executor.test.js +443 -0
- package/dist/hooks/__tests__/hook-executor.test.js.map +1 -0
- package/dist/hooks/__tests__/shell-runner.test.d.ts +5 -0
- package/dist/hooks/__tests__/shell-runner.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/shell-runner.test.js +201 -0
- package/dist/hooks/__tests__/shell-runner.test.js.map +1 -0
- package/dist/hooks/__tests__/webhook-runner.test.d.ts +5 -0
- package/dist/hooks/__tests__/webhook-runner.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/webhook-runner.test.js +453 -0
- package/dist/hooks/__tests__/webhook-runner.test.js.map +1 -0
- package/dist/hooks/hook-executor.d.ts +129 -0
- package/dist/hooks/hook-executor.d.ts.map +1 -0
- package/dist/hooks/hook-executor.js +195 -0
- package/dist/hooks/hook-executor.js.map +1 -0
- package/dist/hooks/index.d.ts +15 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +18 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/runners/discord.d.ts +66 -0
- package/dist/hooks/runners/discord.d.ts.map +1 -0
- package/dist/hooks/runners/discord.js +294 -0
- package/dist/hooks/runners/discord.js.map +1 -0
- package/dist/hooks/runners/shell.d.ts +71 -0
- package/dist/hooks/runners/shell.d.ts.map +1 -0
- package/dist/hooks/runners/shell.js +177 -0
- package/dist/hooks/runners/shell.js.map +1 -0
- package/dist/hooks/runners/webhook.d.ts +66 -0
- package/dist/hooks/runners/webhook.d.ts.map +1 -0
- package/dist/hooks/runners/webhook.js +163 -0
- package/dist/hooks/runners/webhook.js.map +1 -0
- package/dist/hooks/types.d.ts +196 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +12 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/runner/__tests__/sdk-adapter.test.js +4 -3
- package/dist/runner/__tests__/sdk-adapter.test.js.map +1 -1
- package/dist/runner/message-processor.d.ts +5 -1
- package/dist/runner/message-processor.d.ts.map +1 -1
- package/dist/runner/message-processor.js +238 -18
- package/dist/runner/message-processor.js.map +1 -1
- package/dist/runner/sdk-adapter.d.ts.map +1 -1
- package/dist/runner/sdk-adapter.js +8 -1
- package/dist/runner/sdk-adapter.js.map +1 -1
- package/dist/runner/types.d.ts +23 -2
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/scheduler/scheduler.js +9 -0
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/state/schemas/job-metadata.d.ts +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ShellHookRunner
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
5
|
+
import { ShellHookRunner } from "../runners/shell.js";
|
|
6
|
+
describe("ShellHookRunner", () => {
|
|
7
|
+
// Create a mock logger
|
|
8
|
+
const mockLogger = {
|
|
9
|
+
debug: vi.fn(),
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
// Create a sample hook context
|
|
15
|
+
const sampleContext = {
|
|
16
|
+
event: "completed",
|
|
17
|
+
job: {
|
|
18
|
+
id: "job-2024-01-15-abc123",
|
|
19
|
+
agentId: "test-agent",
|
|
20
|
+
scheduleName: "daily-run",
|
|
21
|
+
startedAt: "2024-01-15T10:00:00.000Z",
|
|
22
|
+
completedAt: "2024-01-15T10:05:00.000Z",
|
|
23
|
+
durationMs: 300000,
|
|
24
|
+
},
|
|
25
|
+
result: {
|
|
26
|
+
success: true,
|
|
27
|
+
output: "Job completed successfully",
|
|
28
|
+
},
|
|
29
|
+
agent: {
|
|
30
|
+
id: "test-agent",
|
|
31
|
+
name: "Test Agent",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
describe("execute", () => {
|
|
38
|
+
it("should execute a simple shell command successfully", async () => {
|
|
39
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
40
|
+
const config = {
|
|
41
|
+
type: "shell",
|
|
42
|
+
command: "echo 'test output'",
|
|
43
|
+
};
|
|
44
|
+
const result = await runner.execute(config, sampleContext);
|
|
45
|
+
expect(result.success).toBe(true);
|
|
46
|
+
expect(result.hookType).toBe("shell");
|
|
47
|
+
expect(result.exitCode).toBe(0);
|
|
48
|
+
expect(result.output).toBe("test output");
|
|
49
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
50
|
+
expect(mockLogger.info).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
it("should pass hook context as JSON on stdin", async () => {
|
|
53
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
54
|
+
// Use cat to read stdin and output it
|
|
55
|
+
const config = {
|
|
56
|
+
type: "shell",
|
|
57
|
+
command: "cat",
|
|
58
|
+
};
|
|
59
|
+
const result = await runner.execute(config, sampleContext);
|
|
60
|
+
expect(result.success).toBe(true);
|
|
61
|
+
expect(result.output).toBeDefined();
|
|
62
|
+
// Parse the output to verify it's valid JSON matching our context
|
|
63
|
+
const parsedOutput = JSON.parse(result.output);
|
|
64
|
+
expect(parsedOutput.event).toBe("completed");
|
|
65
|
+
expect(parsedOutput.job.id).toBe("job-2024-01-15-abc123");
|
|
66
|
+
expect(parsedOutput.job.agentId).toBe("test-agent");
|
|
67
|
+
expect(parsedOutput.result.success).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it("should handle command failure with non-zero exit code", async () => {
|
|
70
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
71
|
+
const config = {
|
|
72
|
+
type: "shell",
|
|
73
|
+
command: "exit 1",
|
|
74
|
+
};
|
|
75
|
+
const result = await runner.execute(config, sampleContext);
|
|
76
|
+
expect(result.success).toBe(false);
|
|
77
|
+
expect(result.exitCode).toBe(1);
|
|
78
|
+
expect(result.error).toContain("Exit code 1");
|
|
79
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
it("should capture stderr on failure", async () => {
|
|
82
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
83
|
+
const config = {
|
|
84
|
+
type: "shell",
|
|
85
|
+
command: "echo 'error message' >&2 && exit 1",
|
|
86
|
+
};
|
|
87
|
+
const result = await runner.execute(config, sampleContext);
|
|
88
|
+
expect(result.success).toBe(false);
|
|
89
|
+
expect(result.error).toContain("error message");
|
|
90
|
+
});
|
|
91
|
+
it("should handle command not found", async () => {
|
|
92
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
93
|
+
const config = {
|
|
94
|
+
type: "shell",
|
|
95
|
+
command: "nonexistent_command_xyz_123",
|
|
96
|
+
};
|
|
97
|
+
const result = await runner.execute(config, sampleContext);
|
|
98
|
+
expect(result.success).toBe(false);
|
|
99
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
// Skip: Flaky in CI - process signal handling varies across environments
|
|
102
|
+
it.skip("should respect timeout configuration", async () => {
|
|
103
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
104
|
+
const config = {
|
|
105
|
+
type: "shell",
|
|
106
|
+
command: "node -e \"setTimeout(() => {}, 100000)\"",
|
|
107
|
+
timeout: 100, // 100ms timeout
|
|
108
|
+
};
|
|
109
|
+
const result = await runner.execute(config, sampleContext);
|
|
110
|
+
expect(result.success).toBe(false);
|
|
111
|
+
expect(result.error).toContain("timed out");
|
|
112
|
+
});
|
|
113
|
+
it("should use default timeout when not specified", async () => {
|
|
114
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
115
|
+
const config = {
|
|
116
|
+
type: "shell",
|
|
117
|
+
command: "echo 'quick'",
|
|
118
|
+
// No timeout specified - should use default 30000ms
|
|
119
|
+
};
|
|
120
|
+
const result = await runner.execute(config, sampleContext);
|
|
121
|
+
expect(result.success).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
it("should handle commands with pipes", async () => {
|
|
124
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
125
|
+
const config = {
|
|
126
|
+
type: "shell",
|
|
127
|
+
command: "echo 'hello world' | tr '[:lower:]' '[:upper:]'",
|
|
128
|
+
};
|
|
129
|
+
const result = await runner.execute(config, sampleContext);
|
|
130
|
+
expect(result.success).toBe(true);
|
|
131
|
+
expect(result.output).toBe("HELLO WORLD");
|
|
132
|
+
});
|
|
133
|
+
it("should pass environment variables to the command", async () => {
|
|
134
|
+
const runner = new ShellHookRunner({
|
|
135
|
+
logger: mockLogger,
|
|
136
|
+
env: { MY_VAR: "test_value" },
|
|
137
|
+
});
|
|
138
|
+
const config = {
|
|
139
|
+
type: "shell",
|
|
140
|
+
command: "echo $MY_VAR",
|
|
141
|
+
};
|
|
142
|
+
const result = await runner.execute(config, sampleContext);
|
|
143
|
+
expect(result.success).toBe(true);
|
|
144
|
+
expect(result.output).toBe("test_value");
|
|
145
|
+
});
|
|
146
|
+
it("should handle failed event context", async () => {
|
|
147
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
148
|
+
const failedContext = {
|
|
149
|
+
...sampleContext,
|
|
150
|
+
event: "failed",
|
|
151
|
+
result: {
|
|
152
|
+
success: false,
|
|
153
|
+
output: "Job failed",
|
|
154
|
+
error: "Connection timeout",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const config = {
|
|
158
|
+
type: "shell",
|
|
159
|
+
command: "cat",
|
|
160
|
+
};
|
|
161
|
+
const result = await runner.execute(config, failedContext);
|
|
162
|
+
expect(result.success).toBe(true);
|
|
163
|
+
const parsedOutput = JSON.parse(result.output);
|
|
164
|
+
expect(parsedOutput.event).toBe("failed");
|
|
165
|
+
expect(parsedOutput.result.success).toBe(false);
|
|
166
|
+
expect(parsedOutput.result.error).toBe("Connection timeout");
|
|
167
|
+
});
|
|
168
|
+
it("should measure execution duration", async () => {
|
|
169
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
170
|
+
const config = {
|
|
171
|
+
type: "shell",
|
|
172
|
+
command: "sleep 0.1",
|
|
173
|
+
};
|
|
174
|
+
const result = await runner.execute(config, sampleContext);
|
|
175
|
+
expect(result.success).toBe(true);
|
|
176
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(80); // Allow some tolerance
|
|
177
|
+
expect(result.durationMs).toBeLessThan(5000); // Should not take too long
|
|
178
|
+
});
|
|
179
|
+
it("should work without a logger", async () => {
|
|
180
|
+
const runner = new ShellHookRunner(); // No options
|
|
181
|
+
const config = {
|
|
182
|
+
type: "shell",
|
|
183
|
+
command: "echo 'no logger'",
|
|
184
|
+
};
|
|
185
|
+
const result = await runner.execute(config, sampleContext);
|
|
186
|
+
expect(result.success).toBe(true);
|
|
187
|
+
expect(result.output).toBe("no logger");
|
|
188
|
+
});
|
|
189
|
+
it("should handle multiline output", async () => {
|
|
190
|
+
const runner = new ShellHookRunner({ logger: mockLogger });
|
|
191
|
+
const config = {
|
|
192
|
+
type: "shell",
|
|
193
|
+
command: "echo 'line1'; echo 'line2'; echo 'line3'",
|
|
194
|
+
};
|
|
195
|
+
const result = await runner.execute(config, sampleContext);
|
|
196
|
+
expect(result.success).toBe(true);
|
|
197
|
+
expect(result.output).toBe("line1\nline2\nline3");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
//# sourceMappingURL=shell-runner.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-runner.test.js","sourceRoot":"","sources":["../../../src/hooks/__tests__/shell-runner.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-runner.test.d.ts","sourceRoot":"","sources":["../../../src/hooks/__tests__/webhook-runner.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for WebhookHookRunner
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import { WebhookHookRunner } from "../runners/webhook.js";
|
|
6
|
+
describe("WebhookHookRunner", () => {
|
|
7
|
+
// Create a mock logger
|
|
8
|
+
const mockLogger = {
|
|
9
|
+
debug: vi.fn(),
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
// Create a sample hook context
|
|
15
|
+
const sampleContext = {
|
|
16
|
+
event: "completed",
|
|
17
|
+
job: {
|
|
18
|
+
id: "job-2024-01-15-abc123",
|
|
19
|
+
agentId: "test-agent",
|
|
20
|
+
scheduleName: "daily-run",
|
|
21
|
+
startedAt: "2024-01-15T10:00:00.000Z",
|
|
22
|
+
completedAt: "2024-01-15T10:05:00.000Z",
|
|
23
|
+
durationMs: 300000,
|
|
24
|
+
},
|
|
25
|
+
result: {
|
|
26
|
+
success: true,
|
|
27
|
+
output: "Job completed successfully",
|
|
28
|
+
},
|
|
29
|
+
agent: {
|
|
30
|
+
id: "test-agent",
|
|
31
|
+
name: "Test Agent",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
// Store original env
|
|
35
|
+
const originalEnv = { ...process.env };
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
// Reset env before each test
|
|
39
|
+
process.env = { ...originalEnv };
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
// Restore original env
|
|
43
|
+
process.env = originalEnv;
|
|
44
|
+
});
|
|
45
|
+
describe("execute", () => {
|
|
46
|
+
it("should POST to the webhook URL successfully", async () => {
|
|
47
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
48
|
+
ok: true,
|
|
49
|
+
status: 200,
|
|
50
|
+
statusText: "OK",
|
|
51
|
+
text: () => Promise.resolve('{"received": true}'),
|
|
52
|
+
});
|
|
53
|
+
const runner = new WebhookHookRunner({
|
|
54
|
+
logger: mockLogger,
|
|
55
|
+
fetch: mockFetch,
|
|
56
|
+
});
|
|
57
|
+
const config = {
|
|
58
|
+
type: "webhook",
|
|
59
|
+
url: "https://api.example.com/hooks/job-complete",
|
|
60
|
+
};
|
|
61
|
+
const result = await runner.execute(config, sampleContext);
|
|
62
|
+
expect(result.success).toBe(true);
|
|
63
|
+
expect(result.hookType).toBe("webhook");
|
|
64
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
|
65
|
+
expect(result.output).toBe('{"received": true}');
|
|
66
|
+
// Verify fetch was called correctly
|
|
67
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
68
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
69
|
+
expect(url).toBe("https://api.example.com/hooks/job-complete");
|
|
70
|
+
expect(options.method).toBe("POST");
|
|
71
|
+
expect(options.headers["Content-Type"]).toBe("application/json");
|
|
72
|
+
// Verify body is correct JSON
|
|
73
|
+
const body = JSON.parse(options.body);
|
|
74
|
+
expect(body.event).toBe("completed");
|
|
75
|
+
expect(body.job.id).toBe("job-2024-01-15-abc123");
|
|
76
|
+
expect(body.result.success).toBe(true);
|
|
77
|
+
expect(mockLogger.info).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it("should use PUT method when configured", async () => {
|
|
80
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
81
|
+
ok: true,
|
|
82
|
+
status: 200,
|
|
83
|
+
statusText: "OK",
|
|
84
|
+
text: () => Promise.resolve(""),
|
|
85
|
+
});
|
|
86
|
+
const runner = new WebhookHookRunner({
|
|
87
|
+
logger: mockLogger,
|
|
88
|
+
fetch: mockFetch,
|
|
89
|
+
});
|
|
90
|
+
const config = {
|
|
91
|
+
type: "webhook",
|
|
92
|
+
url: "https://api.example.com/hooks/update",
|
|
93
|
+
method: "PUT",
|
|
94
|
+
};
|
|
95
|
+
const result = await runner.execute(config, sampleContext);
|
|
96
|
+
expect(result.success).toBe(true);
|
|
97
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
98
|
+
expect(options.method).toBe("PUT");
|
|
99
|
+
});
|
|
100
|
+
it("should include custom headers", async () => {
|
|
101
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
102
|
+
ok: true,
|
|
103
|
+
status: 200,
|
|
104
|
+
statusText: "OK",
|
|
105
|
+
text: () => Promise.resolve(""),
|
|
106
|
+
});
|
|
107
|
+
const runner = new WebhookHookRunner({
|
|
108
|
+
logger: mockLogger,
|
|
109
|
+
fetch: mockFetch,
|
|
110
|
+
});
|
|
111
|
+
const config = {
|
|
112
|
+
type: "webhook",
|
|
113
|
+
url: "https://api.example.com/hooks",
|
|
114
|
+
headers: {
|
|
115
|
+
"X-Custom-Header": "custom-value",
|
|
116
|
+
Authorization: "Bearer static-token",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const result = await runner.execute(config, sampleContext);
|
|
120
|
+
expect(result.success).toBe(true);
|
|
121
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
122
|
+
expect(options.headers["X-Custom-Header"]).toBe("custom-value");
|
|
123
|
+
expect(options.headers["Authorization"]).toBe("Bearer static-token");
|
|
124
|
+
expect(options.headers["Content-Type"]).toBe("application/json");
|
|
125
|
+
});
|
|
126
|
+
it("should substitute environment variables in headers", async () => {
|
|
127
|
+
process.env.API_TOKEN = "secret-token-123";
|
|
128
|
+
process.env.CUSTOM_VALUE = "env-custom-value";
|
|
129
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
130
|
+
ok: true,
|
|
131
|
+
status: 200,
|
|
132
|
+
statusText: "OK",
|
|
133
|
+
text: () => Promise.resolve(""),
|
|
134
|
+
});
|
|
135
|
+
const runner = new WebhookHookRunner({
|
|
136
|
+
logger: mockLogger,
|
|
137
|
+
fetch: mockFetch,
|
|
138
|
+
});
|
|
139
|
+
const config = {
|
|
140
|
+
type: "webhook",
|
|
141
|
+
url: "https://api.example.com/hooks",
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: "Bearer ${API_TOKEN}",
|
|
144
|
+
"X-Custom": "${CUSTOM_VALUE}",
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const result = await runner.execute(config, sampleContext);
|
|
148
|
+
expect(result.success).toBe(true);
|
|
149
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
150
|
+
expect(options.headers["Authorization"]).toBe("Bearer secret-token-123");
|
|
151
|
+
expect(options.headers["X-Custom"]).toBe("env-custom-value");
|
|
152
|
+
});
|
|
153
|
+
it("should replace undefined env vars with empty string", async () => {
|
|
154
|
+
// Ensure the env var doesn't exist
|
|
155
|
+
delete process.env.UNDEFINED_VAR;
|
|
156
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
157
|
+
ok: true,
|
|
158
|
+
status: 200,
|
|
159
|
+
statusText: "OK",
|
|
160
|
+
text: () => Promise.resolve(""),
|
|
161
|
+
});
|
|
162
|
+
const runner = new WebhookHookRunner({
|
|
163
|
+
logger: mockLogger,
|
|
164
|
+
fetch: mockFetch,
|
|
165
|
+
});
|
|
166
|
+
const config = {
|
|
167
|
+
type: "webhook",
|
|
168
|
+
url: "https://api.example.com/hooks",
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: "Bearer ${UNDEFINED_VAR}",
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const result = await runner.execute(config, sampleContext);
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
176
|
+
expect(options.headers["Authorization"]).toBe("Bearer ");
|
|
177
|
+
});
|
|
178
|
+
it("should handle multiple env var substitutions in one header", async () => {
|
|
179
|
+
process.env.USER = "testuser";
|
|
180
|
+
process.env.PASS = "testpass";
|
|
181
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
182
|
+
ok: true,
|
|
183
|
+
status: 200,
|
|
184
|
+
statusText: "OK",
|
|
185
|
+
text: () => Promise.resolve(""),
|
|
186
|
+
});
|
|
187
|
+
const runner = new WebhookHookRunner({
|
|
188
|
+
logger: mockLogger,
|
|
189
|
+
fetch: mockFetch,
|
|
190
|
+
});
|
|
191
|
+
const config = {
|
|
192
|
+
type: "webhook",
|
|
193
|
+
url: "https://api.example.com/hooks",
|
|
194
|
+
headers: {
|
|
195
|
+
"X-Credentials": "${USER}:${PASS}",
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
const result = await runner.execute(config, sampleContext);
|
|
199
|
+
expect(result.success).toBe(true);
|
|
200
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
201
|
+
expect(options.headers["X-Credentials"]).toBe("testuser:testpass");
|
|
202
|
+
});
|
|
203
|
+
it("should handle HTTP error responses", async () => {
|
|
204
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
205
|
+
ok: false,
|
|
206
|
+
status: 500,
|
|
207
|
+
statusText: "Internal Server Error",
|
|
208
|
+
text: () => Promise.resolve("Something went wrong"),
|
|
209
|
+
});
|
|
210
|
+
const runner = new WebhookHookRunner({
|
|
211
|
+
logger: mockLogger,
|
|
212
|
+
fetch: mockFetch,
|
|
213
|
+
});
|
|
214
|
+
const config = {
|
|
215
|
+
type: "webhook",
|
|
216
|
+
url: "https://api.example.com/hooks",
|
|
217
|
+
};
|
|
218
|
+
const result = await runner.execute(config, sampleContext);
|
|
219
|
+
expect(result.success).toBe(false);
|
|
220
|
+
expect(result.hookType).toBe("webhook");
|
|
221
|
+
expect(result.error).toContain("HTTP 500");
|
|
222
|
+
expect(result.error).toContain("Internal Server Error");
|
|
223
|
+
expect(result.error).toContain("Something went wrong");
|
|
224
|
+
expect(result.output).toBe("Something went wrong");
|
|
225
|
+
expect(mockLogger.warn).toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
it("should handle 4xx client errors", async () => {
|
|
228
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
229
|
+
ok: false,
|
|
230
|
+
status: 401,
|
|
231
|
+
statusText: "Unauthorized",
|
|
232
|
+
text: () => Promise.resolve("Invalid token"),
|
|
233
|
+
});
|
|
234
|
+
const runner = new WebhookHookRunner({
|
|
235
|
+
logger: mockLogger,
|
|
236
|
+
fetch: mockFetch,
|
|
237
|
+
});
|
|
238
|
+
const config = {
|
|
239
|
+
type: "webhook",
|
|
240
|
+
url: "https://api.example.com/hooks",
|
|
241
|
+
};
|
|
242
|
+
const result = await runner.execute(config, sampleContext);
|
|
243
|
+
expect(result.success).toBe(false);
|
|
244
|
+
expect(result.error).toContain("HTTP 401");
|
|
245
|
+
expect(result.error).toContain("Unauthorized");
|
|
246
|
+
});
|
|
247
|
+
it("should handle network errors", async () => {
|
|
248
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error("Network error: ECONNREFUSED"));
|
|
249
|
+
const runner = new WebhookHookRunner({
|
|
250
|
+
logger: mockLogger,
|
|
251
|
+
fetch: mockFetch,
|
|
252
|
+
});
|
|
253
|
+
const config = {
|
|
254
|
+
type: "webhook",
|
|
255
|
+
url: "https://api.example.com/hooks",
|
|
256
|
+
};
|
|
257
|
+
const result = await runner.execute(config, sampleContext);
|
|
258
|
+
expect(result.success).toBe(false);
|
|
259
|
+
expect(result.error).toContain("ECONNREFUSED");
|
|
260
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
it("should handle timeout", async () => {
|
|
263
|
+
// Create a mock that simulates a timeout by aborting
|
|
264
|
+
const mockFetch = vi.fn().mockImplementation(async (_url, options) => {
|
|
265
|
+
// Wait for abort signal
|
|
266
|
+
return new Promise((_, reject) => {
|
|
267
|
+
options.signal.addEventListener("abort", () => {
|
|
268
|
+
const error = new Error("The operation was aborted");
|
|
269
|
+
error.name = "AbortError";
|
|
270
|
+
reject(error);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
const runner = new WebhookHookRunner({
|
|
275
|
+
logger: mockLogger,
|
|
276
|
+
fetch: mockFetch,
|
|
277
|
+
});
|
|
278
|
+
const config = {
|
|
279
|
+
type: "webhook",
|
|
280
|
+
url: "https://api.example.com/hooks",
|
|
281
|
+
timeout: 50, // Very short timeout
|
|
282
|
+
};
|
|
283
|
+
const result = await runner.execute(config, sampleContext);
|
|
284
|
+
expect(result.success).toBe(false);
|
|
285
|
+
expect(result.error).toContain("timed out");
|
|
286
|
+
expect(result.error).toContain("50ms");
|
|
287
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
it("should use default timeout when not specified", async () => {
|
|
290
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
291
|
+
ok: true,
|
|
292
|
+
status: 200,
|
|
293
|
+
statusText: "OK",
|
|
294
|
+
text: () => Promise.resolve(""),
|
|
295
|
+
});
|
|
296
|
+
const runner = new WebhookHookRunner({
|
|
297
|
+
logger: mockLogger,
|
|
298
|
+
fetch: mockFetch,
|
|
299
|
+
});
|
|
300
|
+
const config = {
|
|
301
|
+
type: "webhook",
|
|
302
|
+
url: "https://api.example.com/hooks",
|
|
303
|
+
// No timeout specified - should use default 10000ms
|
|
304
|
+
};
|
|
305
|
+
const result = await runner.execute(config, sampleContext);
|
|
306
|
+
expect(result.success).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
it("should accept 2xx status codes as success", async () => {
|
|
309
|
+
for (const status of [200, 201, 202, 204]) {
|
|
310
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
311
|
+
ok: true,
|
|
312
|
+
status,
|
|
313
|
+
statusText: "OK",
|
|
314
|
+
text: () => Promise.resolve(""),
|
|
315
|
+
});
|
|
316
|
+
const runner = new WebhookHookRunner({
|
|
317
|
+
logger: mockLogger,
|
|
318
|
+
fetch: mockFetch,
|
|
319
|
+
});
|
|
320
|
+
const config = {
|
|
321
|
+
type: "webhook",
|
|
322
|
+
url: "https://api.example.com/hooks",
|
|
323
|
+
};
|
|
324
|
+
const result = await runner.execute(config, sampleContext);
|
|
325
|
+
expect(result.success).toBe(true);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
it("should handle failed event context", async () => {
|
|
329
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
330
|
+
ok: true,
|
|
331
|
+
status: 200,
|
|
332
|
+
statusText: "OK",
|
|
333
|
+
text: () => Promise.resolve(""),
|
|
334
|
+
});
|
|
335
|
+
const runner = new WebhookHookRunner({
|
|
336
|
+
logger: mockLogger,
|
|
337
|
+
fetch: mockFetch,
|
|
338
|
+
});
|
|
339
|
+
const failedContext = {
|
|
340
|
+
...sampleContext,
|
|
341
|
+
event: "failed",
|
|
342
|
+
result: {
|
|
343
|
+
success: false,
|
|
344
|
+
output: "Job failed",
|
|
345
|
+
error: "Connection timeout",
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
const config = {
|
|
349
|
+
type: "webhook",
|
|
350
|
+
url: "https://api.example.com/hooks",
|
|
351
|
+
};
|
|
352
|
+
const result = await runner.execute(config, failedContext);
|
|
353
|
+
expect(result.success).toBe(true);
|
|
354
|
+
// Verify the failed context was sent in the body
|
|
355
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
356
|
+
const body = JSON.parse(options.body);
|
|
357
|
+
expect(body.event).toBe("failed");
|
|
358
|
+
expect(body.result.success).toBe(false);
|
|
359
|
+
expect(body.result.error).toBe("Connection timeout");
|
|
360
|
+
});
|
|
361
|
+
it("should measure execution duration", async () => {
|
|
362
|
+
const mockFetch = vi.fn().mockImplementation(async () => {
|
|
363
|
+
// Add a small delay to measure
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
status: 200,
|
|
368
|
+
statusText: "OK",
|
|
369
|
+
text: () => Promise.resolve(""),
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
const runner = new WebhookHookRunner({
|
|
373
|
+
logger: mockLogger,
|
|
374
|
+
fetch: mockFetch,
|
|
375
|
+
});
|
|
376
|
+
const config = {
|
|
377
|
+
type: "webhook",
|
|
378
|
+
url: "https://api.example.com/hooks",
|
|
379
|
+
};
|
|
380
|
+
const result = await runner.execute(config, sampleContext);
|
|
381
|
+
expect(result.success).toBe(true);
|
|
382
|
+
expect(result.durationMs).toBeGreaterThanOrEqual(40); // Allow some tolerance
|
|
383
|
+
expect(result.durationMs).toBeLessThan(5000);
|
|
384
|
+
});
|
|
385
|
+
it("should work without a logger", async () => {
|
|
386
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
387
|
+
ok: true,
|
|
388
|
+
status: 200,
|
|
389
|
+
statusText: "OK",
|
|
390
|
+
text: () => Promise.resolve("no logger"),
|
|
391
|
+
});
|
|
392
|
+
const runner = new WebhookHookRunner({ fetch: mockFetch }); // No logger
|
|
393
|
+
const config = {
|
|
394
|
+
type: "webhook",
|
|
395
|
+
url: "https://api.example.com/hooks",
|
|
396
|
+
};
|
|
397
|
+
const result = await runner.execute(config, sampleContext);
|
|
398
|
+
expect(result.success).toBe(true);
|
|
399
|
+
expect(result.output).toBe("no logger");
|
|
400
|
+
});
|
|
401
|
+
it("should handle response body read errors gracefully", async () => {
|
|
402
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
403
|
+
ok: true,
|
|
404
|
+
status: 200,
|
|
405
|
+
statusText: "OK",
|
|
406
|
+
text: () => Promise.reject(new Error("Body read error")),
|
|
407
|
+
});
|
|
408
|
+
const runner = new WebhookHookRunner({
|
|
409
|
+
logger: mockLogger,
|
|
410
|
+
fetch: mockFetch,
|
|
411
|
+
});
|
|
412
|
+
const config = {
|
|
413
|
+
type: "webhook",
|
|
414
|
+
url: "https://api.example.com/hooks",
|
|
415
|
+
};
|
|
416
|
+
const result = await runner.execute(config, sampleContext);
|
|
417
|
+
expect(result.success).toBe(true);
|
|
418
|
+
expect(result.output).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
it("should send complete HookContext in request body", async () => {
|
|
421
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
422
|
+
ok: true,
|
|
423
|
+
status: 200,
|
|
424
|
+
statusText: "OK",
|
|
425
|
+
text: () => Promise.resolve(""),
|
|
426
|
+
});
|
|
427
|
+
const runner = new WebhookHookRunner({
|
|
428
|
+
logger: mockLogger,
|
|
429
|
+
fetch: mockFetch,
|
|
430
|
+
});
|
|
431
|
+
const config = {
|
|
432
|
+
type: "webhook",
|
|
433
|
+
url: "https://api.example.com/hooks",
|
|
434
|
+
};
|
|
435
|
+
await runner.execute(config, sampleContext);
|
|
436
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
437
|
+
const body = JSON.parse(options.body);
|
|
438
|
+
// Verify all context fields are present
|
|
439
|
+
expect(body.event).toBe("completed");
|
|
440
|
+
expect(body.job.id).toBe("job-2024-01-15-abc123");
|
|
441
|
+
expect(body.job.agentId).toBe("test-agent");
|
|
442
|
+
expect(body.job.scheduleName).toBe("daily-run");
|
|
443
|
+
expect(body.job.startedAt).toBe("2024-01-15T10:00:00.000Z");
|
|
444
|
+
expect(body.job.completedAt).toBe("2024-01-15T10:05:00.000Z");
|
|
445
|
+
expect(body.job.durationMs).toBe(300000);
|
|
446
|
+
expect(body.result.success).toBe(true);
|
|
447
|
+
expect(body.result.output).toBe("Job completed successfully");
|
|
448
|
+
expect(body.agent.id).toBe("test-agent");
|
|
449
|
+
expect(body.agent.name).toBe("Test Agent");
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
//# sourceMappingURL=webhook-runner.test.js.map
|