orcheo-canvas 0.2.7 → 0.2.9
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/.bumpversion.cfg +1 -1
- package/package.json +1 -1
- package/vite.config.ts +37 -13
- package/src/App.test.tsx +0 -10
- package/src/features/chatkit/lib/chatkit-client.test.ts +0 -117
- package/src/features/workflow/components/trace/agent-prism/TraceViewer/TraceViewer.test.tsx +0 -183
- package/src/features/workflow/lib/graph-config.integration.logic.test.ts +0 -168
- package/src/features/workflow/lib/graph-config.integration.start-end.test.ts +0 -68
- package/src/features/workflow/lib/graph-config.integration.variables.test.ts +0 -67
- package/src/features/workflow/lib/workflow-storage.list.integration.test.ts +0 -70
- package/src/features/workflow/lib/workflow-storage.sanitized-save.integration.test.ts +0 -116
- package/src/features/workflow/lib/workflow-storage.save.integration.test.ts +0 -124
- package/src/features/workflow/lib/workflow-storage.test-helpers.ts +0 -29
- package/src/features/workflow/pages/workflow-canvas/components/trace-tab-content.test.tsx +0 -204
- package/src/features/workflow/pages/workflow-canvas.test.tsx +0 -194
- package/src/lib/api.test.ts +0 -137
- package/src/setupTests.ts +0 -5
- package/src/test-utils/chatkit-stub.ts +0 -21
- package/src/testing/mocks/backend/credentials.ts +0 -60
- package/src/testing/mocks/backend/request-utils.ts +0 -32
- package/src/testing/mocks/backend/workflows.ts +0 -235
- package/src/testing/mocks/backend.ts +0 -52
- package/src/testing/mocks/chatkit.ts +0 -21
- package/vitest.config.ts +0 -25
package/.bumpversion.cfg
CHANGED
package/package.json
CHANGED
package/vite.config.ts
CHANGED
|
@@ -18,26 +18,34 @@ export default defineConfig({
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
// Fix for CJS/ESM compatibility issues with React 19
|
|
21
|
-
//
|
|
21
|
+
// Force pre-bundling of all dependencies to handle CJS->ESM conversion
|
|
22
22
|
optimizeDeps: {
|
|
23
|
+
// Force re-optimization to ensure all deps are bundled
|
|
24
|
+
force: true,
|
|
25
|
+
// Only exclude test-related packages that shouldn't be in production
|
|
26
|
+
exclude: ['vitest', '@testing-library/react', '@testing-library/jest-dom', '@testing-library/user-event'],
|
|
27
|
+
// Explicitly include all known problematic CJS packages and their subpaths
|
|
23
28
|
include: [
|
|
24
|
-
//
|
|
29
|
+
// React ecosystem
|
|
30
|
+
'react',
|
|
31
|
+
'react-dom',
|
|
32
|
+
'react-dom/client',
|
|
33
|
+
'react/jsx-runtime',
|
|
34
|
+
'react/jsx-dev-runtime',
|
|
35
|
+
// use-sync-external-store (used by zustand, swr, @radix-ui)
|
|
36
|
+
'use-sync-external-store',
|
|
25
37
|
'use-sync-external-store/shim',
|
|
26
38
|
'use-sync-external-store/shim/with-selector',
|
|
27
|
-
|
|
39
|
+
'use-sync-external-store/shim/with-selector.js',
|
|
40
|
+
// prop-types (used by many React libs)
|
|
28
41
|
'prop-types',
|
|
29
|
-
'react-split',
|
|
30
|
-
// Used by @rjsf/utils and @rjsf/validator-ajv8
|
|
31
|
-
'jsonpointer',
|
|
32
|
-
'json-schema-merge-allof',
|
|
33
42
|
'react-is',
|
|
43
|
+
// @rjsf dependencies
|
|
34
44
|
'ajv',
|
|
35
45
|
'ajv-formats',
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
'dom-helpers',
|
|
40
|
-
// Lodash submodules used by @rjsf
|
|
46
|
+
'jsonpointer',
|
|
47
|
+
'json-schema-merge-allof',
|
|
48
|
+
// lodash submodules used by @rjsf
|
|
41
49
|
'lodash/get',
|
|
42
50
|
'lodash/set',
|
|
43
51
|
'lodash/has',
|
|
@@ -66,11 +74,27 @@ export default defineConfig({
|
|
|
66
74
|
'lodash/setWith',
|
|
67
75
|
'lodash/pickBy',
|
|
68
76
|
'lodash/unset',
|
|
77
|
+
'lodash/defaultsDeep',
|
|
78
|
+
'lodash/flatten',
|
|
79
|
+
// Other CJS packages
|
|
80
|
+
'react-split',
|
|
81
|
+
'dom-helpers',
|
|
82
|
+
'dayjs',
|
|
83
|
+
'invariant',
|
|
84
|
+
'copy-to-clipboard',
|
|
85
|
+
'moment',
|
|
86
|
+
'moment-timezone',
|
|
87
|
+
'papaparse',
|
|
88
|
+
// zustand and related
|
|
89
|
+
'zustand',
|
|
90
|
+
'zustand/traditional',
|
|
91
|
+
'zustand/middleware',
|
|
69
92
|
]
|
|
70
93
|
},
|
|
71
94
|
build: {
|
|
72
95
|
commonjsOptions: {
|
|
73
|
-
include: [/
|
|
96
|
+
include: [/node_modules/],
|
|
97
|
+
transformMixedEsModules: true,
|
|
74
98
|
}
|
|
75
99
|
},
|
|
76
100
|
server: {
|
package/src/App.test.tsx
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import App from "./App";
|
|
4
|
-
|
|
5
|
-
describe("App", () => {
|
|
6
|
-
it("renders the Orcheo Canvas navigation", () => {
|
|
7
|
-
render(<App />);
|
|
8
|
-
expect(screen.getByText(/Orcheo Canvas/i)).toBeInTheDocument();
|
|
9
|
-
});
|
|
10
|
-
});
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { buildPublicChatFetch } from "./chatkit-client";
|
|
3
|
-
|
|
4
|
-
const originalFetch = window.fetch;
|
|
5
|
-
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
window.fetch = originalFetch;
|
|
8
|
-
vi.restoreAllMocks();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const createResponse = (status: number, body: unknown) =>
|
|
12
|
-
new Response(JSON.stringify(body), {
|
|
13
|
-
status,
|
|
14
|
-
headers: { "Content-Type": "application/json" },
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("buildPublicChatFetch", () => {
|
|
18
|
-
it("injects workflow id into JSON bodies", async () => {
|
|
19
|
-
const fetchMock = vi.fn(async () => createResponse(200, { ok: true }));
|
|
20
|
-
window.fetch = fetchMock as unknown as typeof window.fetch;
|
|
21
|
-
|
|
22
|
-
const handler = buildPublicChatFetch({
|
|
23
|
-
workflowId: "wf-123",
|
|
24
|
-
backendBaseUrl: "http://localhost:8000",
|
|
25
|
-
metadata: { workflow_name: "LangGraph" },
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await handler("http://localhost:8000/api/chatkit", {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: { "Content-Type": "application/json" },
|
|
31
|
-
body: JSON.stringify({ foo: "bar" }),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
35
|
-
const [, options] = fetchMock.mock.calls[0]!;
|
|
36
|
-
expect(options?.credentials).toBe("include");
|
|
37
|
-
|
|
38
|
-
const payload = JSON.parse((options?.body as string) ?? "{}");
|
|
39
|
-
expect(payload.workflow_id).toBe("wf-123");
|
|
40
|
-
expect(payload.foo).toBe("bar");
|
|
41
|
-
expect(payload.metadata.workflow_id).toBe("wf-123");
|
|
42
|
-
expect(payload.metadata.workflow_name).toBe("LangGraph");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("emits structured errors when the backend rejects a request", async () => {
|
|
46
|
-
const fetchMock = vi.fn(async () =>
|
|
47
|
-
createResponse(401, {
|
|
48
|
-
code: "chatkit.auth.oauth_required",
|
|
49
|
-
message: "login first",
|
|
50
|
-
}),
|
|
51
|
-
);
|
|
52
|
-
window.fetch = fetchMock as unknown as typeof window.fetch;
|
|
53
|
-
|
|
54
|
-
const onHttpError = vi.fn();
|
|
55
|
-
const handler = buildPublicChatFetch({
|
|
56
|
-
workflowId: "wf-123",
|
|
57
|
-
onHttpError,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
await handler("http://localhost:8000/api/chatkit", {
|
|
61
|
-
method: "POST",
|
|
62
|
-
headers: { "Content-Type": "application/json" },
|
|
63
|
-
body: JSON.stringify({}),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
expect(onHttpError).toHaveBeenCalledWith({
|
|
67
|
-
status: 401,
|
|
68
|
-
message: "login first",
|
|
69
|
-
code: "chatkit.auth.oauth_required",
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("merges existing metadata without overwriting it", async () => {
|
|
74
|
-
const fetchMock = vi.fn(async () => createResponse(200, { ok: true }));
|
|
75
|
-
window.fetch = fetchMock as unknown as typeof window.fetch;
|
|
76
|
-
|
|
77
|
-
const handler = buildPublicChatFetch({
|
|
78
|
-
workflowId: "wf-789",
|
|
79
|
-
metadata: { injected: "value" },
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
await handler("http://localhost:8000/api/chatkit", {
|
|
83
|
-
method: "POST",
|
|
84
|
-
headers: { "Content-Type": "application/json" },
|
|
85
|
-
body: JSON.stringify({
|
|
86
|
-
metadata: { existing: "field" },
|
|
87
|
-
}),
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const [, options] = fetchMock.mock.calls[0]!;
|
|
91
|
-
const payload = JSON.parse((options?.body as string) ?? "{}");
|
|
92
|
-
expect(payload.metadata).toMatchObject({
|
|
93
|
-
existing: "field",
|
|
94
|
-
injected: "value",
|
|
95
|
-
workflow_id: "wf-789",
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("does not inject Authorization headers by default", async () => {
|
|
100
|
-
const fetchMock = vi.fn(async () => createResponse(200, { ok: true }));
|
|
101
|
-
window.fetch = fetchMock as unknown as typeof window.fetch;
|
|
102
|
-
|
|
103
|
-
const handler = buildPublicChatFetch({
|
|
104
|
-
workflowId: "wf-222",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await handler("http://localhost:8000/api/chatkit", {
|
|
108
|
-
method: "POST",
|
|
109
|
-
headers: { "Content-Type": "application/json" },
|
|
110
|
-
body: JSON.stringify({}),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const [, options] = fetchMock.mock.calls[0]!;
|
|
114
|
-
const headers = new Headers(options?.headers ?? {});
|
|
115
|
-
expect(headers.has("Authorization")).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { cleanup, render, screen } from "@testing-library/react";
|
|
3
|
-
import userEvent from "@testing-library/user-event";
|
|
4
|
-
import type { TraceRecord, TraceSpan } from "@evilmartians/agent-prism-types";
|
|
5
|
-
|
|
6
|
-
type LayoutMockProps = {
|
|
7
|
-
selectedTraceId?: string;
|
|
8
|
-
filteredSpans: TraceSpan[];
|
|
9
|
-
handleTraceSelect: (trace: TraceRecord) => void;
|
|
10
|
-
traceRecords: TraceRecord[];
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const desktopLayoutMock = vi.fn((props: unknown) => {
|
|
14
|
-
const { selectedTraceId, filteredSpans, handleTraceSelect, traceRecords } =
|
|
15
|
-
props as LayoutMockProps;
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div data-testid="desktop-layout">
|
|
19
|
-
<span data-testid="selected-trace-id">{selectedTraceId ?? "none"}</span>
|
|
20
|
-
<span data-testid="span-count">{filteredSpans.length}</span>
|
|
21
|
-
<button
|
|
22
|
-
type="button"
|
|
23
|
-
onClick={() => handleTraceSelect(traceRecords.at(-1)!)}
|
|
24
|
-
>
|
|
25
|
-
select-last-trace
|
|
26
|
-
</button>
|
|
27
|
-
</div>
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
vi.mock("../shared", () => ({
|
|
32
|
-
useIsMobile: () => false,
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
vi.mock("./TraceViewerDesktopLayout", () => ({
|
|
36
|
-
TraceViewerDesktopLayout: (props: unknown) => desktopLayoutMock(props),
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
vi.mock("./TraceViewerMobileLayout", () => ({
|
|
40
|
-
TraceViewerMobileLayout: () => null,
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
import { TraceViewer, type TraceViewerData } from "./TraceViewer";
|
|
44
|
-
|
|
45
|
-
const createSpan = (id: string): TraceSpan => ({
|
|
46
|
-
id,
|
|
47
|
-
title: `Span ${id}`,
|
|
48
|
-
startTime: new Date("2024-01-01T00:00:00Z"),
|
|
49
|
-
endTime: new Date("2024-01-01T00:00:01Z"),
|
|
50
|
-
duration: 1000,
|
|
51
|
-
type: "llm_call",
|
|
52
|
-
raw: "{}",
|
|
53
|
-
status: "success",
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const createViewerData = (id: string, spanCount: number): TraceViewerData => {
|
|
57
|
-
const spans = Array.from({ length: spanCount }, (_, index) =>
|
|
58
|
-
createSpan(`${id}-span-${index}`),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
traceRecord: {
|
|
63
|
-
id,
|
|
64
|
-
name: `Trace ${id}`,
|
|
65
|
-
spansCount: spanCount,
|
|
66
|
-
durationMs: spanCount * 100,
|
|
67
|
-
agentDescription: "agent",
|
|
68
|
-
totalTokens: spanCount * 10,
|
|
69
|
-
startTime: Date.now(),
|
|
70
|
-
},
|
|
71
|
-
spans,
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
describe("TraceViewer", () => {
|
|
76
|
-
afterEach(() => {
|
|
77
|
-
cleanup();
|
|
78
|
-
desktopLayoutMock.mockClear();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("focuses the active trace id and refreshes spans when data updates", () => {
|
|
82
|
-
const initialData = [
|
|
83
|
-
createViewerData("trace-1", 1),
|
|
84
|
-
createViewerData("trace-2", 1),
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
const { rerender } = render(
|
|
88
|
-
<TraceViewer data={initialData} activeTraceId="trace-2" />,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const selectedTraceIds = () =>
|
|
92
|
-
screen
|
|
93
|
-
.getAllByTestId("selected-trace-id")
|
|
94
|
-
.map((element) => element.textContent);
|
|
95
|
-
const spanCounts = () =>
|
|
96
|
-
screen.getAllByTestId("span-count").map((element) => element.textContent);
|
|
97
|
-
|
|
98
|
-
expect(selectedTraceIds()).toContain("trace-2");
|
|
99
|
-
expect(spanCounts()).toContain("1");
|
|
100
|
-
|
|
101
|
-
const updatedTrace = {
|
|
102
|
-
...initialData[1],
|
|
103
|
-
traceRecord: {
|
|
104
|
-
...initialData[1].traceRecord,
|
|
105
|
-
spansCount: initialData[1].traceRecord.spansCount + 1,
|
|
106
|
-
},
|
|
107
|
-
spans: [...initialData[1].spans, createSpan("trace-2-span-1")],
|
|
108
|
-
} satisfies TraceViewerData;
|
|
109
|
-
|
|
110
|
-
rerender(
|
|
111
|
-
<TraceViewer
|
|
112
|
-
data={[initialData[0], updatedTrace]}
|
|
113
|
-
activeTraceId="trace-2"
|
|
114
|
-
/>,
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
expect(selectedTraceIds()).toContain("trace-2");
|
|
118
|
-
expect(spanCounts()).toContain("2");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("preserves manual selections across data refreshes", async () => {
|
|
122
|
-
const user = userEvent.setup();
|
|
123
|
-
const initialData = [
|
|
124
|
-
createViewerData("trace-1", 1),
|
|
125
|
-
createViewerData("trace-2", 1),
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
const { rerender } = render(<TraceViewer data={initialData} />);
|
|
129
|
-
|
|
130
|
-
const [selectLastTraceButton] = screen.getAllByRole("button", {
|
|
131
|
-
name: /select-last-trace/i,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
await user.click(selectLastTraceButton);
|
|
135
|
-
|
|
136
|
-
expect(
|
|
137
|
-
screen
|
|
138
|
-
.getAllByTestId("selected-trace-id")
|
|
139
|
-
.map((element) => element.textContent),
|
|
140
|
-
).toContain("trace-2");
|
|
141
|
-
|
|
142
|
-
const refreshedTrace = {
|
|
143
|
-
...initialData[1],
|
|
144
|
-
traceRecord: {
|
|
145
|
-
...initialData[1].traceRecord,
|
|
146
|
-
spansCount: initialData[1].traceRecord.spansCount + 1,
|
|
147
|
-
},
|
|
148
|
-
spans: [...initialData[1].spans, createSpan("trace-2-span-1")],
|
|
149
|
-
} satisfies TraceViewerData;
|
|
150
|
-
|
|
151
|
-
rerender(<TraceViewer data={[initialData[0], refreshedTrace]} />);
|
|
152
|
-
|
|
153
|
-
expect(
|
|
154
|
-
screen
|
|
155
|
-
.getAllByTestId("selected-trace-id")
|
|
156
|
-
.map((element) => element.textContent),
|
|
157
|
-
).toContain("trace-2");
|
|
158
|
-
expect(
|
|
159
|
-
screen.getAllByTestId("span-count").map((element) => element.textContent),
|
|
160
|
-
).toContain("2");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("invokes onTraceSelect when a user picks a trace", () => {
|
|
164
|
-
const initialData = [
|
|
165
|
-
createViewerData("trace-1", 1),
|
|
166
|
-
createViewerData("trace-2", 1),
|
|
167
|
-
];
|
|
168
|
-
const handleTraceSelect = vi.fn();
|
|
169
|
-
|
|
170
|
-
render(
|
|
171
|
-
<TraceViewer data={initialData} onTraceSelect={handleTraceSelect} />,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
const lastCall = desktopLayoutMock.mock.calls.at(
|
|
175
|
-
-1,
|
|
176
|
-
)?.[0] as LayoutMockProps;
|
|
177
|
-
lastCall.handleTraceSelect(lastCall.traceRecords.at(-1)!);
|
|
178
|
-
|
|
179
|
-
expect(handleTraceSelect).toHaveBeenCalledWith(
|
|
180
|
-
expect.objectContaining({ id: "trace-2" }),
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import type { Edge, Node } from "@xyflow/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { buildGraphConfigFromCanvas } from "./graph-config";
|
|
5
|
-
|
|
6
|
-
describe("buildGraphConfigFromCanvas integration - logic and utility nodes", () => {
|
|
7
|
-
it("serializes logic and utility nodes for backend consumption", async () => {
|
|
8
|
-
const nodes: Node[] = [
|
|
9
|
-
{
|
|
10
|
-
id: "prep-1",
|
|
11
|
-
type: "utility",
|
|
12
|
-
position: { x: -1, y: 0 },
|
|
13
|
-
data: {
|
|
14
|
-
label: "Prepare score",
|
|
15
|
-
backendType: "SetVariableNode",
|
|
16
|
-
variables: [
|
|
17
|
-
{ name: "state.user.score", valueType: "number", value: "8" },
|
|
18
|
-
],
|
|
19
|
-
},
|
|
20
|
-
} as Node,
|
|
21
|
-
{
|
|
22
|
-
id: "if-1",
|
|
23
|
-
type: "logic",
|
|
24
|
-
position: { x: 0, y: 0 },
|
|
25
|
-
data: {
|
|
26
|
-
label: "Decision",
|
|
27
|
-
backendType: "IfElseNode",
|
|
28
|
-
conditions: [
|
|
29
|
-
{
|
|
30
|
-
id: "cond-1",
|
|
31
|
-
left: "{{ state.user.score }}",
|
|
32
|
-
operator: "greater_than",
|
|
33
|
-
right: 5,
|
|
34
|
-
caseSensitive: false,
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "cond-2",
|
|
38
|
-
left: true,
|
|
39
|
-
operator: "is_truthy",
|
|
40
|
-
right: null,
|
|
41
|
-
caseSensitive: true,
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
conditionLogic: "and",
|
|
45
|
-
},
|
|
46
|
-
} as Node,
|
|
47
|
-
{
|
|
48
|
-
id: "set-1",
|
|
49
|
-
type: "utility",
|
|
50
|
-
position: { x: 1, y: 0 },
|
|
51
|
-
data: {
|
|
52
|
-
label: "Assign",
|
|
53
|
-
backendType: "SetVariableNode",
|
|
54
|
-
variables: [
|
|
55
|
-
{ name: "profile.name", valueType: "string", value: "Ada" },
|
|
56
|
-
{ name: "profile.score", valueType: "number", value: "42" },
|
|
57
|
-
{
|
|
58
|
-
name: "preferences",
|
|
59
|
-
valueType: "object",
|
|
60
|
-
value: { theme: "dark" },
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "flags",
|
|
64
|
-
valueType: "array",
|
|
65
|
-
value: ["beta", "ops"],
|
|
66
|
-
},
|
|
67
|
-
{ name: "isActive", valueType: "boolean", value: "true" },
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
} as Node,
|
|
71
|
-
{
|
|
72
|
-
id: "delay-1",
|
|
73
|
-
type: "utility",
|
|
74
|
-
position: { x: 2, y: 0 },
|
|
75
|
-
data: {
|
|
76
|
-
label: "Delay",
|
|
77
|
-
backendType: "DelayNode",
|
|
78
|
-
durationSeconds: "2.5",
|
|
79
|
-
},
|
|
80
|
-
} as Node,
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
const edges: Edge[] = [
|
|
84
|
-
{ id: "prep-to-if", source: "prep-1", target: "if-1" } as Edge,
|
|
85
|
-
{
|
|
86
|
-
id: "if-to-set",
|
|
87
|
-
source: "if-1",
|
|
88
|
-
target: "set-1",
|
|
89
|
-
sourceHandle: "true",
|
|
90
|
-
} as Edge,
|
|
91
|
-
{
|
|
92
|
-
id: "if-to-delay",
|
|
93
|
-
source: "if-1",
|
|
94
|
-
target: "delay-1",
|
|
95
|
-
sourceHandle: "false",
|
|
96
|
-
} as Edge,
|
|
97
|
-
{ id: "set-to-delay", source: "set-1", target: "delay-1" } as Edge,
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
const { config, canvasToGraph, graphToCanvas, warnings } =
|
|
101
|
-
await buildGraphConfigFromCanvas(nodes, edges);
|
|
102
|
-
|
|
103
|
-
expect(warnings).toHaveLength(0);
|
|
104
|
-
|
|
105
|
-
const prepName = canvasToGraph["prep-1"];
|
|
106
|
-
const ifElseName = canvasToGraph["if-1"];
|
|
107
|
-
const setVariableName = canvasToGraph["set-1"];
|
|
108
|
-
const delayName = canvasToGraph["delay-1"];
|
|
109
|
-
|
|
110
|
-
expect(prepName).toBeDefined();
|
|
111
|
-
expect(ifElseName).toBeDefined();
|
|
112
|
-
expect(graphToCanvas[ifElseName]).toBe("if-1");
|
|
113
|
-
|
|
114
|
-
expect(config.nodes.some((node) => node.name === ifElseName)).toBe(false);
|
|
115
|
-
expect(config.edge_nodes).toBeDefined();
|
|
116
|
-
|
|
117
|
-
const ifElseNode = config.edge_nodes?.find(
|
|
118
|
-
(node) => node.name === ifElseName,
|
|
119
|
-
);
|
|
120
|
-
expect(ifElseNode).toBeDefined();
|
|
121
|
-
expect(ifElseNode).toMatchObject({
|
|
122
|
-
type: "IfElseNode",
|
|
123
|
-
condition_logic: "and",
|
|
124
|
-
});
|
|
125
|
-
expect(ifElseNode?.conditions).toEqual([
|
|
126
|
-
expect.objectContaining({
|
|
127
|
-
left: "{{ state.user.score }}",
|
|
128
|
-
operator: "greater_than",
|
|
129
|
-
right: 5,
|
|
130
|
-
case_sensitive: false,
|
|
131
|
-
}),
|
|
132
|
-
expect.objectContaining({
|
|
133
|
-
left: true,
|
|
134
|
-
operator: "is_truthy",
|
|
135
|
-
case_sensitive: true,
|
|
136
|
-
}),
|
|
137
|
-
]);
|
|
138
|
-
|
|
139
|
-
const setVariableNode = config.nodes.find(
|
|
140
|
-
(node) => node.name === setVariableName,
|
|
141
|
-
);
|
|
142
|
-
expect(setVariableNode).toBeDefined();
|
|
143
|
-
expect(setVariableNode?.variables).toEqual({
|
|
144
|
-
"profile.name": "Ada",
|
|
145
|
-
"profile.score": 42,
|
|
146
|
-
preferences: { theme: "dark" },
|
|
147
|
-
flags: ["beta", "ops"],
|
|
148
|
-
isActive: true,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const delayNode = config.nodes.find((node) => node.name === delayName);
|
|
152
|
-
expect(delayNode).toMatchObject({
|
|
153
|
-
type: "DelayNode",
|
|
154
|
-
duration_seconds: 2.5,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
expect(config.conditional_edges).toContainEqual({
|
|
158
|
-
source: prepName,
|
|
159
|
-
path: ifElseName,
|
|
160
|
-
mapping: { true: setVariableName, false: delayName },
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
expect(config.edges).toContainEqual({
|
|
164
|
-
source: setVariableName,
|
|
165
|
-
target: delayName,
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Edge, Node } from "@xyflow/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { buildGraphConfigFromCanvas } from "./graph-config";
|
|
5
|
-
|
|
6
|
-
describe("buildGraphConfigFromCanvas integration - start/end filtering", () => {
|
|
7
|
-
it("filters out canvas start and end nodes from serialization", async () => {
|
|
8
|
-
const nodes: Node[] = [
|
|
9
|
-
{
|
|
10
|
-
id: "start-node",
|
|
11
|
-
type: "start",
|
|
12
|
-
position: { x: 0, y: 0 },
|
|
13
|
-
data: { label: "Workflow Start", type: "start" },
|
|
14
|
-
} as Node,
|
|
15
|
-
{
|
|
16
|
-
id: "set-var",
|
|
17
|
-
type: "function",
|
|
18
|
-
position: { x: 100, y: 0 },
|
|
19
|
-
data: {
|
|
20
|
-
label: "Set Variable",
|
|
21
|
-
backendType: "SetVariableNode",
|
|
22
|
-
variables: [
|
|
23
|
-
{ name: "my_variable", valueType: "string", value: "sample" },
|
|
24
|
-
{ name: "num", valueType: "number", value: 2 },
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
} as Node,
|
|
28
|
-
{
|
|
29
|
-
id: "end-node",
|
|
30
|
-
type: "end",
|
|
31
|
-
position: { x: 200, y: 0 },
|
|
32
|
-
data: { label: "Workflow End", type: "end" },
|
|
33
|
-
} as Node,
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const edges: Edge[] = [
|
|
37
|
-
{ id: "start-to-set", source: "start-node", target: "set-var" } as Edge,
|
|
38
|
-
{ id: "set-to-end", source: "set-var", target: "end-node" } as Edge,
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const { config, canvasToGraph, warnings } =
|
|
42
|
-
await buildGraphConfigFromCanvas(nodes, edges);
|
|
43
|
-
|
|
44
|
-
expect(warnings).toHaveLength(0);
|
|
45
|
-
expect(canvasToGraph["start-node"]).toBeUndefined();
|
|
46
|
-
expect(canvasToGraph["end-node"]).toBeUndefined();
|
|
47
|
-
|
|
48
|
-
expect(canvasToGraph["set-var"]).toBeDefined();
|
|
49
|
-
const setVarName = canvasToGraph["set-var"];
|
|
50
|
-
|
|
51
|
-
expect(config.nodes).toHaveLength(3);
|
|
52
|
-
expect(config.nodes[0]).toMatchObject({ name: "START", type: "START" });
|
|
53
|
-
expect(config.nodes[1]).toMatchObject({
|
|
54
|
-
name: setVarName,
|
|
55
|
-
type: "SetVariableNode",
|
|
56
|
-
});
|
|
57
|
-
expect(config.nodes[2]).toMatchObject({ name: "END", type: "END" });
|
|
58
|
-
|
|
59
|
-
expect(config.edges).toContainEqual({
|
|
60
|
-
source: "START",
|
|
61
|
-
target: setVarName,
|
|
62
|
-
});
|
|
63
|
-
expect(config.edges).toContainEqual({
|
|
64
|
-
source: setVarName,
|
|
65
|
-
target: "END",
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
});
|