@q1k-oss/btree-workflows 0.0.1
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/.claude/settings.local.json +31 -0
- package/CLAUDE.md +181 -0
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/behaviour-tree-workflows-landing/index.html +16 -0
- package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
- package/behaviour-tree-workflows-landing/package.json +31 -0
- package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
- package/behaviour-tree-workflows-landing/src/App.css +103 -0
- package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
- package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
- package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
- package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
- package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
- package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
- package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
- package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
- package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
- package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
- package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
- package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
- package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
- package/demo-google-sheets.ts +181 -0
- package/demo-runtime-variables.ts +174 -0
- package/demo-template.ts +208 -0
- package/docs/ARCHITECTURE_SUMMARY.md +613 -0
- package/docs/NODE_REFERENCE.md +504 -0
- package/docs/README.md +53 -0
- package/docs/custom-nodes-architecture.md +826 -0
- package/docs/observability.md +175 -0
- package/docs/yaml-specification.md +990 -0
- package/examples/temporal/README.md +117 -0
- package/examples/temporal/activities.ts +373 -0
- package/examples/temporal/client.ts +115 -0
- package/examples/temporal/python-worker/activities.py +339 -0
- package/examples/temporal/python-worker/requirements.txt +12 -0
- package/examples/temporal/python-worker/worker.py +106 -0
- package/examples/temporal/worker.ts +66 -0
- package/examples/temporal/workflows.ts +6 -0
- package/examples/temporal/yaml-workflow-loader.ts +105 -0
- package/examples/yaml-test.ts +97 -0
- package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
- package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
- package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
- package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
- package/examples/yaml-workflows/05-order-processing.yaml +146 -0
- package/examples/yaml-workflows/06-activity-test.yaml +71 -0
- package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
- package/examples/yaml-workflows/08-file-processing.yaml +141 -0
- package/examples/yaml-workflows/09-http-request.yaml +137 -0
- package/examples/yaml-workflows/README.md +211 -0
- package/package.json +38 -0
- package/src/actions/code-execution.schema.ts +27 -0
- package/src/actions/code-execution.ts +218 -0
- package/src/actions/generate-file.test.ts +516 -0
- package/src/actions/generate-file.ts +166 -0
- package/src/actions/http-request.test.ts +784 -0
- package/src/actions/http-request.ts +228 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/parse-file.test.ts +448 -0
- package/src/actions/parse-file.ts +139 -0
- package/src/actions/python-script.test.ts +439 -0
- package/src/actions/python-script.ts +154 -0
- package/src/base-node.test.ts +511 -0
- package/src/base-node.ts +605 -0
- package/src/behavior-tree.test.ts +431 -0
- package/src/behavior-tree.ts +283 -0
- package/src/blackboard.test.ts +222 -0
- package/src/blackboard.ts +192 -0
- package/src/composites/conditional.schema.ts +19 -0
- package/src/composites/conditional.test.ts +309 -0
- package/src/composites/conditional.ts +129 -0
- package/src/composites/for-each.schema.ts +23 -0
- package/src/composites/for-each.test.ts +254 -0
- package/src/composites/for-each.ts +132 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/memory-sequence.schema.ts +19 -0
- package/src/composites/memory-sequence.test.ts +223 -0
- package/src/composites/memory-sequence.ts +98 -0
- package/src/composites/parallel.schema.ts +28 -0
- package/src/composites/parallel.test.ts +502 -0
- package/src/composites/parallel.ts +157 -0
- package/src/composites/reactive-sequence.schema.ts +19 -0
- package/src/composites/reactive-sequence.test.ts +170 -0
- package/src/composites/reactive-sequence.ts +85 -0
- package/src/composites/recovery.schema.ts +19 -0
- package/src/composites/recovery.test.ts +366 -0
- package/src/composites/recovery.ts +90 -0
- package/src/composites/selector.schema.ts +19 -0
- package/src/composites/selector.test.ts +387 -0
- package/src/composites/selector.ts +85 -0
- package/src/composites/sequence.schema.ts +19 -0
- package/src/composites/sequence.test.ts +337 -0
- package/src/composites/sequence.ts +72 -0
- package/src/composites/sub-tree.schema.ts +21 -0
- package/src/composites/sub-tree.test.ts +893 -0
- package/src/composites/sub-tree.ts +177 -0
- package/src/composites/while.schema.ts +24 -0
- package/src/composites/while.test.ts +381 -0
- package/src/composites/while.ts +149 -0
- package/src/data-store/index.ts +10 -0
- package/src/data-store/memory-store.ts +161 -0
- package/src/data-store/types.ts +94 -0
- package/src/debug/breakpoint.test.ts +47 -0
- package/src/debug/breakpoint.ts +30 -0
- package/src/debug/index.ts +17 -0
- package/src/debug/resume-point.test.ts +49 -0
- package/src/debug/resume-point.ts +29 -0
- package/src/decorators/delay.schema.ts +21 -0
- package/src/decorators/delay.test.ts +261 -0
- package/src/decorators/delay.ts +140 -0
- package/src/decorators/force-result.schema.ts +32 -0
- package/src/decorators/force-result.test.ts +133 -0
- package/src/decorators/force-result.ts +63 -0
- package/src/decorators/index.ts +13 -0
- package/src/decorators/invert.schema.ts +19 -0
- package/src/decorators/invert.test.ts +135 -0
- package/src/decorators/invert.ts +42 -0
- package/src/decorators/keep-running.schema.ts +20 -0
- package/src/decorators/keep-running.test.ts +105 -0
- package/src/decorators/keep-running.ts +49 -0
- package/src/decorators/precondition.schema.ts +19 -0
- package/src/decorators/precondition.test.ts +351 -0
- package/src/decorators/precondition.ts +139 -0
- package/src/decorators/repeat.schema.ts +21 -0
- package/src/decorators/repeat.test.ts +187 -0
- package/src/decorators/repeat.ts +94 -0
- package/src/decorators/run-once.schema.ts +19 -0
- package/src/decorators/run-once.test.ts +140 -0
- package/src/decorators/run-once.ts +61 -0
- package/src/decorators/soft-assert.schema.ts +19 -0
- package/src/decorators/soft-assert.test.ts +107 -0
- package/src/decorators/soft-assert.ts +68 -0
- package/src/decorators/timeout.schema.ts +21 -0
- package/src/decorators/timeout.test.ts +274 -0
- package/src/decorators/timeout.ts +159 -0
- package/src/errors.test.ts +63 -0
- package/src/errors.ts +34 -0
- package/src/events.test.ts +347 -0
- package/src/events.ts +183 -0
- package/src/index.ts +80 -0
- package/src/integrations/index.ts +30 -0
- package/src/integrations/integration-action.test.ts +571 -0
- package/src/integrations/integration-action.ts +233 -0
- package/src/integrations/piece-executor.ts +320 -0
- package/src/observability/execution-tracker.ts +320 -0
- package/src/observability/index.ts +23 -0
- package/src/observability/sinks.ts +138 -0
- package/src/observability/types.ts +130 -0
- package/src/registry-utils.ts +147 -0
- package/src/registry.test.ts +466 -0
- package/src/registry.ts +334 -0
- package/src/schemas/base.schema.ts +104 -0
- package/src/schemas/index.ts +223 -0
- package/src/schemas/integration.test.ts +238 -0
- package/src/schemas/tree-definition.schema.ts +170 -0
- package/src/schemas/validation.test.ts +146 -0
- package/src/schemas/validation.ts +122 -0
- package/src/scripting/index.ts +22 -0
- package/src/templates/template-loader.test.ts +281 -0
- package/src/templates/template-loader.ts +152 -0
- package/src/temporal-integration.test.ts +213 -0
- package/src/test-nodes.ts +259 -0
- package/src/types.ts +503 -0
- package/src/utilities/index.ts +17 -0
- package/src/utilities/log-message.test.ts +275 -0
- package/src/utilities/log-message.ts +134 -0
- package/src/utilities/regex-extract.test.ts +138 -0
- package/src/utilities/regex-extract.ts +108 -0
- package/src/utilities/variable-resolver.test.ts +416 -0
- package/src/utilities/variable-resolver.ts +318 -0
- package/src/utils/error-handler.test.ts +117 -0
- package/src/utils/error-handler.ts +48 -0
- package/src/utils/signal-check.test.ts +234 -0
- package/src/utils/signal-check.ts +140 -0
- package/src/yaml/errors.ts +143 -0
- package/src/yaml/index.ts +30 -0
- package/src/yaml/loader.ts +39 -0
- package/src/yaml/parser.ts +286 -0
- package/src/yaml/validation/semantic-validator.ts +196 -0
- package/templates/google-sheets/insert-row.yaml +76 -0
- package/templates/notification-sender.yaml +33 -0
- package/templates/order-validation.yaml +44 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +25 -0
- package/workflows/order-processor.yaml +59 -0
- package/workflows/process-order-workflow.yaml +142 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IntegrationAction Node Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { ScopedBlackboard } from "../blackboard.js";
|
|
7
|
+
import { Registry } from "../registry.js";
|
|
8
|
+
import { type TemporalContext, NodeStatus } from "../types.js";
|
|
9
|
+
import {
|
|
10
|
+
IntegrationAction,
|
|
11
|
+
type IntegrationActionConfig,
|
|
12
|
+
type IntegrationContext,
|
|
13
|
+
type TokenProvider,
|
|
14
|
+
envTokenProvider,
|
|
15
|
+
} from "./integration-action.js";
|
|
16
|
+
import type { PieceAuth } from "./piece-executor.js";
|
|
17
|
+
|
|
18
|
+
describe("IntegrationAction Node", () => {
|
|
19
|
+
let blackboard: ScopedBlackboard;
|
|
20
|
+
let registry: Registry;
|
|
21
|
+
let context: IntegrationContext;
|
|
22
|
+
|
|
23
|
+
// Mock token provider that returns a test token
|
|
24
|
+
const mockTokenProvider: TokenProvider = vi.fn(async () => ({
|
|
25
|
+
access_token: "test_token_123",
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
blackboard = new ScopedBlackboard();
|
|
30
|
+
registry = new Registry();
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
|
|
33
|
+
context = {
|
|
34
|
+
blackboard,
|
|
35
|
+
treeRegistry: registry,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
deltaTime: 0,
|
|
38
|
+
tokenProvider: mockTokenProvider,
|
|
39
|
+
tenantId: "tenant-123",
|
|
40
|
+
userId: "user-456",
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("Construction and validation", () => {
|
|
45
|
+
it("should create node with valid config", () => {
|
|
46
|
+
const node = new IntegrationAction({
|
|
47
|
+
id: "test-action",
|
|
48
|
+
provider: "google-sheets",
|
|
49
|
+
action: "append_row",
|
|
50
|
+
inputs: { spreadsheetId: "123" },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(node).toBeDefined();
|
|
54
|
+
expect(node.id).toBe("test-action");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should require provider", () => {
|
|
58
|
+
expect(() => {
|
|
59
|
+
new IntegrationAction({
|
|
60
|
+
id: "test",
|
|
61
|
+
action: "test",
|
|
62
|
+
} as unknown as IntegrationActionConfig);
|
|
63
|
+
}).toThrow(/requires provider/i);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should require action", () => {
|
|
67
|
+
expect(() => {
|
|
68
|
+
new IntegrationAction({
|
|
69
|
+
id: "test",
|
|
70
|
+
provider: "google-sheets",
|
|
71
|
+
} as unknown as IntegrationActionConfig);
|
|
72
|
+
}).toThrow(/requires action/i);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should accept empty inputs", () => {
|
|
76
|
+
const node = new IntegrationAction({
|
|
77
|
+
id: "test",
|
|
78
|
+
provider: "slack",
|
|
79
|
+
action: "send_message",
|
|
80
|
+
});
|
|
81
|
+
expect(node).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("Blackboard variable resolution", () => {
|
|
86
|
+
it("should resolve simple ${bb.key} references", () => {
|
|
87
|
+
blackboard.set("spreadsheetId", "sheet-123");
|
|
88
|
+
blackboard.set("sheetName", "Orders");
|
|
89
|
+
|
|
90
|
+
const node = new IntegrationAction({
|
|
91
|
+
id: "test",
|
|
92
|
+
provider: "google-sheets",
|
|
93
|
+
action: "append_row",
|
|
94
|
+
inputs: {
|
|
95
|
+
spreadsheetId: "${bb.spreadsheetId}",
|
|
96
|
+
sheetName: "${bb.sheetName}",
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Access private method via any
|
|
101
|
+
const resolvedInputs = (node as any).resolveInputs(context);
|
|
102
|
+
|
|
103
|
+
expect(resolvedInputs.spreadsheetId).toBe("sheet-123");
|
|
104
|
+
expect(resolvedInputs.sheetName).toBe("Orders");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should resolve nested ${bb.nested.key} references", () => {
|
|
108
|
+
blackboard.set("user", {
|
|
109
|
+
profile: {
|
|
110
|
+
name: "John Doe",
|
|
111
|
+
email: "john@example.com",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const node = new IntegrationAction({
|
|
116
|
+
id: "test",
|
|
117
|
+
provider: "slack",
|
|
118
|
+
action: "send_message",
|
|
119
|
+
inputs: {
|
|
120
|
+
text: "${bb.user.profile.name}",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const resolvedInputs = (node as any).resolveInputs(context);
|
|
125
|
+
|
|
126
|
+
expect(resolvedInputs.text).toBe("John Doe");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should handle template interpolation", () => {
|
|
130
|
+
blackboard.set("orderId", "ORD-123");
|
|
131
|
+
blackboard.set("customerName", "Jane");
|
|
132
|
+
|
|
133
|
+
const node = new IntegrationAction({
|
|
134
|
+
id: "test",
|
|
135
|
+
provider: "slack",
|
|
136
|
+
action: "send_message",
|
|
137
|
+
inputs: {
|
|
138
|
+
text: "New order ${bb.orderId} from ${bb.customerName}!",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const resolvedInputs = (node as any).resolveInputs(context);
|
|
143
|
+
|
|
144
|
+
expect(resolvedInputs.text).toBe("New order ORD-123 from Jane!");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should resolve arrays with ${bb.key} references", () => {
|
|
148
|
+
blackboard.set("col1", "A");
|
|
149
|
+
blackboard.set("col2", "B");
|
|
150
|
+
blackboard.set("col3", "C");
|
|
151
|
+
|
|
152
|
+
const node = new IntegrationAction({
|
|
153
|
+
id: "test",
|
|
154
|
+
provider: "google-sheets",
|
|
155
|
+
action: "append_row",
|
|
156
|
+
inputs: {
|
|
157
|
+
values: ["${bb.col1}", "${bb.col2}", "${bb.col3}"],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const resolvedInputs = (node as any).resolveInputs(context);
|
|
162
|
+
|
|
163
|
+
expect(resolvedInputs.values).toEqual(["A", "B", "C"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should preserve non-string values", () => {
|
|
167
|
+
blackboard.set("count", 42);
|
|
168
|
+
blackboard.set("active", true);
|
|
169
|
+
|
|
170
|
+
const node = new IntegrationAction({
|
|
171
|
+
id: "test",
|
|
172
|
+
provider: "test",
|
|
173
|
+
action: "test",
|
|
174
|
+
inputs: {
|
|
175
|
+
// Full reference returns the actual value
|
|
176
|
+
numericValue: "${bb.count}",
|
|
177
|
+
boolValue: "${bb.active}",
|
|
178
|
+
// Static values
|
|
179
|
+
staticNum: 100,
|
|
180
|
+
staticBool: false,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const resolvedInputs = (node as any).resolveInputs(context);
|
|
185
|
+
|
|
186
|
+
expect(resolvedInputs.numericValue).toBe(42);
|
|
187
|
+
expect(resolvedInputs.boolValue).toBe(true);
|
|
188
|
+
expect(resolvedInputs.staticNum).toBe(100);
|
|
189
|
+
expect(resolvedInputs.staticBool).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should return empty string for undefined references in template", () => {
|
|
193
|
+
const node = new IntegrationAction({
|
|
194
|
+
id: "test",
|
|
195
|
+
provider: "test",
|
|
196
|
+
action: "test",
|
|
197
|
+
inputs: {
|
|
198
|
+
text: "Hello ${bb.undefinedKey}!",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const resolvedInputs = (node as any).resolveInputs(context);
|
|
203
|
+
|
|
204
|
+
expect(resolvedInputs.text).toBe("Hello !");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should return undefined for full undefined reference", () => {
|
|
208
|
+
const node = new IntegrationAction({
|
|
209
|
+
id: "test",
|
|
210
|
+
provider: "test",
|
|
211
|
+
action: "test",
|
|
212
|
+
inputs: {
|
|
213
|
+
value: "${bb.undefinedKey}",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const resolvedInputs = (node as any).resolveInputs(context);
|
|
218
|
+
|
|
219
|
+
expect(resolvedInputs.value).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Token provider integration", () => {
|
|
224
|
+
it("should fail without token provider in context", async () => {
|
|
225
|
+
const contextWithoutProvider: TemporalContext = {
|
|
226
|
+
blackboard,
|
|
227
|
+
treeRegistry: registry,
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
deltaTime: 0,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const node = new IntegrationAction({
|
|
233
|
+
id: "test",
|
|
234
|
+
provider: "google-sheets",
|
|
235
|
+
action: "append_row",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const status = await node.tick(contextWithoutProvider);
|
|
239
|
+
|
|
240
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
241
|
+
expect(node.lastError).toContain("No token provider");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should call token provider with correct arguments", async () => {
|
|
245
|
+
// Mock the piece execution to avoid actual API calls
|
|
246
|
+
vi.mock("./piece-executor.js", () => ({
|
|
247
|
+
executePieceAction: vi.fn().mockResolvedValue({ success: true }),
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
const node = new IntegrationAction({
|
|
251
|
+
id: "test",
|
|
252
|
+
provider: "slack",
|
|
253
|
+
action: "send_message",
|
|
254
|
+
connectionId: "conn-789",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// We expect failure due to piece not being installed, but token provider should be called
|
|
258
|
+
await node.tick(context);
|
|
259
|
+
|
|
260
|
+
expect(mockTokenProvider).toHaveBeenCalledWith(
|
|
261
|
+
context,
|
|
262
|
+
"slack",
|
|
263
|
+
"conn-789"
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("Result storage", () => {
|
|
269
|
+
it("should store result in default key by default", async () => {
|
|
270
|
+
// This test verifies the storeResult logic
|
|
271
|
+
const node = new IntegrationAction({
|
|
272
|
+
id: "my-action",
|
|
273
|
+
provider: "test",
|
|
274
|
+
action: "test",
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Verify default resultKey
|
|
278
|
+
expect((node as any).resultKey).toBe("my-action.result");
|
|
279
|
+
expect((node as any).storeResult).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should use custom result key when specified", () => {
|
|
283
|
+
const node = new IntegrationAction({
|
|
284
|
+
id: "test",
|
|
285
|
+
provider: "test",
|
|
286
|
+
action: "test",
|
|
287
|
+
resultKey: "custom.result.key",
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect((node as any).resultKey).toBe("custom.result.key");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("should not store result when storeResult is false", () => {
|
|
294
|
+
const node = new IntegrationAction({
|
|
295
|
+
id: "test",
|
|
296
|
+
provider: "test",
|
|
297
|
+
action: "test",
|
|
298
|
+
storeResult: false,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect((node as any).storeResult).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("Node lifecycle", () => {
|
|
306
|
+
it("should clone correctly", () => {
|
|
307
|
+
const node = new IntegrationAction({
|
|
308
|
+
id: "original",
|
|
309
|
+
provider: "google-sheets",
|
|
310
|
+
action: "append_row",
|
|
311
|
+
inputs: { test: "value" },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const cloned = node.clone() as IntegrationAction;
|
|
315
|
+
|
|
316
|
+
expect(cloned.id).toBe("original");
|
|
317
|
+
expect((cloned as any).provider).toBe("google-sheets");
|
|
318
|
+
expect((cloned as any).action).toBe("append_row");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should reset status correctly", async () => {
|
|
322
|
+
const node = new IntegrationAction({
|
|
323
|
+
id: "test",
|
|
324
|
+
provider: "test",
|
|
325
|
+
action: "test",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Trigger failure (no token provider)
|
|
329
|
+
await node.tick({
|
|
330
|
+
blackboard,
|
|
331
|
+
treeRegistry: registry,
|
|
332
|
+
timestamp: Date.now(),
|
|
333
|
+
deltaTime: 0,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(node.status()).toBe(NodeStatus.FAILURE);
|
|
337
|
+
|
|
338
|
+
node.reset();
|
|
339
|
+
|
|
340
|
+
expect(node.status()).toBe(NodeStatus.IDLE);
|
|
341
|
+
expect(node.lastError).toBeUndefined();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("Activity execution mode", () => {
|
|
347
|
+
let blackboard: ScopedBlackboard;
|
|
348
|
+
let registry: Registry;
|
|
349
|
+
|
|
350
|
+
// Mock token provider that returns a test token
|
|
351
|
+
const mockTokenProvider: TokenProvider = vi.fn(async () => ({
|
|
352
|
+
access_token: "test_token_123",
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
beforeEach(() => {
|
|
356
|
+
blackboard = new ScopedBlackboard();
|
|
357
|
+
registry = new Registry();
|
|
358
|
+
vi.clearAllMocks();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should use activity when provided in context", async () => {
|
|
362
|
+
const mockActivity = vi.fn().mockResolvedValue({ success: true });
|
|
363
|
+
|
|
364
|
+
const context: IntegrationContext = {
|
|
365
|
+
blackboard,
|
|
366
|
+
treeRegistry: registry,
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
deltaTime: 0,
|
|
369
|
+
tokenProvider: mockTokenProvider,
|
|
370
|
+
activities: { executePieceAction: mockActivity },
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const node = new IntegrationAction({
|
|
374
|
+
id: "test",
|
|
375
|
+
provider: "google-sheets",
|
|
376
|
+
action: "append_row",
|
|
377
|
+
inputs: { spreadsheetId: "123" },
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const status = await node.tick(context);
|
|
381
|
+
|
|
382
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
383
|
+
expect(mockActivity).toHaveBeenCalledWith(
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
provider: "google-sheets",
|
|
386
|
+
action: "append_row",
|
|
387
|
+
inputs: { spreadsheetId: "123" },
|
|
388
|
+
auth: { access_token: "test_token_123" },
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("should fall back to inline execution without activity", async () => {
|
|
394
|
+
// This test verifies that the node attempts inline execution when no activity is provided
|
|
395
|
+
// Due to vi.mock in an earlier test, executePieceAction is mocked globally
|
|
396
|
+
const context: IntegrationContext = {
|
|
397
|
+
blackboard,
|
|
398
|
+
treeRegistry: registry,
|
|
399
|
+
timestamp: Date.now(),
|
|
400
|
+
deltaTime: 0,
|
|
401
|
+
tokenProvider: mockTokenProvider,
|
|
402
|
+
activities: undefined, // No activities provided - should fall back to inline
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const node = new IntegrationAction({
|
|
406
|
+
id: "test",
|
|
407
|
+
provider: "google-sheets",
|
|
408
|
+
action: "append_row",
|
|
409
|
+
inputs: { spreadsheetId: "123" },
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// With the global mock, this will succeed via inline execution
|
|
413
|
+
// The important thing is it doesn't fail with "no activity" error
|
|
414
|
+
const status = await node.tick(context);
|
|
415
|
+
|
|
416
|
+
// Succeeds because vi.mock mocks executePieceAction
|
|
417
|
+
expect(status).toBe(NodeStatus.SUCCESS);
|
|
418
|
+
// No error about missing activities
|
|
419
|
+
expect(node.lastError).toBeUndefined();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("should handle activity execution errors", async () => {
|
|
423
|
+
const failingActivity = vi
|
|
424
|
+
.fn()
|
|
425
|
+
.mockRejectedValue(new Error("API timeout"));
|
|
426
|
+
|
|
427
|
+
const context: IntegrationContext = {
|
|
428
|
+
blackboard,
|
|
429
|
+
treeRegistry: registry,
|
|
430
|
+
timestamp: Date.now(),
|
|
431
|
+
deltaTime: 0,
|
|
432
|
+
tokenProvider: mockTokenProvider,
|
|
433
|
+
activities: { executePieceAction: failingActivity },
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const node = new IntegrationAction({
|
|
437
|
+
id: "test",
|
|
438
|
+
provider: "slack",
|
|
439
|
+
action: "send_message",
|
|
440
|
+
inputs: { channel: "#general", text: "Hello" },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const status = await node.tick(context);
|
|
444
|
+
|
|
445
|
+
expect(status).toBe(NodeStatus.FAILURE);
|
|
446
|
+
expect(node.lastError).toContain("API timeout");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should store activity result in blackboard", async () => {
|
|
450
|
+
const mockActivity = vi.fn().mockResolvedValue({
|
|
451
|
+
messageId: "msg-123",
|
|
452
|
+
timestamp: "2024-01-15T10:30:00Z"
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const context: IntegrationContext = {
|
|
456
|
+
blackboard,
|
|
457
|
+
treeRegistry: registry,
|
|
458
|
+
timestamp: Date.now(),
|
|
459
|
+
deltaTime: 0,
|
|
460
|
+
tokenProvider: mockTokenProvider,
|
|
461
|
+
activities: { executePieceAction: mockActivity },
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const node = new IntegrationAction({
|
|
465
|
+
id: "send-slack",
|
|
466
|
+
provider: "slack",
|
|
467
|
+
action: "send_message",
|
|
468
|
+
inputs: { channel: "#general", text: "Hello" },
|
|
469
|
+
resultKey: "slack.response",
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await node.tick(context);
|
|
473
|
+
|
|
474
|
+
expect(blackboard.get("slack.response")).toEqual({
|
|
475
|
+
messageId: "msg-123",
|
|
476
|
+
timestamp: "2024-01-15T10:30:00Z",
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("should pass resolved inputs to activity", async () => {
|
|
481
|
+
const mockActivity = vi.fn().mockResolvedValue({ success: true });
|
|
482
|
+
|
|
483
|
+
blackboard.set("targetChannel", "#engineering");
|
|
484
|
+
blackboard.set("userName", "Alice");
|
|
485
|
+
|
|
486
|
+
const context: IntegrationContext = {
|
|
487
|
+
blackboard,
|
|
488
|
+
treeRegistry: registry,
|
|
489
|
+
timestamp: Date.now(),
|
|
490
|
+
deltaTime: 0,
|
|
491
|
+
input: { messagePrefix: "Notification:" },
|
|
492
|
+
tokenProvider: mockTokenProvider,
|
|
493
|
+
activities: { executePieceAction: mockActivity },
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const node = new IntegrationAction({
|
|
497
|
+
id: "notify",
|
|
498
|
+
provider: "slack",
|
|
499
|
+
action: "send_message",
|
|
500
|
+
inputs: {
|
|
501
|
+
channel: "${bb.targetChannel}",
|
|
502
|
+
text: "${input.messagePrefix} Hello ${bb.userName}!",
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
await node.tick(context);
|
|
507
|
+
|
|
508
|
+
expect(mockActivity).toHaveBeenCalledWith(
|
|
509
|
+
expect.objectContaining({
|
|
510
|
+
inputs: {
|
|
511
|
+
channel: "#engineering",
|
|
512
|
+
text: "Notification: Hello Alice!",
|
|
513
|
+
},
|
|
514
|
+
})
|
|
515
|
+
);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe("envTokenProvider", () => {
|
|
520
|
+
const originalEnv = process.env;
|
|
521
|
+
|
|
522
|
+
beforeEach(() => {
|
|
523
|
+
process.env = { ...originalEnv };
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
afterAll(() => {
|
|
527
|
+
process.env = originalEnv;
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should return access_token from environment", async () => {
|
|
531
|
+
process.env.GOOGLE_SHEETS_ACCESS_TOKEN = "test_access_token";
|
|
532
|
+
|
|
533
|
+
const auth = await envTokenProvider({} as any, "google-sheets");
|
|
534
|
+
|
|
535
|
+
expect(auth).toEqual({ access_token: "test_access_token" });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("should return api_key from environment", async () => {
|
|
539
|
+
process.env.OPENAI_API_KEY = "sk-test-key";
|
|
540
|
+
|
|
541
|
+
const auth = await envTokenProvider({} as any, "openai");
|
|
542
|
+
|
|
543
|
+
expect(auth).toEqual({ api_key: "sk-test-key" });
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("should prefer access_token over api_key", async () => {
|
|
547
|
+
process.env.SLACK_ACCESS_TOKEN = "xoxb-token";
|
|
548
|
+
process.env.SLACK_API_KEY = "some-key";
|
|
549
|
+
|
|
550
|
+
const auth = await envTokenProvider({} as any, "slack");
|
|
551
|
+
|
|
552
|
+
expect(auth).toEqual({ access_token: "xoxb-token" });
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("should handle hyphens in provider name", async () => {
|
|
556
|
+
process.env.GOOGLE_SHEETS_ACCESS_TOKEN = "test_token";
|
|
557
|
+
|
|
558
|
+
const auth = await envTokenProvider({} as any, "google-sheets");
|
|
559
|
+
|
|
560
|
+
expect(auth).toEqual({ access_token: "test_token" });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("should throw if no token found", async () => {
|
|
564
|
+
delete process.env.UNKNOWN_ACCESS_TOKEN;
|
|
565
|
+
delete process.env.UNKNOWN_API_KEY;
|
|
566
|
+
|
|
567
|
+
await expect(envTokenProvider({} as any, "unknown")).rejects.toThrow(
|
|
568
|
+
/No token found/
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
});
|