@burdenoff/fluidgrids-fe-sdk 2026.1031.7 → 2026.1101.2

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.
Files changed (51) hide show
  1. package/dist/{ExecutionDetailPage-4LPCNWM3.js → ExecutionDetailPage-Q77U6YJO.js} +11 -8
  2. package/dist/{ExecutionsListPage-HNPUKYD7.js → ExecutionsListPage-X4H4FVLN.js} +6 -5
  3. package/dist/{WorkflowBuilder-QFSCYCUC.js → WorkflowBuilder-RJPRUMQR.js} +11 -8
  4. package/dist/{WorkflowsList-RV4BRJO7.js → WorkflowsList-R2JJK7BL.js} +7 -6
  5. package/dist/agentTools-MISTH7JI.js +349 -0
  6. package/dist/{chunk-77HIU3CX.js → chunk-26HGYPBC.js} +1 -1
  7. package/dist/{chunk-76UUEMTN.js → chunk-37VUN74D.js} +4 -4
  8. package/dist/{chunk-QPZCJKII.js → chunk-3ERT6FQK.js} +3 -3
  9. package/dist/chunk-472HJ3S3.js +3929 -0
  10. package/dist/{chunk-WNZV4SLU.js → chunk-4QHCXTHW.js} +4 -4
  11. package/dist/chunk-AASHQI77.js +126 -0
  12. package/dist/chunk-CGXQVZGY.js +260 -0
  13. package/dist/chunk-CRDG2SQQ.js +185 -0
  14. package/dist/chunk-DQ2ZCXYX.js +21875 -0
  15. package/dist/chunk-F7DD7BOB.js +1164 -0
  16. package/dist/chunk-GTEBQQ7J.js +24 -0
  17. package/dist/{chunk-ALFMJSD6.js → chunk-H7XPXONT.js} +1 -1
  18. package/dist/{chunk-UO5DUKYN.js → chunk-HKKSXOFO.js} +8 -7
  19. package/dist/chunk-IA2ZCHNQ.js +1336 -0
  20. package/dist/{chunk-3M36XWAH.js → chunk-IGRIN7NL.js} +8 -7
  21. package/dist/chunk-JDLQ6QIB.js +283 -0
  22. package/dist/{chunk-AD7BICOT.js → chunk-JKBCLBHY.js} +1 -1
  23. package/dist/{chunk-6JX5UPDW.js → chunk-JNZCXFLD.js} +1 -1
  24. package/dist/{chunk-SEFOT2LD.js → chunk-LWLHPVIA.js} +1 -1
  25. package/dist/{chunk-JWROEOG5.js → chunk-LYTR6IQO.js} +1 -38
  26. package/dist/chunk-MR2B67NW.js +338 -0
  27. package/dist/{chunk-RIHYPPMQ.js → chunk-NLYQ3XDE.js} +1 -1
  28. package/dist/{chunk-6ILIBJFH.js → chunk-NZJWKVER.js} +7904 -7943
  29. package/dist/chunk-OCJTTHTS.js +46 -0
  30. package/dist/chunk-OSQ3OCR3.js +1193 -0
  31. package/dist/{chunk-VJL3DFJT.js → chunk-REO4S5MY.js} +2 -2
  32. package/dist/{chunk-XKQMCPSB.js → chunk-T2UUYYVJ.js} +4 -4
  33. package/dist/{chunk-BXR6BWXM.js → chunk-W7MTSATB.js} +5 -5
  34. package/dist/{chunk-A47PSD7A.js → chunk-WF6GMBR4.js} +7 -4
  35. package/dist/chunk-WYCB6OQL.js +19709 -0
  36. package/dist/{chunk-YOLPHVOE.js → chunk-YKFR7BZJ.js} +1 -1
  37. package/dist/{chunk-26NENGXA.js → chunk-YSXNJGCY.js} +4 -4
  38. package/dist/code-editor-field-JKCUR6BI.js +3 -0
  39. package/dist/executions.js +12 -9
  40. package/dist/history.js +4 -3
  41. package/dist/index.js +41 -32
  42. package/dist/node-detail-view-PCYFQX62.js +2034 -0
  43. package/dist/observability.js +5 -4
  44. package/dist/quick-config-panel-BRZZHOSI.js +402 -0
  45. package/dist/settings.js +6 -5
  46. package/dist/templates.js +10 -7
  47. package/dist/workflow-builder.js +12 -9
  48. package/dist/workflows-list.js +7 -6
  49. package/package.json +4 -1
  50. package/dist/chunk-NMEI7TQW.js +0 -2628
  51. package/dist/chunk-S7XV65DB.js +0 -50350
@@ -0,0 +1,1164 @@
1
+ import { operatorSpec, normaliseAssertRules, describeAssertRuleProblem } from './chunk-JDLQ6QIB.js';
2
+ import { useEntityTags, useTagEntity, useUntagEntity } from './chunk-PZ45H72P.js';
3
+ import { notifyFluidGridsAiCreditsUpdated } from './chunk-Y53CVZVV.js';
4
+ import { ErrorType } from './chunk-7KKYIRLH.js';
5
+ import { throwIfResultError, isForbiddenError } from './chunk-NZJWKVER.js';
6
+ import { getFluidGridsConfig } from './chunk-6AMNEX2Y.js';
7
+ import { WorkflowProposalDocument, ProposeRunFixDocument, ProposeWorkflowChangeDocument, ApplyWorkflowProposalDocument, RejectWorkflowProposalDocument, CancelWorkflowProposalDocument, WorkflowTestCasesDocument, WorkflowTestRunsDocument, WorkflowTestRunDocument, CreateWorkflowTestCaseDocument, UpdateWorkflowTestCaseDocument, DeleteWorkflowTestCaseDocument, RunWorkflowTestsDocument, CreateWorkflowTestCaseFromRunDocument } from './chunk-IHEV5HTD.js';
8
+ import { useTr } from './chunk-UULVIT25.js';
9
+ import { getFluidGridsClient } from './chunk-NTWG3CJX.js';
10
+ import { useState, useRef, useEffect, useCallback } from 'react';
11
+ import { TagBadge, TagsWidget } from '@burdenoff/fe-libs/tag';
12
+ import { jsx, jsxs } from 'react/jsx-runtime';
13
+ import { Icon } from '@iconify/react';
14
+ import { Badge, Button } from '@burdenoff/fe-libs/ui';
15
+
16
+ function toTagItem(assignment) {
17
+ return {
18
+ id: assignment.tagId,
19
+ key: assignment.tagInfo?.key ?? assignment.tagId,
20
+ label: assignment.tagInfo?.label ?? assignment.tagInfo?.key ?? "Tag",
21
+ color: assignment.tagInfo?.color ?? null
22
+ };
23
+ }
24
+ function EntityTagPicker({
25
+ entityType,
26
+ entityId,
27
+ onDraftChange,
28
+ readOnly = false,
29
+ onChange,
30
+ className
31
+ }) {
32
+ const draftMode = !entityId;
33
+ const { workspaceId } = getFluidGridsConfig();
34
+ const { tags: assignments, refetch } = useEntityTags(entityType, entityId);
35
+ const tagEntity = useTagEntity();
36
+ const untagEntity = useUntagEntity();
37
+ const [current, setCurrent] = useState([]);
38
+ const currentRef = useRef([]);
39
+ const assignmentKey = assignments.map((a) => a.tagId).join("|");
40
+ const syncedKey = useRef(null);
41
+ useEffect(() => {
42
+ if (draftMode || syncedKey.current === assignmentKey) return;
43
+ syncedKey.current = assignmentKey;
44
+ const synced = assignments.map(toTagItem);
45
+ currentRef.current = synced;
46
+ setCurrent(synced);
47
+ }, [assignmentKey, assignments, draftMode]);
48
+ const handleAdd = useCallback(
49
+ async (tag) => {
50
+ if (currentRef.current.some((t) => t.id === tag.id)) return;
51
+ const next = [...currentRef.current, tag];
52
+ currentRef.current = next;
53
+ setCurrent(next);
54
+ if (draftMode) {
55
+ onDraftChange?.(next.map((t) => t.id));
56
+ return;
57
+ }
58
+ try {
59
+ await tagEntity(entityType, entityId, tag.id);
60
+ await refetch();
61
+ onChange?.();
62
+ } catch {
63
+ const rolledBack = currentRef.current.filter((t) => t.id !== tag.id);
64
+ currentRef.current = rolledBack;
65
+ setCurrent(rolledBack);
66
+ }
67
+ },
68
+ [
69
+ draftMode,
70
+ entityId,
71
+ entityType,
72
+ onChange,
73
+ onDraftChange,
74
+ refetch,
75
+ tagEntity
76
+ ]
77
+ );
78
+ const handleRemove = useCallback(
79
+ async (tagId) => {
80
+ const removed = currentRef.current.find((t) => t.id === tagId);
81
+ const next = currentRef.current.filter((t) => t.id !== tagId);
82
+ currentRef.current = next;
83
+ setCurrent(next);
84
+ if (draftMode) {
85
+ onDraftChange?.(next.map((t) => t.id));
86
+ return;
87
+ }
88
+ try {
89
+ await untagEntity(entityType, entityId, tagId);
90
+ await refetch();
91
+ onChange?.();
92
+ } catch {
93
+ if (removed) {
94
+ const rolledBack = [...currentRef.current, removed];
95
+ currentRef.current = rolledBack;
96
+ setCurrent(rolledBack);
97
+ }
98
+ }
99
+ },
100
+ [
101
+ draftMode,
102
+ entityId,
103
+ entityType,
104
+ onChange,
105
+ onDraftChange,
106
+ refetch,
107
+ untagEntity
108
+ ]
109
+ );
110
+ if (readOnly) {
111
+ if (current.length === 0) return null;
112
+ return /* @__PURE__ */ jsx("div", { className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: current.map((tag) => /* @__PURE__ */ jsx(
113
+ TagBadge,
114
+ {
115
+ id: tag.id,
116
+ label: tag.label,
117
+ color: tag.color ?? void 0,
118
+ size: "sm"
119
+ },
120
+ tag.id
121
+ )) });
122
+ }
123
+ return /* @__PURE__ */ jsx(
124
+ TagsWidget,
125
+ {
126
+ entityType,
127
+ entityId: entityId ?? "",
128
+ workspaceId: workspaceId || void 0,
129
+ tags: current,
130
+ onAdd: handleAdd,
131
+ onRemove: handleRemove,
132
+ syncGraphNotifications: !draftMode,
133
+ className
134
+ }
135
+ );
136
+ }
137
+ var PROPOSAL_POLL_INTERVAL_MS = 3e3;
138
+ var PROPOSAL_MAX_WAIT_MS = 6 * 60 * 1e3;
139
+ function messageOf(error) {
140
+ return error instanceof Error ? error.message : String(error);
141
+ }
142
+ function isProposalRunning(status) {
143
+ return status === "PROCESSING";
144
+ }
145
+ function useWorkflowProposal() {
146
+ const [proposal, setProposal] = useState(null);
147
+ const [isWorking, setIsWorking] = useState(false);
148
+ const [isDeciding, setIsDeciding] = useState(false);
149
+ const [error, setError] = useState(null);
150
+ const [timedOut, setTimedOut] = useState(false);
151
+ const runRef = useRef(0);
152
+ const timerRef = useRef(null);
153
+ const clearTimer = useCallback(() => {
154
+ if (timerRef.current) clearTimeout(timerRef.current);
155
+ timerRef.current = null;
156
+ }, []);
157
+ useEffect(
158
+ () => () => {
159
+ runRef.current++;
160
+ clearTimer();
161
+ },
162
+ [clearTimer]
163
+ );
164
+ const reset = useCallback(() => {
165
+ runRef.current++;
166
+ clearTimer();
167
+ setProposal(null);
168
+ setIsWorking(false);
169
+ setIsDeciding(false);
170
+ setError(null);
171
+ setTimedOut(false);
172
+ }, [clearTimer]);
173
+ const poll = useCallback(
174
+ async (run, id, startedAt) => {
175
+ if (run !== runRef.current) return;
176
+ try {
177
+ const result = throwIfResultError(
178
+ await getFluidGridsClient().query({
179
+ query: WorkflowProposalDocument,
180
+ variables: { id },
181
+ fetchPolicy: "network-only"
182
+ })
183
+ );
184
+ if (run !== runRef.current) return;
185
+ const next = result.data?.workflowProposal ?? null;
186
+ if (next) setProposal(next);
187
+ const status = next ? String(next.status) : void 0;
188
+ if (isProposalRunning(status)) {
189
+ if (Date.now() - startedAt < PROPOSAL_MAX_WAIT_MS) {
190
+ timerRef.current = setTimeout(
191
+ () => void poll(run, id, startedAt),
192
+ PROPOSAL_POLL_INTERVAL_MS
193
+ );
194
+ return;
195
+ }
196
+ setTimedOut(true);
197
+ } else {
198
+ notifyFluidGridsAiCreditsUpdated();
199
+ }
200
+ setIsWorking(false);
201
+ } catch (pollError) {
202
+ if (run !== runRef.current) return;
203
+ setError(messageOf(pollError));
204
+ setIsWorking(false);
205
+ }
206
+ },
207
+ []
208
+ );
209
+ const start = useCallback(
210
+ async (input) => {
211
+ const run = ++runRef.current;
212
+ clearTimer();
213
+ setProposal(null);
214
+ setError(null);
215
+ setTimedOut(false);
216
+ setIsWorking(true);
217
+ try {
218
+ const client = getFluidGridsClient();
219
+ let started;
220
+ if (input.kind === "runFix") {
221
+ const result = throwIfResultError(
222
+ await client.mutate({
223
+ mutation: ProposeRunFixDocument,
224
+ variables: {
225
+ input: {
226
+ runId: input.runId,
227
+ ...input.prompt ? { prompt: input.prompt } : {}
228
+ }
229
+ }
230
+ })
231
+ );
232
+ started = result.data?.proposeRunFix;
233
+ } else {
234
+ const result = throwIfResultError(
235
+ await client.mutate({
236
+ mutation: ProposeWorkflowChangeDocument,
237
+ variables: {
238
+ input: {
239
+ prompt: input.prompt,
240
+ ...input.workflowKey ? { workflowKey: input.workflowKey } : {},
241
+ ...input.baseVersion ? { baseVersion: input.baseVersion } : {},
242
+ ...input.template ? { template: input.template } : {}
243
+ }
244
+ }
245
+ })
246
+ );
247
+ started = result.data?.proposeWorkflowChange;
248
+ }
249
+ if (!started?.id) throw new Error("The proposal did not start");
250
+ if (run !== runRef.current) return;
251
+ setProposal(started);
252
+ const id = started.id;
253
+ timerRef.current = setTimeout(
254
+ () => void poll(run, id, Date.now()),
255
+ PROPOSAL_POLL_INTERVAL_MS
256
+ );
257
+ } catch (startError) {
258
+ if (run !== runRef.current) return;
259
+ setError(messageOf(startError));
260
+ setIsWorking(false);
261
+ }
262
+ },
263
+ [clearTimer, poll]
264
+ );
265
+ const open = useCallback(
266
+ async (id) => {
267
+ const run = ++runRef.current;
268
+ clearTimer();
269
+ setError(null);
270
+ setTimedOut(false);
271
+ setIsWorking(true);
272
+ await poll(run, id, Date.now());
273
+ },
274
+ [clearTimer, poll]
275
+ );
276
+ const decide = useCallback(
277
+ async (action) => {
278
+ const id = proposal?.id;
279
+ if (!id) return null;
280
+ const run = runRef.current;
281
+ setIsDeciding(true);
282
+ setError(null);
283
+ try {
284
+ const client = getFluidGridsClient();
285
+ let next;
286
+ if (action === "apply") {
287
+ const result = throwIfResultError(
288
+ await client.mutate({ mutation: ApplyWorkflowProposalDocument, variables: { id } })
289
+ );
290
+ next = result.data?.applyWorkflowProposal;
291
+ } else if (action === "reject") {
292
+ const result = throwIfResultError(
293
+ await client.mutate({ mutation: RejectWorkflowProposalDocument, variables: { id } })
294
+ );
295
+ next = result.data?.rejectWorkflowProposal;
296
+ } else {
297
+ clearTimer();
298
+ const result = throwIfResultError(
299
+ await client.mutate({ mutation: CancelWorkflowProposalDocument, variables: { id } })
300
+ );
301
+ next = result.data?.cancelWorkflowProposal;
302
+ setIsWorking(false);
303
+ }
304
+ if (run === runRef.current && next) setProposal(next);
305
+ return next ?? null;
306
+ } catch (decideError) {
307
+ if (run === runRef.current) setError(messageOf(decideError));
308
+ return null;
309
+ } finally {
310
+ if (run === runRef.current) setIsDeciding(false);
311
+ }
312
+ },
313
+ [clearTimer, proposal?.id]
314
+ );
315
+ const apply = useCallback(() => decide("apply"), [decide]);
316
+ const reject = useCallback(async () => {
317
+ await decide("reject");
318
+ }, [decide]);
319
+ const cancel = useCallback(async () => {
320
+ await decide("cancel");
321
+ }, [decide]);
322
+ return {
323
+ proposal,
324
+ isWorking,
325
+ isDeciding,
326
+ error,
327
+ timedOut,
328
+ start,
329
+ open,
330
+ apply,
331
+ reject,
332
+ cancel,
333
+ reset
334
+ };
335
+ }
336
+ var STATUS_TONE = {
337
+ PROCESSING: "secondary",
338
+ PROPOSED: "default",
339
+ APPLIED: "default",
340
+ REJECTED: "outline",
341
+ STALE: "outline",
342
+ CANCELLED: "outline",
343
+ FAILED: "destructive"
344
+ };
345
+ function edgeLabel(edge) {
346
+ const from = edge.sourceHandle ? `${edge.source} (${edge.sourceHandle})` : edge.source;
347
+ return `${from} \u2192 ${edge.target}`;
348
+ }
349
+ function ChangeList({
350
+ title,
351
+ icon,
352
+ items,
353
+ testId
354
+ }) {
355
+ if (items.length === 0) return null;
356
+ return /* @__PURE__ */ jsxs("div", { "data-testid": testId, children: [
357
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary", children: [
358
+ /* @__PURE__ */ jsx(Icon, { icon, width: 14 }),
359
+ title,
360
+ " (",
361
+ items.length,
362
+ ")"
363
+ ] }),
364
+ /* @__PURE__ */ jsx("ul", { className: "mt-1 space-y-0.5 text-sm text-text-primary", children: items.map((item) => /* @__PURE__ */ jsx("li", { className: "truncate", children: item }, item)) })
365
+ ] });
366
+ }
367
+ function nodeLine(node, withFields) {
368
+ const base = `${node.label} \xB7 ${node.type}`;
369
+ return withFields && node.fields.length > 0 ? `${base} \u2014 ${node.fields.join(", ")}` : base;
370
+ }
371
+ function visibleErrors(errors, shownMessage) {
372
+ const shown = normalise(shownMessage ?? "");
373
+ const seen = /* @__PURE__ */ new Set();
374
+ const result = [];
375
+ for (const error of errors) {
376
+ const key = normalise(error);
377
+ if (!key || seen.has(key)) continue;
378
+ seen.add(key);
379
+ if (shown && shown.includes(key)) continue;
380
+ result.push(error);
381
+ }
382
+ return result;
383
+ }
384
+ function normalise(text) {
385
+ return text.trim().toLowerCase().replace(/^[\s.:;,]+|[\s.:;,]+$/g, "");
386
+ }
387
+ function WorkflowProposalCard({
388
+ proposal,
389
+ isDeciding = false,
390
+ onOpenWorkflow,
391
+ onApply,
392
+ onReject,
393
+ onCancel,
394
+ onRevert
395
+ }) {
396
+ const tr = useTr();
397
+ const status = String(proposal.status);
398
+ const diff = proposal.diff;
399
+ const warnings = proposal.validation?.warnings ?? [];
400
+ const showMessage = Boolean(
401
+ proposal.message && status !== "PROPOSED" && status !== "PROCESSING" && status !== "APPLIED"
402
+ );
403
+ const errors = visibleErrors(
404
+ proposal.validation?.valid === false ? proposal.validation.errors : [],
405
+ showMessage ? proposal.message : null
406
+ );
407
+ const revertTarget = proposal.baseVersion && proposal.workflowKey && proposal.appliedWorkflowKey === proposal.workflowKey ? { workflowKey: proposal.workflowKey, version: proposal.baseVersion } : null;
408
+ return /* @__PURE__ */ jsxs(
409
+ "section",
410
+ {
411
+ className: "rounded-lg border border-border-secondary bg-background-secondary p-4",
412
+ "data-testid": "workflow-proposal-card",
413
+ "data-status": status,
414
+ children: [
415
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-center gap-2", children: [
416
+ /* @__PURE__ */ jsx(
417
+ Icon,
418
+ {
419
+ icon: "mdi:creation-outline",
420
+ width: 18,
421
+ className: "text-text-secondary"
422
+ }
423
+ ),
424
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-text-primary", children: proposal.name ?? tr("fluidgrids.ai.proposal.title", "AI proposal") }),
425
+ /* @__PURE__ */ jsx(
426
+ Badge,
427
+ {
428
+ variant: STATUS_TONE[status] ?? "outline",
429
+ "data-testid": "workflow-proposal-status",
430
+ children: status
431
+ }
432
+ ),
433
+ proposal.baseVersion && /* @__PURE__ */ jsx("span", { className: "text-xs text-text-secondary", children: tr(
434
+ "fluidgrids.ai.proposal.against",
435
+ "against version {{version}}",
436
+ {
437
+ version: proposal.baseVersion
438
+ }
439
+ ) })
440
+ ] }),
441
+ status === "PROCESSING" && /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center gap-2 text-sm text-text-secondary", children: [
442
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:loading", width: 16, className: "animate-spin" }),
443
+ tr("fluidgrids.ai.proposal.working", "Working ({{stage}})\u2026", {
444
+ stage: proposal.progress?.stage ?? "prompting"
445
+ }),
446
+ onCancel && /* @__PURE__ */ jsx(
447
+ Button,
448
+ {
449
+ size: "sm",
450
+ variant: "ghost",
451
+ onClick: onCancel,
452
+ disabled: isDeciding,
453
+ children: tr("fluidgrids.ai.proposal.cancel", "Cancel")
454
+ }
455
+ )
456
+ ] }),
457
+ proposal.explanation && /* @__PURE__ */ jsx(
458
+ "p",
459
+ {
460
+ className: "mt-3 text-sm text-text-primary",
461
+ "data-testid": "workflow-proposal-explanation",
462
+ children: proposal.explanation
463
+ }
464
+ ),
465
+ showMessage && /* @__PURE__ */ jsx(
466
+ "p",
467
+ {
468
+ className: "mt-2 text-sm text-text-secondary",
469
+ "data-testid": "workflow-proposal-message",
470
+ children: proposal.message
471
+ }
472
+ ),
473
+ diff && /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", "data-testid": "workflow-proposal-diff", children: [
474
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-text-primary", children: diff.summary }),
475
+ /* @__PURE__ */ jsx(
476
+ ChangeList,
477
+ {
478
+ testId: "workflow-proposal-added",
479
+ icon: "mdi:plus-circle-outline",
480
+ title: tr("fluidgrids.ai.proposal.added", "Added nodes"),
481
+ items: diff.addedNodes.map((node) => nodeLine(node, false))
482
+ }
483
+ ),
484
+ /* @__PURE__ */ jsx(
485
+ ChangeList,
486
+ {
487
+ testId: "workflow-proposal-changed",
488
+ icon: "mdi:pencil-circle-outline",
489
+ title: tr("fluidgrids.ai.proposal.changed", "Changed nodes"),
490
+ items: diff.changedNodes.map((node) => nodeLine(node, true))
491
+ }
492
+ ),
493
+ /* @__PURE__ */ jsx(
494
+ ChangeList,
495
+ {
496
+ testId: "workflow-proposal-removed",
497
+ icon: "mdi:minus-circle-outline",
498
+ title: tr("fluidgrids.ai.proposal.removed", "Removed nodes"),
499
+ items: diff.removedNodes.map((node) => nodeLine(node, false))
500
+ }
501
+ ),
502
+ /* @__PURE__ */ jsx(
503
+ ChangeList,
504
+ {
505
+ testId: "workflow-proposal-edges",
506
+ icon: "mdi:vector-line",
507
+ title: tr(
508
+ "fluidgrids.ai.proposal.connections",
509
+ "Connection changes"
510
+ ),
511
+ items: [
512
+ ...diff.addedEdges.map((edge) => `+ ${edgeLabel(edge)}`),
513
+ ...diff.removedEdges.map((edge) => `\u2212 ${edgeLabel(edge)}`)
514
+ ]
515
+ }
516
+ )
517
+ ] }),
518
+ errors.length > 0 && /* @__PURE__ */ jsx(
519
+ "ul",
520
+ {
521
+ className: "mt-3 space-y-1 text-sm text-[var(--color-status-error-text)]",
522
+ "data-testid": "workflow-proposal-errors",
523
+ children: errors.map((message) => /* @__PURE__ */ jsx("li", { children: message }, message))
524
+ }
525
+ ),
526
+ warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-3", "data-testid": "workflow-proposal-warnings", children: [
527
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-text-secondary", children: tr("fluidgrids.ai.proposal.todo", "Still to set up") }),
528
+ /* @__PURE__ */ jsx("ul", { className: "mt-1 space-y-0.5 text-sm text-text-secondary", children: warnings.map((warning) => /* @__PURE__ */ jsx("li", { children: warning }, warning)) })
529
+ ] }),
530
+ status === "PROPOSED" && (onApply || onReject) && /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-2", children: [
531
+ onApply && /* @__PURE__ */ jsx(
532
+ Button,
533
+ {
534
+ size: "sm",
535
+ onClick: onApply,
536
+ disabled: isDeciding,
537
+ "data-testid": "workflow-proposal-apply",
538
+ children: proposal.baseVersion ? tr(
539
+ "fluidgrids.ai.proposal.applyVersion",
540
+ "Apply as a new draft version"
541
+ ) : tr(
542
+ "fluidgrids.ai.proposal.applyNew",
543
+ "Create as a draft workflow"
544
+ )
545
+ }
546
+ ),
547
+ onReject && /* @__PURE__ */ jsx(
548
+ Button,
549
+ {
550
+ size: "sm",
551
+ variant: "outline",
552
+ onClick: onReject,
553
+ disabled: isDeciding,
554
+ children: tr("fluidgrids.ai.proposal.reject", "Reject")
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-text-secondary", children: tr(
558
+ "fluidgrids.ai.proposal.neverActivates",
559
+ "Nothing goes live: the result is a draft you can review, run as a test and activate yourself."
560
+ ) })
561
+ ] }),
562
+ status === "APPLIED" && proposal.appliedWorkflowKey && proposal.appliedVersion && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
563
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
564
+ onOpenWorkflow && /* @__PURE__ */ jsx(
565
+ Button,
566
+ {
567
+ size: "sm",
568
+ variant: "outline",
569
+ "data-testid": "workflow-proposal-open",
570
+ onClick: () => onOpenWorkflow(
571
+ proposal.appliedWorkflowKey ?? "",
572
+ proposal.appliedVersion ?? ""
573
+ ),
574
+ children: tr("fluidgrids.ai.proposal.open", "Open draft {{version}}", {
575
+ version: proposal.appliedVersion
576
+ })
577
+ }
578
+ ),
579
+ onRevert && revertTarget && /* @__PURE__ */ jsx(
580
+ Button,
581
+ {
582
+ size: "sm",
583
+ variant: "ghost",
584
+ "data-testid": "workflow-proposal-revert",
585
+ disabled: isDeciding,
586
+ onClick: () => onRevert(revertTarget.workflowKey, revertTarget.version),
587
+ children: tr(
588
+ "fluidgrids.ai.proposal.revert",
589
+ "Revert to {{version}}",
590
+ { version: revertTarget.version }
591
+ )
592
+ }
593
+ )
594
+ ] }),
595
+ !onRevert && proposal.restoreHint && /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: proposal.restoreHint })
596
+ ] })
597
+ ]
598
+ }
599
+ );
600
+ }
601
+
602
+ // src/builder/lib/workflow-tests.ts
603
+ var MAX_TEST_CASES = 50;
604
+ var MAX_CASE_JSON_BYTES = 256 * 1024;
605
+ var MAX_CASE_ENTRIES = 100;
606
+ var MAX_CASE_NAME = 200;
607
+ function emptyTestCaseDraft() {
608
+ return {
609
+ name: "",
610
+ description: "",
611
+ enabled: true,
612
+ expectedStatus: "COMPLETED" /* Completed */,
613
+ triggerNodeId: "",
614
+ triggerData: "{}",
615
+ pinnedOutputs: "{}",
616
+ connectionMocks: "[]",
617
+ expectations: "{}"
618
+ };
619
+ }
620
+ function pretty(value) {
621
+ return JSON.stringify(value, null, 2);
622
+ }
623
+ function isRecord(value) {
624
+ return typeof value === "object" && value !== null && !Array.isArray(value);
625
+ }
626
+ function wireRule(rule) {
627
+ const wire = {
628
+ operator: operatorSpec(rule.operator)?.wire ?? String(rule.operator)
629
+ };
630
+ if (rule.path) wire.path = rule.path;
631
+ if (rule.expected !== null && rule.expected !== void 0)
632
+ wire.expected = rule.expected;
633
+ if (rule.min !== null && rule.min !== void 0) wire.min = rule.min;
634
+ if (rule.max !== null && rule.max !== void 0) wire.max = rule.max;
635
+ if (rule.message) wire.message = rule.message;
636
+ return wire;
637
+ }
638
+ function draftFromTestCase(testCase) {
639
+ const expectations = {};
640
+ for (const expectation of testCase.expectations)
641
+ expectations[expectation.nodeId] = expectation.rules.map(wireRule);
642
+ const mocks = testCase.connectionMocks.map((mock) => {
643
+ const entry = { nodeId: mock.nodeId };
644
+ if (mock.operation) entry.operation = mock.operation;
645
+ if (mock.error) {
646
+ entry.error = mock.error.errorType ? { message: mock.error.message, errorType: mock.error.errorType } : { message: mock.error.message };
647
+ } else {
648
+ entry.response = mock.response ?? {};
649
+ }
650
+ return entry;
651
+ });
652
+ return {
653
+ name: testCase.name,
654
+ description: testCase.description ?? "",
655
+ enabled: testCase.enabled,
656
+ expectedStatus: testCase.expectedStatus,
657
+ triggerNodeId: testCase.triggerNodeId ?? "",
658
+ triggerData: pretty(testCase.triggerData ?? {}),
659
+ pinnedOutputs: pretty(testCase.pinnedOutputs ?? {}),
660
+ connectionMocks: pretty(mocks),
661
+ expectations: pretty(expectations)
662
+ };
663
+ }
664
+ function pinnedOutputsFromCanvas(pinned) {
665
+ const outputs = {};
666
+ for (const [nodeId, entry] of Object.entries(pinned ?? {})) {
667
+ if (entry && isRecord(entry.data)) outputs[nodeId] = { data: entry.data };
668
+ }
669
+ return outputs;
670
+ }
671
+ function parseJson(text, field, label) {
672
+ const source = text.trim();
673
+ if (source === "") return { value: void 0 };
674
+ try {
675
+ return { value: JSON.parse(source) };
676
+ } catch (err) {
677
+ const reason = err instanceof Error ? err.message : String(err);
678
+ return {
679
+ error: {
680
+ field,
681
+ key: "fluidgrids.builder.workflowTests.invalidJson",
682
+ fallback: `${label} is not valid JSON: ${reason}`
683
+ }
684
+ };
685
+ }
686
+ }
687
+ function fieldError(field, key, fallback) {
688
+ return {
689
+ error: { field, key: `fluidgrids.builder.workflowTests.${key}`, fallback }
690
+ };
691
+ }
692
+ function parseTriggerData(text) {
693
+ const parsed = parseJson(text, "triggerData", "Trigger data");
694
+ if ("error" in parsed) return parsed;
695
+ if (parsed.value === void 0) return { value: {} };
696
+ if (!isRecord(parsed.value))
697
+ return fieldError(
698
+ "triggerData",
699
+ "triggerDataShape",
700
+ "Trigger data must be a JSON object."
701
+ );
702
+ return { value: parsed.value };
703
+ }
704
+ function parsePinnedOutputs(text) {
705
+ const parsed = parseJson(text, "pinnedOutputs", "Pinned outputs");
706
+ if ("error" in parsed) return parsed;
707
+ if (parsed.value === void 0) return { value: {} };
708
+ if (!isRecord(parsed.value))
709
+ return fieldError(
710
+ "pinnedOutputs",
711
+ "pinnedShape",
712
+ 'Pinned outputs must be an object of { "nodeId": { "data": { ... } } }.'
713
+ );
714
+ const entries = Object.entries(parsed.value);
715
+ if (entries.length > MAX_CASE_ENTRIES)
716
+ return fieldError(
717
+ "pinnedOutputs",
718
+ "pinnedTooMany",
719
+ `At most ${MAX_CASE_ENTRIES} pinned nodes.`
720
+ );
721
+ const outputs = {};
722
+ for (const [nodeId, entry] of entries) {
723
+ if (!isRecord(entry) || !isRecord(entry.data))
724
+ return fieldError(
725
+ "pinnedOutputs",
726
+ "pinnedShape",
727
+ `Pinned output "${nodeId}" must be { "data": { ... } }.`
728
+ );
729
+ outputs[nodeId] = { data: entry.data };
730
+ }
731
+ return { value: outputs };
732
+ }
733
+ var ERROR_TYPES = new Set(Object.values(ErrorType));
734
+ function isErrorType(value) {
735
+ return ERROR_TYPES.has(value);
736
+ }
737
+ function parseConnectionMocks(text) {
738
+ const parsed = parseJson(text, "connectionMocks", "Connection mocks");
739
+ if ("error" in parsed) return parsed;
740
+ if (parsed.value === void 0) return { value: [] };
741
+ if (!Array.isArray(parsed.value))
742
+ return fieldError(
743
+ "connectionMocks",
744
+ "mocksShape",
745
+ 'Connection mocks must be an array of { "nodeId", "response" } or { "nodeId", "error" }.'
746
+ );
747
+ if (parsed.value.length > MAX_CASE_ENTRIES)
748
+ return fieldError(
749
+ "connectionMocks",
750
+ "mocksTooMany",
751
+ `At most ${MAX_CASE_ENTRIES} connection mocks.`
752
+ );
753
+ const mocks = [];
754
+ for (const [index, raw] of parsed.value.entries()) {
755
+ const at = `Mock ${index + 1}`;
756
+ if (!isRecord(raw) || typeof raw.nodeId !== "string" || !raw.nodeId)
757
+ return fieldError(
758
+ "connectionMocks",
759
+ "mockNodeRequired",
760
+ `${at} needs a "nodeId".`
761
+ );
762
+ if (raw.operation !== void 0 && raw.operation !== null && typeof raw.operation !== "string")
763
+ return fieldError(
764
+ "connectionMocks",
765
+ "mockOperationInvalid",
766
+ `${at}: "operation" must be text.`
767
+ );
768
+ const hasResponse = raw.response !== void 0 && raw.response !== null;
769
+ const hasError = raw.error !== void 0 && raw.error !== null;
770
+ if (hasResponse === hasError)
771
+ return fieldError(
772
+ "connectionMocks",
773
+ "mockResponseOrError",
774
+ `${at} must set exactly one of "response" and "error".`
775
+ );
776
+ const mock = { nodeId: raw.nodeId };
777
+ if (typeof raw.operation === "string" && raw.operation)
778
+ mock.operation = raw.operation;
779
+ if (hasResponse) {
780
+ mock.response = jsonScalar(raw.response);
781
+ } else {
782
+ const error = raw.error;
783
+ if (!isRecord(error) || typeof error.message !== "string")
784
+ return fieldError(
785
+ "connectionMocks",
786
+ "mockErrorShape",
787
+ `${at}: "error" must be { "message": "...", "errorType"?: "..." }.`
788
+ );
789
+ const errorType = error.errorType;
790
+ if (errorType !== void 0 && errorType !== null) {
791
+ if (typeof errorType !== "string" || !isErrorType(errorType))
792
+ return fieldError(
793
+ "connectionMocks",
794
+ "mockErrorType",
795
+ `${at}: "errorType" must be one of ${[...ERROR_TYPES].join(", ")}.`
796
+ );
797
+ mock.error = { message: error.message, errorType };
798
+ } else {
799
+ mock.error = { message: error.message };
800
+ }
801
+ }
802
+ mocks.push(mock);
803
+ }
804
+ return { value: mocks };
805
+ }
806
+ function jsonScalar(value) {
807
+ return value;
808
+ }
809
+ function ruleInput(rule) {
810
+ const spec = operatorSpec(rule.operator);
811
+ if (!spec) return [];
812
+ const input = { operator: spec.value };
813
+ if (rule.path !== void 0) input.path = rule.path;
814
+ if (rule.expected !== void 0) input.expected = jsonScalar(rule.expected);
815
+ if (rule.min !== void 0) input.min = rule.min;
816
+ if (rule.max !== void 0) input.max = rule.max;
817
+ if (rule.message !== void 0) input.message = rule.message;
818
+ return [input];
819
+ }
820
+ function expectationEntries(value) {
821
+ if (isRecord(value)) return Object.entries(value);
822
+ if (Array.isArray(value)) {
823
+ const entries = [];
824
+ for (const item of value) {
825
+ if (!isRecord(item)) return null;
826
+ entries.push([item.nodeId, item.rules]);
827
+ }
828
+ return entries;
829
+ }
830
+ return null;
831
+ }
832
+ function parseExpectations(text) {
833
+ const parsed = parseJson(text, "expectations", "Expectations");
834
+ if ("error" in parsed) return parsed;
835
+ if (parsed.value === void 0) return { value: [] };
836
+ const entries = expectationEntries(parsed.value);
837
+ if (!entries)
838
+ return fieldError(
839
+ "expectations",
840
+ "expectationsShape",
841
+ 'Expectations must be an object of { "nodeId": [rules] }.'
842
+ );
843
+ if (entries.length > MAX_CASE_ENTRIES)
844
+ return fieldError(
845
+ "expectations",
846
+ "expectationsTooMany",
847
+ `At most ${MAX_CASE_ENTRIES} checked nodes.`
848
+ );
849
+ const expectations = [];
850
+ for (const [nodeId, rules] of entries) {
851
+ if (typeof nodeId !== "string" || !nodeId || !Array.isArray(rules))
852
+ return fieldError(
853
+ "expectations",
854
+ "expectationsShape",
855
+ 'Expectations must be an object of { "nodeId": [rules] }.'
856
+ );
857
+ const result = normaliseAssertRules(rules);
858
+ if ("problem" in result) {
859
+ const problem = describeAssertRuleProblem(result.problem);
860
+ return {
861
+ error: {
862
+ field: "expectations",
863
+ key: problem.key,
864
+ fallback: `${nodeId}: ${problem.fallback}`
865
+ }
866
+ };
867
+ }
868
+ expectations.push({ nodeId, rules: result.rules.flatMap(ruleInput) });
869
+ }
870
+ return { value: expectations };
871
+ }
872
+ function draftToInput(draft, sortOrder) {
873
+ const name = draft.name.trim();
874
+ if (!name)
875
+ return fieldError("name", "nameRequired", "Give the test case a name.");
876
+ if (name.length > MAX_CASE_NAME)
877
+ return fieldError(
878
+ "name",
879
+ "nameTooLong",
880
+ `A name may have at most ${MAX_CASE_NAME} characters.`
881
+ );
882
+ const triggerData = parseTriggerData(draft.triggerData);
883
+ if ("error" in triggerData) return triggerData;
884
+ const pinnedOutputs = parsePinnedOutputs(draft.pinnedOutputs);
885
+ if ("error" in pinnedOutputs) return pinnedOutputs;
886
+ const connectionMocks = parseConnectionMocks(draft.connectionMocks);
887
+ if ("error" in connectionMocks) return connectionMocks;
888
+ const expectations = parseExpectations(draft.expectations);
889
+ if ("error" in expectations) return expectations;
890
+ const bytes = new TextEncoder().encode(
891
+ JSON.stringify([
892
+ triggerData.value,
893
+ pinnedOutputs.value,
894
+ connectionMocks.value,
895
+ expectations.value
896
+ ])
897
+ ).length;
898
+ if (bytes > MAX_CASE_JSON_BYTES)
899
+ return fieldError(
900
+ "size",
901
+ "tooLarge",
902
+ `The case holds ${Math.ceil(bytes / 1024)} KiB of JSON; the limit is ${MAX_CASE_JSON_BYTES / 1024} KiB.`
903
+ );
904
+ const description = draft.description.trim();
905
+ const input = {
906
+ name,
907
+ description: description || null,
908
+ triggerNodeId: draft.triggerNodeId || null,
909
+ triggerData: triggerData.value,
910
+ pinnedOutputs: pinnedOutputs.value,
911
+ connectionMocks: connectionMocks.value,
912
+ expectedStatus: draft.expectedStatus,
913
+ expectations: expectations.value,
914
+ enabled: draft.enabled
915
+ };
916
+ if (sortOrder !== void 0) input.sortOrder = sortOrder;
917
+ return { input };
918
+ }
919
+ function isTestRunOpen(status) {
920
+ return status === "PENDING" /* Pending */ || status === "RUNNING" /* Running */;
921
+ }
922
+ function testStatusTone(status) {
923
+ switch (status) {
924
+ case "PASSED" /* Passed */:
925
+ return "success";
926
+ case "FAILED" /* Failed */:
927
+ return "error";
928
+ case "ERROR" /* Error */:
929
+ return "warning";
930
+ default:
931
+ return "pending";
932
+ }
933
+ }
934
+ var TEST_STATUS_TONE_CLASS = {
935
+ success: "border-[var(--color-status-success-border)] bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)]",
936
+ error: "border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg-subtle)] text-[var(--color-status-error-text)]",
937
+ warning: "border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg-subtle)] text-[var(--color-status-warning-text)]",
938
+ pending: "border-border bg-muted text-muted-foreground"
939
+ };
940
+ function testStatusLabel(status) {
941
+ const fallback = {
942
+ ["PENDING" /* Pending */]: "Pending",
943
+ ["RUNNING" /* Running */]: "Running",
944
+ ["PASSED" /* Passed */]: "Passed",
945
+ ["FAILED" /* Failed */]: "Failed",
946
+ ["ERROR" /* Error */]: "Error"
947
+ };
948
+ return {
949
+ key: `fluidgrids.builder.workflowTests.status.${status.toLowerCase()}`,
950
+ fallback: fallback[status]
951
+ };
952
+ }
953
+ function testRunSummaryText(summary) {
954
+ const parts = [`${summary.passed} passed`];
955
+ if (summary.failed) parts.push(`${summary.failed} failed`);
956
+ if (summary.errored) parts.push(`${summary.errored} errored`);
957
+ if (summary.pending) parts.push(`${summary.pending} running`);
958
+ return `${parts.join(" \xB7 ")} of ${summary.total}`;
959
+ }
960
+ function enabledCaseIds(cases) {
961
+ return cases.filter((testCase) => testCase.enabled).map((c) => c.id);
962
+ }
963
+ function nextSortOrder(cases) {
964
+ return cases.reduce((max, c) => Math.max(max, c.sortOrder + 1), 0);
965
+ }
966
+ var SAVEABLE_RUN_STATUSES = /* @__PURE__ */ new Set([
967
+ "COMPLETED",
968
+ "FAILED"
969
+ ]);
970
+ function canSaveRunAsTestCase(run) {
971
+ return SAVEABLE_RUN_STATUSES.has(run.status) && !!run.workflowKey && !run.isDurable;
972
+ }
973
+ var TEST_RUN_POLL_INTERVAL_MS = 2e3;
974
+ var TEST_RUN_HISTORY_LIMIT = 10;
975
+ function messageOf2(error) {
976
+ return error instanceof Error ? error.message : String(error);
977
+ }
978
+ function upsertRun(runs, run) {
979
+ return [run, ...runs.filter((item) => item.id !== run.id)].slice(
980
+ 0,
981
+ TEST_RUN_HISTORY_LIMIT
982
+ );
983
+ }
984
+ function useWorkflowTests(workflowKey, enabled) {
985
+ const [cases, setCases] = useState([]);
986
+ const [runs, setRuns] = useState([]);
987
+ const [activeRunId, setActiveRunId] = useState(null);
988
+ const [loading, setLoading] = useState(false);
989
+ const [error, setError] = useState(null);
990
+ const [forbidden, setForbidden] = useState(false);
991
+ const [pollTick, setPollTick] = useState(0);
992
+ const keyRef = useRef(workflowKey);
993
+ const refetch = useCallback(async () => {
994
+ if (!enabled || !workflowKey) return;
995
+ setLoading(true);
996
+ setError(null);
997
+ try {
998
+ const client = getFluidGridsClient();
999
+ const [caseResult, runResult] = await Promise.all([
1000
+ client.query({
1001
+ query: WorkflowTestCasesDocument,
1002
+ variables: { workflowKey },
1003
+ fetchPolicy: "network-only"
1004
+ }),
1005
+ client.query({
1006
+ query: WorkflowTestRunsDocument,
1007
+ variables: { workflowKey, limit: TEST_RUN_HISTORY_LIMIT },
1008
+ fetchPolicy: "network-only"
1009
+ })
1010
+ ]);
1011
+ throwIfResultError(caseResult);
1012
+ throwIfResultError(runResult);
1013
+ if (keyRef.current !== workflowKey) return;
1014
+ setCases(caseResult.data?.workflowTestCases ?? []);
1015
+ setRuns(runResult.data?.workflowTestRuns ?? []);
1016
+ setForbidden(false);
1017
+ } catch (err) {
1018
+ if (keyRef.current !== workflowKey) return;
1019
+ if (isForbiddenError(err)) setForbidden(true);
1020
+ else setError(messageOf2(err));
1021
+ } finally {
1022
+ if (keyRef.current === workflowKey) setLoading(false);
1023
+ }
1024
+ }, [enabled, workflowKey]);
1025
+ useEffect(() => {
1026
+ keyRef.current = workflowKey;
1027
+ setCases([]);
1028
+ setRuns([]);
1029
+ setActiveRunId(null);
1030
+ setForbidden(false);
1031
+ setError(null);
1032
+ }, [workflowKey]);
1033
+ const activeRun = (activeRunId ? runs.find((run) => run.id === activeRunId) : runs[0]) ?? null;
1034
+ const followedId = activeRun && isTestRunOpen(activeRun.status) ? activeRun.id : null;
1035
+ useEffect(() => {
1036
+ if (!enabled || !followedId) return;
1037
+ let cancelled = false;
1038
+ const key = workflowKey;
1039
+ const timer = setTimeout(() => {
1040
+ void (async () => {
1041
+ try {
1042
+ const result = await getFluidGridsClient().query({
1043
+ query: WorkflowTestRunDocument,
1044
+ variables: { id: followedId },
1045
+ fetchPolicy: "network-only"
1046
+ });
1047
+ throwIfResultError(result);
1048
+ const run = result.data?.workflowTestRun;
1049
+ if (cancelled || keyRef.current !== key || !run) return;
1050
+ setRuns((current) => upsertRun(current, run));
1051
+ if (!isTestRunOpen(run.status)) await refetchCases(key);
1052
+ } catch (err) {
1053
+ if (!cancelled && keyRef.current === key) setError(messageOf2(err));
1054
+ } finally {
1055
+ if (!cancelled) setPollTick((tick) => tick + 1);
1056
+ }
1057
+ })();
1058
+ }, TEST_RUN_POLL_INTERVAL_MS);
1059
+ async function refetchCases(forKey) {
1060
+ const result = await getFluidGridsClient().query({
1061
+ query: WorkflowTestCasesDocument,
1062
+ variables: { workflowKey: forKey },
1063
+ fetchPolicy: "network-only"
1064
+ });
1065
+ throwIfResultError(result);
1066
+ if (!cancelled && keyRef.current === forKey)
1067
+ setCases(result.data?.workflowTestCases ?? []);
1068
+ }
1069
+ return () => {
1070
+ cancelled = true;
1071
+ clearTimeout(timer);
1072
+ };
1073
+ }, [enabled, followedId, workflowKey, pollTick]);
1074
+ const createCase = useCallback(
1075
+ async (input) => {
1076
+ const result = await getFluidGridsClient().mutate({
1077
+ mutation: CreateWorkflowTestCaseDocument,
1078
+ variables: { workflowKey, input }
1079
+ });
1080
+ throwIfResultError(result);
1081
+ const created = result.data?.createWorkflowTestCase;
1082
+ if (!created) throw new Error("The test case was not saved.");
1083
+ await refetch();
1084
+ return created;
1085
+ },
1086
+ [workflowKey, refetch]
1087
+ );
1088
+ const updateCase = useCallback(
1089
+ async (id, input) => {
1090
+ const result = await getFluidGridsClient().mutate({
1091
+ mutation: UpdateWorkflowTestCaseDocument,
1092
+ variables: { id, input }
1093
+ });
1094
+ throwIfResultError(result);
1095
+ const updated = result.data?.updateWorkflowTestCase;
1096
+ if (!updated) throw new Error("The test case was not saved.");
1097
+ await refetch();
1098
+ return updated;
1099
+ },
1100
+ [refetch]
1101
+ );
1102
+ const deleteCase = useCallback(
1103
+ async (id) => {
1104
+ const result = await getFluidGridsClient().mutate({
1105
+ mutation: DeleteWorkflowTestCaseDocument,
1106
+ variables: { id }
1107
+ });
1108
+ throwIfResultError(result);
1109
+ await refetch();
1110
+ },
1111
+ [refetch]
1112
+ );
1113
+ const runTests = useCallback(
1114
+ async (options = {}) => {
1115
+ const result = await getFluidGridsClient().mutate({
1116
+ mutation: RunWorkflowTestsDocument,
1117
+ variables: {
1118
+ input: {
1119
+ workflowKey,
1120
+ source: "UI" /* Ui */,
1121
+ ...options.version ? { version: options.version } : {},
1122
+ ...options.caseIds ? { caseIds: options.caseIds } : {}
1123
+ }
1124
+ }
1125
+ });
1126
+ throwIfResultError(result);
1127
+ const run = result.data?.runWorkflowTests;
1128
+ if (!run) throw new Error("The test run did not start.");
1129
+ setRuns((current) => upsertRun(current, run));
1130
+ setActiveRunId(run.id);
1131
+ return run;
1132
+ },
1133
+ [workflowKey]
1134
+ );
1135
+ return {
1136
+ cases,
1137
+ runs,
1138
+ activeRun,
1139
+ loading,
1140
+ error,
1141
+ forbidden,
1142
+ refetch,
1143
+ createCase,
1144
+ updateCase,
1145
+ deleteCase,
1146
+ runTests
1147
+ };
1148
+ }
1149
+ async function saveRunAsTestCase(runId, name, description) {
1150
+ const result = await getFluidGridsClient().mutate({
1151
+ mutation: CreateWorkflowTestCaseFromRunDocument,
1152
+ variables: {
1153
+ runId,
1154
+ name,
1155
+ ...description ? { description } : {}
1156
+ }
1157
+ });
1158
+ throwIfResultError(result);
1159
+ const created = result.data?.createWorkflowTestCaseFromRun;
1160
+ if (!created) throw new Error("The test case was not saved.");
1161
+ return created;
1162
+ }
1163
+
1164
+ export { EntityTagPicker, MAX_CASE_NAME, MAX_TEST_CASES, TEST_STATUS_TONE_CLASS, WorkflowProposalCard, canSaveRunAsTestCase, draftFromTestCase, draftToInput, emptyTestCaseDraft, enabledCaseIds, isTestRunOpen, nextSortOrder, pinnedOutputsFromCanvas, saveRunAsTestCase, testRunSummaryText, testStatusLabel, testStatusTone, useWorkflowProposal, useWorkflowTests };