@griffin-app/griffin-executor 0.1.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/README.md +152 -0
- package/dist/adapters/axios.d.ts +5 -0
- package/dist/adapters/axios.d.ts.map +1 -0
- package/dist/adapters/axios.js +36 -0
- package/dist/adapters/axios.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/stub.d.ts +22 -0
- package/dist/adapters/stub.d.ts.map +1 -0
- package/dist/adapters/stub.js +36 -0
- package/dist/adapters/stub.js.map +1 -0
- package/dist/events/adapters/in-memory.d.ts +52 -0
- package/dist/events/adapters/in-memory.d.ts.map +1 -0
- package/dist/events/adapters/in-memory.js +70 -0
- package/dist/events/adapters/in-memory.js.map +1 -0
- package/dist/events/adapters/in-memory.test.d.ts +2 -0
- package/dist/events/adapters/in-memory.test.d.ts.map +1 -0
- package/dist/events/adapters/in-memory.test.js +109 -0
- package/dist/events/adapters/in-memory.test.js.map +1 -0
- package/dist/events/adapters/index.d.ts +9 -0
- package/dist/events/adapters/index.d.ts.map +1 -0
- package/dist/events/adapters/index.js +9 -0
- package/dist/events/adapters/index.js.map +1 -0
- package/dist/events/adapters/kinesis.d.ts +91 -0
- package/dist/events/adapters/kinesis.d.ts.map +1 -0
- package/dist/events/adapters/kinesis.js +136 -0
- package/dist/events/adapters/kinesis.js.map +1 -0
- package/dist/events/adapters/kinesis.test.d.ts +2 -0
- package/dist/events/adapters/kinesis.test.d.ts.map +1 -0
- package/dist/events/adapters/kinesis.test.js +249 -0
- package/dist/events/adapters/kinesis.test.js.map +1 -0
- package/dist/events/emitter.d.ts +68 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +83 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/events/emitter.test.d.ts +2 -0
- package/dist/events/emitter.test.d.ts.map +1 -0
- package/dist/events/emitter.test.js +262 -0
- package/dist/events/emitter.test.js.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +4 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/types.d.ts +112 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +9 -0
- package/dist/events/types.js.map +1 -0
- package/dist/executor.d.ts +4 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +799 -0
- package/dist/executor.js.map +1 -0
- package/dist/executor.test.d.ts +2 -0
- package/dist/executor.test.d.ts.map +1 -0
- package/dist/executor.test.js +1584 -0
- package/dist/executor.test.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/secrets/factory.d.ts +121 -0
- package/dist/secrets/factory.d.ts.map +1 -0
- package/dist/secrets/factory.js +137 -0
- package/dist/secrets/factory.js.map +1 -0
- package/dist/secrets/index.d.ts +14 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +18 -0
- package/dist/secrets/index.js.map +1 -0
- package/dist/secrets/providers/aws.d.ts +63 -0
- package/dist/secrets/providers/aws.d.ts.map +1 -0
- package/dist/secrets/providers/aws.js +110 -0
- package/dist/secrets/providers/aws.js.map +1 -0
- package/dist/secrets/providers/env.d.ts +36 -0
- package/dist/secrets/providers/env.d.ts.map +1 -0
- package/dist/secrets/providers/env.js +37 -0
- package/dist/secrets/providers/env.js.map +1 -0
- package/dist/secrets/providers/index.d.ts +7 -0
- package/dist/secrets/providers/index.d.ts.map +1 -0
- package/dist/secrets/providers/index.js +7 -0
- package/dist/secrets/providers/index.js.map +1 -0
- package/dist/secrets/providers/vault.d.ts +75 -0
- package/dist/secrets/providers/vault.d.ts.map +1 -0
- package/dist/secrets/providers/vault.js +143 -0
- package/dist/secrets/providers/vault.js.map +1 -0
- package/dist/secrets/registry.d.ts +39 -0
- package/dist/secrets/registry.d.ts.map +1 -0
- package/dist/secrets/registry.js +134 -0
- package/dist/secrets/registry.js.map +1 -0
- package/dist/secrets/resolver.d.ts +45 -0
- package/dist/secrets/resolver.d.ts.map +1 -0
- package/dist/secrets/resolver.js +188 -0
- package/dist/secrets/resolver.js.map +1 -0
- package/dist/secrets/secrets.test.d.ts +2 -0
- package/dist/secrets/secrets.test.d.ts.map +1 -0
- package/dist/secrets/secrets.test.js +317 -0
- package/dist/secrets/secrets.test.js.map +1 -0
- package/dist/secrets/types.d.ts +70 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +42 -0
- package/dist/secrets/types.js.map +1 -0
- package/dist/shared.d.ts +8 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +30 -0
- package/dist/shared.js.map +1 -0
- package/dist/test-monitor-types.d.ts +43 -0
- package/dist/test-monitor-types.d.ts.map +1 -0
- package/dist/test-monitor-types.js +2 -0
- package/dist/test-monitor-types.js.map +1 -0
- package/dist/test-plan-types.d.ts +43 -0
- package/dist/test-plan-types.d.ts.map +1 -0
- package/dist/test-plan-types.js +2 -0
- package/dist/test-plan-types.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/dates.d.ts +11 -0
- package/dist/utils/dates.d.ts.map +1 -0
- package/dist/utils/dates.js +13 -0
- package/dist/utils/dates.js.map +1 -0
- package/package.json +39 -0
- package/src/adapters/axios.ts +39 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/stub.ts +47 -0
- package/src/events/adapters/README.md +144 -0
- package/src/events/adapters/in-memory.test.ts +146 -0
- package/src/events/adapters/in-memory.ts +93 -0
- package/src/events/adapters/index.ts +9 -0
- package/src/events/adapters/kinesis.test.ts +323 -0
- package/src/events/adapters/kinesis.ts +211 -0
- package/src/events/emitter.test.ts +327 -0
- package/src/events/emitter.ts +133 -0
- package/src/events/index.ts +3 -0
- package/src/events/types.ts +136 -0
- package/src/executor.test.ts +1732 -0
- package/src/executor.ts +1075 -0
- package/src/index.ts +81 -0
- package/src/secrets/factory.ts +248 -0
- package/src/secrets/index.ts +48 -0
- package/src/secrets/providers/aws.ts +178 -0
- package/src/secrets/providers/env.ts +66 -0
- package/src/secrets/providers/index.ts +15 -0
- package/src/secrets/providers/vault.ts +257 -0
- package/src/secrets/resolver.ts +269 -0
- package/src/secrets/secrets.test.ts +402 -0
- package/src/secrets/types.ts +106 -0
- package/src/shared.ts +46 -0
- package/src/test-monitor-types.ts +49 -0
- package/src/types.ts +114 -0
- package/src/utils/dates.ts +13 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { InMemoryAdapter } from "./in-memory.js";
|
|
3
|
+
import type { ExecutionEvent } from "../types.js";
|
|
4
|
+
|
|
5
|
+
describe("InMemoryAdapter", () => {
|
|
6
|
+
let adapter: InMemoryAdapter;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
adapter = new InMemoryAdapter();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const createMockEvent = (
|
|
13
|
+
type: ExecutionEvent["type"],
|
|
14
|
+
executionId: string,
|
|
15
|
+
seq: number,
|
|
16
|
+
): ExecutionEvent =>
|
|
17
|
+
({
|
|
18
|
+
type,
|
|
19
|
+
event_id: `event-${seq}`,
|
|
20
|
+
seq,
|
|
21
|
+
timestamp: Date.now(),
|
|
22
|
+
monitor_id: "monitor-1",
|
|
23
|
+
execution_id: executionId,
|
|
24
|
+
organization_id: "org-1",
|
|
25
|
+
monitor_name: "Test Monitor",
|
|
26
|
+
monitor_version: "1.0.0",
|
|
27
|
+
node_count: 1,
|
|
28
|
+
edge_count: 1,
|
|
29
|
+
}) as ExecutionEvent;
|
|
30
|
+
|
|
31
|
+
describe("publish", () => {
|
|
32
|
+
it("should store events", async () => {
|
|
33
|
+
const events = [
|
|
34
|
+
createMockEvent("MONITOR_START", "exec-1", 0),
|
|
35
|
+
createMockEvent("MONITOR_END", "exec-1", 1),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
await adapter.publish(events);
|
|
39
|
+
|
|
40
|
+
const stored = adapter.getEvents();
|
|
41
|
+
expect(stored).toHaveLength(2);
|
|
42
|
+
expect(stored[0].type).toBe("MONITOR_START");
|
|
43
|
+
expect(stored[1].type).toBe("MONITOR_END");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should track publish count", async () => {
|
|
47
|
+
await adapter.publish([createMockEvent("MONITOR_START", "exec-1", 0)]);
|
|
48
|
+
await adapter.publish([createMockEvent("MONITOR_END", "exec-1", 1)]);
|
|
49
|
+
|
|
50
|
+
expect(adapter.getPublishCount()).toBe(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should append events across multiple publishes", async () => {
|
|
54
|
+
await adapter.publish([createMockEvent("MONITOR_START", "exec-1", 0)]);
|
|
55
|
+
await adapter.publish([createMockEvent("MONITOR_END", "exec-1", 1)]);
|
|
56
|
+
|
|
57
|
+
const events = adapter.getEvents();
|
|
58
|
+
expect(events).toHaveLength(2);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("getEventsByType", () => {
|
|
63
|
+
it("should filter events by type", async () => {
|
|
64
|
+
const events = [
|
|
65
|
+
createMockEvent("MONITOR_START", "exec-1", 0),
|
|
66
|
+
createMockEvent("MONITOR_END", "exec-1", 1),
|
|
67
|
+
createMockEvent("MONITOR_START", "exec-2", 2),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
await adapter.publish(events);
|
|
71
|
+
|
|
72
|
+
const planStartEvents = adapter.getEventsByType("MONITOR_START");
|
|
73
|
+
expect(planStartEvents).toHaveLength(2);
|
|
74
|
+
expect(planStartEvents.every((e) => e.type === "MONITOR_START")).toBe(
|
|
75
|
+
true,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("getEventsForExecution", () => {
|
|
81
|
+
it("should filter events by executionId", async () => {
|
|
82
|
+
const events = [
|
|
83
|
+
createMockEvent("MONITOR_START", "exec-1", 0),
|
|
84
|
+
createMockEvent("MONITOR_END", "exec-1", 1),
|
|
85
|
+
createMockEvent("MONITOR_START", "exec-2", 2),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
await adapter.publish(events);
|
|
89
|
+
|
|
90
|
+
const exec1Events = adapter.getEventsForExecution("exec-1");
|
|
91
|
+
expect(exec1Events).toHaveLength(2);
|
|
92
|
+
expect(exec1Events.every((e) => e.execution_id === "exec-1")).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("clear", () => {
|
|
97
|
+
it("should clear all events and reset counters", async () => {
|
|
98
|
+
await adapter.publish([createMockEvent("MONITOR_START", "exec-1", 0)]);
|
|
99
|
+
|
|
100
|
+
adapter.clear();
|
|
101
|
+
|
|
102
|
+
expect(adapter.getEvents()).toHaveLength(0);
|
|
103
|
+
expect(adapter.getPublishCount()).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe.skip("latency simulation", () => {
|
|
108
|
+
it("should simulate latency", async () => {
|
|
109
|
+
const adapterWithLatency = new InMemoryAdapter({ latencyMs: 50 });
|
|
110
|
+
const start = Date.now();
|
|
111
|
+
|
|
112
|
+
await adapterWithLatency.publish([
|
|
113
|
+
createMockEvent("MONITOR_START", "exec-1", 0),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const duration = Date.now() - start;
|
|
117
|
+
expect(duration).toBeGreaterThanOrEqual(50);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("failure simulation", () => {
|
|
122
|
+
it("should simulate failures", async () => {
|
|
123
|
+
const adapterWithFailures = new InMemoryAdapter({
|
|
124
|
+
failureProbability: 1.0, // Always fail
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await expect(
|
|
128
|
+
adapterWithFailures.publish([
|
|
129
|
+
createMockEvent("MONITOR_START", "exec-1", 0),
|
|
130
|
+
]),
|
|
131
|
+
).rejects.toThrow("Simulated publish failure");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should succeed when failure probability is 0", async () => {
|
|
135
|
+
const adapterNoFailures = new InMemoryAdapter({
|
|
136
|
+
failureProbability: 0.0,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await expect(
|
|
140
|
+
adapterNoFailures.publish([
|
|
141
|
+
createMockEvent("MONITOR_START", "exec-1", 0),
|
|
142
|
+
]),
|
|
143
|
+
).resolves.not.toThrow();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory event bus adapter for testing and local development.
|
|
3
|
+
*
|
|
4
|
+
* Stores events in memory and provides access for inspection.
|
|
5
|
+
* Useful for testing the DurableEventEmitter batching behavior without
|
|
6
|
+
* requiring actual infrastructure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DurableEventBusAdapter } from "../emitter.js";
|
|
10
|
+
import type { ExecutionEvent } from "../types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* In-memory adapter that stores events for inspection.
|
|
14
|
+
*
|
|
15
|
+
* Primarily useful for testing and development.
|
|
16
|
+
* Can simulate failures and latency.
|
|
17
|
+
*/
|
|
18
|
+
export class InMemoryAdapter implements DurableEventBusAdapter {
|
|
19
|
+
private readonly events: ExecutionEvent[] = [];
|
|
20
|
+
private publishCount = 0;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private options: {
|
|
24
|
+
/** Simulate latency in milliseconds */
|
|
25
|
+
latencyMs?: number;
|
|
26
|
+
/** Probability of failure (0-1) for testing error handling */
|
|
27
|
+
failureProbability?: number;
|
|
28
|
+
} = {},
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
async publish(events: ExecutionEvent[]): Promise<void> {
|
|
32
|
+
// Simulate latency
|
|
33
|
+
if (this.options.latencyMs) {
|
|
34
|
+
await this.sleep(this.options.latencyMs);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Simulate failures
|
|
38
|
+
if (
|
|
39
|
+
this.options.failureProbability &&
|
|
40
|
+
Math.random() < this.options.failureProbability
|
|
41
|
+
) {
|
|
42
|
+
throw new Error("Simulated publish failure");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.events.push(...events);
|
|
46
|
+
this.publishCount++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all published events.
|
|
51
|
+
*/
|
|
52
|
+
getEvents(): ExecutionEvent[] {
|
|
53
|
+
return [...this.events];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get events by type.
|
|
58
|
+
*/
|
|
59
|
+
getEventsByType<T extends ExecutionEvent["type"]>(
|
|
60
|
+
type: T,
|
|
61
|
+
): Array<Extract<ExecutionEvent, { type: T }>> {
|
|
62
|
+
return this.events.filter((e) => e.type === type) as Array<
|
|
63
|
+
Extract<ExecutionEvent, { type: T }>
|
|
64
|
+
>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get events for a specific execution.
|
|
69
|
+
*/
|
|
70
|
+
getEventsForExecution(executionId: string): ExecutionEvent[] {
|
|
71
|
+
return this.events.filter((e) => e.execution_id === executionId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the number of times publish() was called.
|
|
76
|
+
* Useful for verifying batching behavior.
|
|
77
|
+
*/
|
|
78
|
+
getPublishCount(): number {
|
|
79
|
+
return this.publishCount;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clear all stored events.
|
|
84
|
+
*/
|
|
85
|
+
clear(): void {
|
|
86
|
+
this.events.length = 0;
|
|
87
|
+
this.publishCount = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private sleep(ms: number): Promise<void> {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event bus adapters for durable event publishing.
|
|
3
|
+
*
|
|
4
|
+
* These adapters implement the DurableEventBusAdapter interface
|
|
5
|
+
* and can be used with the DurableEventEmitter for batched event publishing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from "./kinesis.js";
|
|
9
|
+
export * from "./in-memory.js";
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { KinesisAdapter } from "./kinesis.js";
|
|
3
|
+
import type { ExecutionEvent } from "../types.js";
|
|
4
|
+
import type { KinesisClient } from "@aws-sdk/client-kinesis";
|
|
5
|
+
|
|
6
|
+
describe("KinesisAdapter", () => {
|
|
7
|
+
const createMockEvent = (
|
|
8
|
+
type: ExecutionEvent["type"],
|
|
9
|
+
executionId: string,
|
|
10
|
+
organizationId: string,
|
|
11
|
+
seq: number,
|
|
12
|
+
): ExecutionEvent =>
|
|
13
|
+
({
|
|
14
|
+
type,
|
|
15
|
+
event_id: `event-${seq}`,
|
|
16
|
+
seq,
|
|
17
|
+
timestamp: Date.now(),
|
|
18
|
+
monitor_id: "monitor-1",
|
|
19
|
+
execution_id: executionId,
|
|
20
|
+
organization_id: organizationId,
|
|
21
|
+
monitor_name: "Test Monitor",
|
|
22
|
+
monitor_version: "1.0.0",
|
|
23
|
+
node_count: 1,
|
|
24
|
+
edge_count: 1,
|
|
25
|
+
}) as ExecutionEvent;
|
|
26
|
+
|
|
27
|
+
describe("publish", () => {
|
|
28
|
+
it("should publish events successfully", async () => {
|
|
29
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
30
|
+
FailedRecordCount: 0,
|
|
31
|
+
Records: [{ SequenceNumber: "seq-1", ShardId: "shard-1" }],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const mockClient = {
|
|
35
|
+
send: mockSend,
|
|
36
|
+
} as unknown as KinesisClient;
|
|
37
|
+
|
|
38
|
+
const adapter = new KinesisAdapter({
|
|
39
|
+
client: mockClient,
|
|
40
|
+
streamName: "test-stream",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const events = [createMockEvent("MONITOR_START", "exec-1", "org-1", 0)];
|
|
44
|
+
|
|
45
|
+
await adapter.publish(events);
|
|
46
|
+
|
|
47
|
+
expect(mockSend).toHaveBeenCalledOnce();
|
|
48
|
+
const command = mockSend.mock.calls[0][0];
|
|
49
|
+
expect(command.input.StreamName).toBe("test-stream");
|
|
50
|
+
expect(command.input.Records).toHaveLength(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should handle empty event array", async () => {
|
|
54
|
+
const mockSend = vi.fn();
|
|
55
|
+
const mockClient = {
|
|
56
|
+
send: mockSend,
|
|
57
|
+
} as unknown as KinesisClient;
|
|
58
|
+
|
|
59
|
+
const adapter = new KinesisAdapter({
|
|
60
|
+
client: mockClient,
|
|
61
|
+
streamName: "test-stream",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await adapter.publish([]);
|
|
65
|
+
|
|
66
|
+
expect(mockSend).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should batch events into chunks of 500", async () => {
|
|
70
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
71
|
+
FailedRecordCount: 0,
|
|
72
|
+
Records: [],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const mockClient = {
|
|
76
|
+
send: mockSend,
|
|
77
|
+
} as unknown as KinesisClient;
|
|
78
|
+
|
|
79
|
+
const adapter = new KinesisAdapter({
|
|
80
|
+
client: mockClient,
|
|
81
|
+
streamName: "test-stream",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Create 1250 events (should result in 3 batches: 500, 500, 250)
|
|
85
|
+
const events = Array.from({ length: 1250 }, (_, i) =>
|
|
86
|
+
createMockEvent("MONITOR_START", "exec-1", "org-1", i),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
await adapter.publish(events);
|
|
90
|
+
|
|
91
|
+
expect(mockSend).toHaveBeenCalledTimes(3);
|
|
92
|
+
expect(mockSend.mock.calls[0][0].input.Records).toHaveLength(500);
|
|
93
|
+
expect(mockSend.mock.calls[1][0].input.Records).toHaveLength(500);
|
|
94
|
+
expect(mockSend.mock.calls[2][0].input.Records).toHaveLength(250);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("partition key strategies", () => {
|
|
99
|
+
it("should use executionId as partition key by default", async () => {
|
|
100
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
101
|
+
FailedRecordCount: 0,
|
|
102
|
+
Records: [],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const mockClient = {
|
|
106
|
+
send: mockSend,
|
|
107
|
+
} as unknown as KinesisClient;
|
|
108
|
+
|
|
109
|
+
const adapter = new KinesisAdapter({
|
|
110
|
+
client: mockClient,
|
|
111
|
+
streamName: "test-stream",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const events = [
|
|
115
|
+
createMockEvent("MONITOR_START", "exec-123", "org-456", 0),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
await adapter.publish(events);
|
|
119
|
+
|
|
120
|
+
const records = mockSend.mock.calls[0][0].input.Records;
|
|
121
|
+
expect(records[0].PartitionKey).toBe("exec-123");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should use organizationId as partition key when configured", async () => {
|
|
125
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
126
|
+
FailedRecordCount: 0,
|
|
127
|
+
Records: [],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const mockClient = {
|
|
131
|
+
send: mockSend,
|
|
132
|
+
} as unknown as KinesisClient;
|
|
133
|
+
|
|
134
|
+
const adapter = new KinesisAdapter({
|
|
135
|
+
client: mockClient,
|
|
136
|
+
streamName: "test-stream",
|
|
137
|
+
partitionKeyStrategy: "organizationId",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const events = [
|
|
141
|
+
createMockEvent("MONITOR_START", "exec-123", "org-456", 0),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
await adapter.publish(events);
|
|
145
|
+
|
|
146
|
+
const records = mockSend.mock.calls[0][0].input.Records;
|
|
147
|
+
expect(records[0].PartitionKey).toBe("org-456");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should use composite partition key when configured", async () => {
|
|
151
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
152
|
+
FailedRecordCount: 0,
|
|
153
|
+
Records: [],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const mockClient = {
|
|
157
|
+
send: mockSend,
|
|
158
|
+
} as unknown as KinesisClient;
|
|
159
|
+
|
|
160
|
+
const adapter = new KinesisAdapter({
|
|
161
|
+
client: mockClient,
|
|
162
|
+
streamName: "test-stream",
|
|
163
|
+
partitionKeyStrategy: "composite",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const events = [
|
|
167
|
+
createMockEvent("MONITOR_START", "exec-123", "org-456", 0),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
await adapter.publish(events);
|
|
171
|
+
|
|
172
|
+
const records = mockSend.mock.calls[0][0].input.Records;
|
|
173
|
+
expect(records[0].PartitionKey).toBe("org-456:exec-123");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("retry logic", () => {
|
|
178
|
+
it("should retry failed records with exponential backoff", async () => {
|
|
179
|
+
let callCount = 0;
|
|
180
|
+
const mockSend = vi.fn().mockImplementation(() => {
|
|
181
|
+
callCount++;
|
|
182
|
+
if (callCount === 1) {
|
|
183
|
+
// First call: one record fails
|
|
184
|
+
return Promise.resolve({
|
|
185
|
+
FailedRecordCount: 1,
|
|
186
|
+
Records: [{ ErrorCode: "ProvisionedThroughputExceededException" }],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Second call: success
|
|
190
|
+
return Promise.resolve({
|
|
191
|
+
FailedRecordCount: 0,
|
|
192
|
+
Records: [{ SequenceNumber: "seq-1", ShardId: "shard-1" }],
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const mockClient = {
|
|
197
|
+
send: mockSend,
|
|
198
|
+
} as unknown as KinesisClient;
|
|
199
|
+
|
|
200
|
+
const adapter = new KinesisAdapter({
|
|
201
|
+
client: mockClient,
|
|
202
|
+
streamName: "test-stream",
|
|
203
|
+
maxRetries: 3,
|
|
204
|
+
retryDelayMs: 10, // Short delay for testing
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const events = [createMockEvent("MONITOR_START", "exec-1", "org-1", 0)];
|
|
208
|
+
|
|
209
|
+
await adapter.publish(events);
|
|
210
|
+
|
|
211
|
+
expect(mockSend).toHaveBeenCalledTimes(2); // Initial + 1 retry
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should stop retrying after max attempts", async () => {
|
|
215
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
216
|
+
FailedRecordCount: 1,
|
|
217
|
+
Records: [{ ErrorCode: "ProvisionedThroughputExceededException" }],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const mockClient = {
|
|
221
|
+
send: mockSend,
|
|
222
|
+
} as unknown as KinesisClient;
|
|
223
|
+
|
|
224
|
+
const adapter = new KinesisAdapter({
|
|
225
|
+
client: mockClient,
|
|
226
|
+
streamName: "test-stream",
|
|
227
|
+
maxRetries: 2,
|
|
228
|
+
retryDelayMs: 10,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const events = [createMockEvent("MONITOR_START", "exec-1", "org-1", 0)];
|
|
232
|
+
|
|
233
|
+
// Should not throw, but log errors
|
|
234
|
+
await adapter.publish(events);
|
|
235
|
+
|
|
236
|
+
expect(mockSend).toHaveBeenCalledTimes(3); // Initial + 2 retries
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should retry on client errors", async () => {
|
|
240
|
+
let callCount = 0;
|
|
241
|
+
const mockSend = vi.fn().mockImplementation(() => {
|
|
242
|
+
callCount++;
|
|
243
|
+
if (callCount === 1) {
|
|
244
|
+
return Promise.reject(new Error("Network error"));
|
|
245
|
+
}
|
|
246
|
+
return Promise.resolve({
|
|
247
|
+
FailedRecordCount: 0,
|
|
248
|
+
Records: [{ SequenceNumber: "seq-1", ShardId: "shard-1" }],
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const mockClient = {
|
|
253
|
+
send: mockSend,
|
|
254
|
+
} as unknown as KinesisClient;
|
|
255
|
+
|
|
256
|
+
const adapter = new KinesisAdapter({
|
|
257
|
+
client: mockClient,
|
|
258
|
+
streamName: "test-stream",
|
|
259
|
+
maxRetries: 3,
|
|
260
|
+
retryDelayMs: 10,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const events = [createMockEvent("MONITOR_START", "exec-1", "org-1", 0)];
|
|
264
|
+
|
|
265
|
+
await adapter.publish(events);
|
|
266
|
+
|
|
267
|
+
expect(mockSend).toHaveBeenCalledTimes(2); // Initial + 1 retry
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should throw after max retries on client errors", async () => {
|
|
271
|
+
const mockSend = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
272
|
+
|
|
273
|
+
const mockClient = {
|
|
274
|
+
send: mockSend,
|
|
275
|
+
} as unknown as KinesisClient;
|
|
276
|
+
|
|
277
|
+
const adapter = new KinesisAdapter({
|
|
278
|
+
client: mockClient,
|
|
279
|
+
streamName: "test-stream",
|
|
280
|
+
maxRetries: 2,
|
|
281
|
+
retryDelayMs: 10,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const events = [createMockEvent("MONITOR_START", "exec-1", "org-1", 0)];
|
|
285
|
+
|
|
286
|
+
await expect(adapter.publish(events)).rejects.toThrow("Network error");
|
|
287
|
+
expect(mockSend).toHaveBeenCalledTimes(3); // Initial + 2 retries
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("event serialization", () => {
|
|
292
|
+
it("should serialize events as JSON", async () => {
|
|
293
|
+
const mockSend = vi.fn().mockResolvedValue({
|
|
294
|
+
FailedRecordCount: 0,
|
|
295
|
+
Records: [],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const mockClient = {
|
|
299
|
+
send: mockSend,
|
|
300
|
+
} as unknown as KinesisClient;
|
|
301
|
+
|
|
302
|
+
const adapter = new KinesisAdapter({
|
|
303
|
+
client: mockClient,
|
|
304
|
+
streamName: "test-stream",
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const event = createMockEvent("MONITOR_START", "exec-1", "org-1", 0);
|
|
308
|
+
await adapter.publish([event]);
|
|
309
|
+
|
|
310
|
+
const records = mockSend.mock.calls[0][0].input.Records;
|
|
311
|
+
const data = records[0].Data;
|
|
312
|
+
|
|
313
|
+
// Decode the Uint8Array back to JSON
|
|
314
|
+
const decoder = new TextDecoder();
|
|
315
|
+
const json = decoder.decode(data);
|
|
316
|
+
const parsed = JSON.parse(json);
|
|
317
|
+
|
|
318
|
+
expect(parsed.type).toBe("MONITOR_START");
|
|
319
|
+
expect(parsed.execution_id).toBe("exec-1");
|
|
320
|
+
expect(parsed.organization_id).toBe("org-1");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|