stagent 0.9.5 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { BlueprintStepSchema } from "../blueprint";
|
|
3
|
+
|
|
4
|
+
// ── Task step fixtures ──────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const validTaskStep = {
|
|
7
|
+
name: "Research prospect",
|
|
8
|
+
profileId: "sales-researcher",
|
|
9
|
+
promptTemplate: "Research {{row.name}}",
|
|
10
|
+
requiresApproval: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// ── Delay step fixtures ─────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const validDelayStep = {
|
|
16
|
+
name: "Wait 3 days",
|
|
17
|
+
delayDuration: "3d",
|
|
18
|
+
requiresApproval: false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("BlueprintStepSchema", () => {
|
|
22
|
+
describe("task step (profile + prompt)", () => {
|
|
23
|
+
it("accepts a valid task step", () => {
|
|
24
|
+
const result = BlueprintStepSchema.safeParse(validTaskStep);
|
|
25
|
+
expect(result.success).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("accepts a task step with optional expectedOutput and condition", () => {
|
|
29
|
+
const result = BlueprintStepSchema.safeParse({
|
|
30
|
+
...validTaskStep,
|
|
31
|
+
expectedOutput: "structured-findings",
|
|
32
|
+
condition: "rowCount > 0",
|
|
33
|
+
});
|
|
34
|
+
expect(result.success).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects a task step missing profileId", () => {
|
|
38
|
+
const { profileId: _profileId, ...stepWithoutProfile } = validTaskStep;
|
|
39
|
+
const result = BlueprintStepSchema.safeParse(stepWithoutProfile);
|
|
40
|
+
expect(result.success).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("rejects a task step missing promptTemplate", () => {
|
|
44
|
+
const { promptTemplate: _promptTemplate, ...stepWithoutPrompt } = validTaskStep;
|
|
45
|
+
const result = BlueprintStepSchema.safeParse(stepWithoutPrompt);
|
|
46
|
+
expect(result.success).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("delay step (duration only)", () => {
|
|
51
|
+
it("accepts a valid delay step", () => {
|
|
52
|
+
const result = BlueprintStepSchema.safeParse(validDelayStep);
|
|
53
|
+
expect(result.success).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("accepts all valid duration formats", () => {
|
|
57
|
+
for (const duration of ["1m", "30m", "2h", "3d", "1w", "30d"]) {
|
|
58
|
+
const result = BlueprintStepSchema.safeParse({
|
|
59
|
+
...validDelayStep,
|
|
60
|
+
delayDuration: duration,
|
|
61
|
+
});
|
|
62
|
+
expect(result.success, `duration "${duration}" should validate`).toBe(true);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rejects a delay step with invalid duration format", () => {
|
|
67
|
+
const result = BlueprintStepSchema.safeParse({
|
|
68
|
+
...validDelayStep,
|
|
69
|
+
delayDuration: "bogus",
|
|
70
|
+
});
|
|
71
|
+
expect(result.success).toBe(false);
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
expect(JSON.stringify(result.error.issues)).toMatch(/30m, 2h, 3d, 1w/);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("rejects a delay step with duration below 1 minute", () => {
|
|
78
|
+
const result = BlueprintStepSchema.safeParse({
|
|
79
|
+
...validDelayStep,
|
|
80
|
+
delayDuration: "0m",
|
|
81
|
+
});
|
|
82
|
+
expect(result.success).toBe(false);
|
|
83
|
+
if (!result.success) {
|
|
84
|
+
expect(JSON.stringify(result.error.issues)).toMatch(/minimum/i);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("rejects a delay step with duration above 30 days", () => {
|
|
89
|
+
const result = BlueprintStepSchema.safeParse({
|
|
90
|
+
...validDelayStep,
|
|
91
|
+
delayDuration: "31d",
|
|
92
|
+
});
|
|
93
|
+
expect(result.success).toBe(false);
|
|
94
|
+
if (!result.success) {
|
|
95
|
+
expect(JSON.stringify(result.error.issues)).toMatch(/maximum/i);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("rejects compound duration formats", () => {
|
|
100
|
+
const result = BlueprintStepSchema.safeParse({
|
|
101
|
+
...validDelayStep,
|
|
102
|
+
delayDuration: "3d2h",
|
|
103
|
+
});
|
|
104
|
+
expect(result.success).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("XOR cross-field validation", () => {
|
|
109
|
+
it("rejects a step that mixes delayDuration with profileId", () => {
|
|
110
|
+
const result = BlueprintStepSchema.safeParse({
|
|
111
|
+
name: "Mixed",
|
|
112
|
+
profileId: "sales-researcher",
|
|
113
|
+
delayDuration: "3d",
|
|
114
|
+
requiresApproval: false,
|
|
115
|
+
});
|
|
116
|
+
expect(result.success).toBe(false);
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
expect(JSON.stringify(result.error.issues)).toMatch(/delay|task|both/i);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("rejects a step that mixes delayDuration with promptTemplate", () => {
|
|
123
|
+
const result = BlueprintStepSchema.safeParse({
|
|
124
|
+
name: "Mixed",
|
|
125
|
+
promptTemplate: "Do the thing",
|
|
126
|
+
delayDuration: "3d",
|
|
127
|
+
requiresApproval: false,
|
|
128
|
+
});
|
|
129
|
+
expect(result.success).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("rejects a step that mixes all three fields", () => {
|
|
133
|
+
const result = BlueprintStepSchema.safeParse({
|
|
134
|
+
name: "Mixed",
|
|
135
|
+
profileId: "sales-researcher",
|
|
136
|
+
promptTemplate: "Do the thing",
|
|
137
|
+
delayDuration: "3d",
|
|
138
|
+
requiresApproval: false,
|
|
139
|
+
});
|
|
140
|
+
expect(result.success).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("rejects a step with neither delay nor task fields", () => {
|
|
144
|
+
const result = BlueprintStepSchema.safeParse({
|
|
145
|
+
name: "Empty",
|
|
146
|
+
requiresApproval: false,
|
|
147
|
+
});
|
|
148
|
+
expect(result.success).toBe(false);
|
|
149
|
+
if (!result.success) {
|
|
150
|
+
expect(JSON.stringify(result.error.issues)).toMatch(/delay|profile|prompt|required/i);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("rejects a step with profileId but missing promptTemplate (partial task step)", () => {
|
|
155
|
+
const result = BlueprintStepSchema.safeParse({
|
|
156
|
+
name: "Partial",
|
|
157
|
+
profileId: "sales-researcher",
|
|
158
|
+
requiresApproval: false,
|
|
159
|
+
});
|
|
160
|
+
expect(result.success).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("rejects a step with promptTemplate but missing profileId (partial task step)", () => {
|
|
164
|
+
const result = BlueprintStepSchema.safeParse({
|
|
165
|
+
name: "Partial",
|
|
166
|
+
promptTemplate: "Do the thing",
|
|
167
|
+
requiresApproval: false,
|
|
168
|
+
});
|
|
169
|
+
expect(result.success).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -83,14 +83,24 @@ describe("updateAuthSettingsSchema", () => {
|
|
|
83
83
|
describe("updateOpenAISettingsSchema", () => {
|
|
84
84
|
it("accepts valid OpenAI API keys", () => {
|
|
85
85
|
const result = updateOpenAISettingsSchema.safeParse({
|
|
86
|
+
method: "api_key",
|
|
86
87
|
apiKey: "sk-test-openai",
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
expect(result.success).toBe(true);
|
|
90
91
|
});
|
|
91
92
|
|
|
93
|
+
it("accepts oauth mode without an API key", () => {
|
|
94
|
+
const result = updateOpenAISettingsSchema.safeParse({
|
|
95
|
+
method: "oauth",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.success).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
it("rejects keys without the sk- prefix", () => {
|
|
93
102
|
const result = updateOpenAISettingsSchema.safeParse({
|
|
103
|
+
method: "api_key",
|
|
94
104
|
apiKey: "invalid",
|
|
95
105
|
});
|
|
96
106
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { parseDuration } from "@/lib/workflows/delay";
|
|
2
3
|
|
|
3
4
|
export const BlueprintVariableSchema = z.object({
|
|
4
5
|
id: z.string(),
|
|
@@ -15,14 +16,74 @@ export const BlueprintVariableSchema = z.object({
|
|
|
15
16
|
max: z.number().optional(),
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
/**
|
|
20
|
+
* A blueprint step is either a task step (profileId + promptTemplate) OR a
|
|
21
|
+
* delay step (delayDuration). The discriminator lives in the cross-field
|
|
22
|
+
* refinement below — we can't use Zod's discriminatedUnion directly because
|
|
23
|
+
* YAML blueprints don't carry an explicit `type` field, and we want validation
|
|
24
|
+
* errors to point at the actual offending field, not a missing discriminator.
|
|
25
|
+
*
|
|
26
|
+
* See features/workflow-step-delays.md for the XOR rule rationale.
|
|
27
|
+
*/
|
|
28
|
+
export const BlueprintStepSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
name: z.string(),
|
|
31
|
+
profileId: z.string().optional(),
|
|
32
|
+
promptTemplate: z.string().optional(),
|
|
33
|
+
delayDuration: z.string().optional(),
|
|
34
|
+
requiresApproval: z.boolean(),
|
|
35
|
+
expectedOutput: z.string().optional(),
|
|
36
|
+
condition: z.string().optional(),
|
|
37
|
+
})
|
|
38
|
+
.superRefine((step, ctx) => {
|
|
39
|
+
const hasDelay = step.delayDuration != null;
|
|
40
|
+
const hasProfile = step.profileId != null;
|
|
41
|
+
const hasPrompt = step.promptTemplate != null;
|
|
42
|
+
const hasAnyTaskField = hasProfile || hasPrompt;
|
|
43
|
+
|
|
44
|
+
// XOR: exactly one of (delay step) or (task step) must be present.
|
|
45
|
+
if (hasDelay && hasAnyTaskField) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: "custom",
|
|
48
|
+
path: ["delayDuration"],
|
|
49
|
+
message:
|
|
50
|
+
"Step cannot be both a delay and a task: remove delayDuration, or remove profileId/promptTemplate.",
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hasDelay) {
|
|
56
|
+
// Delay step: validate the duration string parses and is within bounds.
|
|
57
|
+
try {
|
|
58
|
+
parseDuration(step.delayDuration as string);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: "custom",
|
|
62
|
+
path: ["delayDuration"],
|
|
63
|
+
message: err instanceof Error ? err.message : String(err),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Task step: profileId AND promptTemplate are both required.
|
|
70
|
+
if (!hasProfile) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: "custom",
|
|
73
|
+
path: ["profileId"],
|
|
74
|
+
message:
|
|
75
|
+
"Task step requires profileId. For a delay step, set delayDuration instead (e.g. '3d').",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!hasPrompt) {
|
|
79
|
+
ctx.addIssue({
|
|
80
|
+
code: "custom",
|
|
81
|
+
path: ["promptTemplate"],
|
|
82
|
+
message:
|
|
83
|
+
"Task step requires promptTemplate. For a delay step, set delayDuration instead (e.g. '3d').",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
26
87
|
|
|
27
88
|
export const BlueprintSchema = z.object({
|
|
28
89
|
id: z.string(),
|
|
@@ -35,7 +96,7 @@ export const BlueprintSchema = z.object({
|
|
|
35
96
|
variables: z.array(BlueprintVariableSchema),
|
|
36
97
|
steps: z.array(BlueprintStepSchema).min(1),
|
|
37
98
|
author: z.string().optional(),
|
|
38
|
-
source: z.
|
|
99
|
+
source: z.url().optional(),
|
|
39
100
|
estimatedDuration: z.string().optional(),
|
|
40
101
|
difficulty: z.enum(["beginner", "intermediate", "advanced"]).optional(),
|
|
41
102
|
});
|
|
@@ -33,7 +33,7 @@ const profileRuntimeCapabilityOverrideSchema = z.object({
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
export const importMetaSchema = z.object({
|
|
36
|
-
repoUrl: z.
|
|
36
|
+
repoUrl: z.url(),
|
|
37
37
|
repoOwner: z.string(),
|
|
38
38
|
repoName: z.string(),
|
|
39
39
|
branch: z.string(),
|
|
@@ -64,7 +64,7 @@ export const ProfileConfigSchema = z.object({
|
|
|
64
64
|
maxTurns: z.number().positive().optional(),
|
|
65
65
|
outputFormat: z.string().optional(),
|
|
66
66
|
author: z.string().optional(),
|
|
67
|
-
source: z.
|
|
67
|
+
source: z.url().optional(),
|
|
68
68
|
tests: profileTestsSchema.optional(),
|
|
69
69
|
importMeta: importMetaSchema.optional(),
|
|
70
70
|
supportedRuntimes: z.array(runtimeIdSchema).optional(),
|
|
@@ -12,9 +12,11 @@ export const updateAuthSettingsSchema = z.object({
|
|
|
12
12
|
export type UpdateAuthSettingsInput = z.infer<typeof updateAuthSettingsSchema>;
|
|
13
13
|
|
|
14
14
|
export const updateOpenAISettingsSchema = z.object({
|
|
15
|
+
method: z.enum(["api_key", "oauth"]),
|
|
15
16
|
apiKey: z
|
|
16
17
|
.string()
|
|
17
|
-
.startsWith("sk-", "API key must start with sk-")
|
|
18
|
+
.startsWith("sk-", "API key must start with sk-")
|
|
19
|
+
.optional(),
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
export type UpdateOpenAISettingsInput = z.infer<typeof updateOpenAISettingsSchema>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseDuration, formatDuration, checkDelayStep } from "../delay";
|
|
3
|
+
|
|
4
|
+
describe("parseDuration", () => {
|
|
5
|
+
describe("valid inputs", () => {
|
|
6
|
+
it("parses minutes", () => {
|
|
7
|
+
expect(parseDuration("1m")).toBe(60_000);
|
|
8
|
+
expect(parseDuration("30m")).toBe(30 * 60_000);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("parses hours", () => {
|
|
12
|
+
expect(parseDuration("1h")).toBe(60 * 60_000);
|
|
13
|
+
expect(parseDuration("2h")).toBe(2 * 60 * 60_000);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("parses days", () => {
|
|
17
|
+
expect(parseDuration("1d")).toBe(24 * 60 * 60_000);
|
|
18
|
+
expect(parseDuration("3d")).toBe(3 * 24 * 60 * 60_000);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("parses weeks", () => {
|
|
22
|
+
expect(parseDuration("1w")).toBe(7 * 24 * 60 * 60_000);
|
|
23
|
+
expect(parseDuration("2w")).toBe(14 * 24 * 60 * 60_000);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("bounds", () => {
|
|
28
|
+
it("accepts minimum of 1 minute", () => {
|
|
29
|
+
expect(parseDuration("1m")).toBe(60_000);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("accepts maximum of 30 days", () => {
|
|
33
|
+
expect(parseDuration("30d")).toBe(30 * 24 * 60 * 60_000);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("rejects durations below 1 minute", () => {
|
|
37
|
+
// Note: "0m" is syntactically valid but below minimum
|
|
38
|
+
expect(() => parseDuration("0m")).toThrow(/minimum/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects durations above 30 days", () => {
|
|
42
|
+
expect(() => parseDuration("31d")).toThrow(/maximum/i);
|
|
43
|
+
expect(() => parseDuration("5w")).toThrow(/maximum/i);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("invalid formats", () => {
|
|
48
|
+
it("rejects empty string", () => {
|
|
49
|
+
expect(() => parseDuration("")).toThrow(/invalid duration/i);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects missing unit", () => {
|
|
53
|
+
expect(() => parseDuration("30")).toThrow(/invalid duration/i);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("rejects missing number", () => {
|
|
57
|
+
expect(() => parseDuration("m")).toThrow(/invalid duration/i);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects unsupported units", () => {
|
|
61
|
+
expect(() => parseDuration("30s")).toThrow(/invalid duration/i);
|
|
62
|
+
expect(() => parseDuration("1y")).toThrow(/invalid duration/i);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("rejects compound durations", () => {
|
|
66
|
+
expect(() => parseDuration("3d2h")).toThrow(/invalid duration/i);
|
|
67
|
+
expect(() => parseDuration("1h30m")).toThrow(/invalid duration/i);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rejects decimal values", () => {
|
|
71
|
+
expect(() => parseDuration("1.5h")).toThrow(/invalid duration/i);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects negative values", () => {
|
|
75
|
+
expect(() => parseDuration("-1h")).toThrow(/invalid duration/i);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("rejects whitespace", () => {
|
|
79
|
+
expect(() => parseDuration(" 1h")).toThrow(/invalid duration/i);
|
|
80
|
+
expect(() => parseDuration("1h ")).toThrow(/invalid duration/i);
|
|
81
|
+
expect(() => parseDuration("1 h")).toThrow(/invalid duration/i);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects uppercase units", () => {
|
|
85
|
+
// Strict format — users must use lowercase per the pattern hint
|
|
86
|
+
expect(() => parseDuration("1H")).toThrow(/invalid duration/i);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("error messages", () => {
|
|
91
|
+
it("includes format hint in invalid-format errors", () => {
|
|
92
|
+
expect(() => parseDuration("bogus")).toThrow(/30m, 2h, 3d, 1w/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("includes the minimum bound in too-small errors", () => {
|
|
96
|
+
expect(() => parseDuration("0m")).toThrow(/1 minute|1m/i);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("includes the maximum bound in too-large errors", () => {
|
|
100
|
+
expect(() => parseDuration("31d")).toThrow(/30 day|30d/i);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("formatDuration", () => {
|
|
106
|
+
it("formats exact minutes", () => {
|
|
107
|
+
expect(formatDuration(60_000)).toBe("1m");
|
|
108
|
+
expect(formatDuration(30 * 60_000)).toBe("30m");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("formats exact hours", () => {
|
|
112
|
+
expect(formatDuration(60 * 60_000)).toBe("1h");
|
|
113
|
+
expect(formatDuration(2 * 60 * 60_000)).toBe("2h");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("formats exact days", () => {
|
|
117
|
+
expect(formatDuration(24 * 60 * 60_000)).toBe("1d");
|
|
118
|
+
expect(formatDuration(3 * 24 * 60 * 60_000)).toBe("3d");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("formats exact weeks", () => {
|
|
122
|
+
expect(formatDuration(7 * 24 * 60 * 60_000)).toBe("1w");
|
|
123
|
+
expect(formatDuration(2 * 7 * 24 * 60 * 60_000)).toBe("2w");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("prefers the largest exact unit", () => {
|
|
127
|
+
// 7 days = 1w, not 7d
|
|
128
|
+
expect(formatDuration(7 * 24 * 60 * 60_000)).toBe("1w");
|
|
129
|
+
// 24 hours = 1d, not 24h
|
|
130
|
+
expect(formatDuration(24 * 60 * 60_000)).toBe("1d");
|
|
131
|
+
// 60 minutes = 1h, not 60m
|
|
132
|
+
expect(formatDuration(60 * 60_000)).toBe("1h");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("falls back to minutes when no larger unit divides cleanly", () => {
|
|
136
|
+
// 90 minutes — not a clean hour
|
|
137
|
+
expect(formatDuration(90 * 60_000)).toBe("90m");
|
|
138
|
+
// 95 minutes — not a clean hour, day, or week
|
|
139
|
+
expect(formatDuration(95 * 60_000)).toBe("95m");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("uses hour level when minutes divide cleanly into hours but not days", () => {
|
|
143
|
+
// 25 hours — divides cleanly as hours, but not as a day
|
|
144
|
+
expect(formatDuration(25 * 60 * 60_000)).toBe("25h");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("roundtrips with parseDuration for canonical forms", () => {
|
|
148
|
+
const canonical = ["1m", "30m", "1h", "2h", "1d", "3d", "1w", "2w", "30d"];
|
|
149
|
+
for (const input of canonical) {
|
|
150
|
+
expect(formatDuration(parseDuration(input))).toBe(input);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("checkDelayStep", () => {
|
|
156
|
+
const now = 1_700_000_000_000; // fixed epoch ms for deterministic tests
|
|
157
|
+
|
|
158
|
+
it("returns type 'task' for a step without delayDuration", () => {
|
|
159
|
+
expect(checkDelayStep({}, now)).toEqual({ type: "task" });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("returns type 'task' when delayDuration is undefined explicitly", () => {
|
|
163
|
+
expect(checkDelayStep({ delayDuration: undefined }, now)).toEqual({ type: "task" });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns type 'delay' with resumeAt = now + duration for a valid delay", () => {
|
|
167
|
+
const result = checkDelayStep({ delayDuration: "3d" }, now);
|
|
168
|
+
expect(result).toEqual({
|
|
169
|
+
type: "delay",
|
|
170
|
+
resumeAt: now + 3 * 24 * 60 * 60_000,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("computes resumeAt correctly for each unit", () => {
|
|
175
|
+
expect(checkDelayStep({ delayDuration: "30m" }, now)).toEqual({
|
|
176
|
+
type: "delay",
|
|
177
|
+
resumeAt: now + 30 * 60_000,
|
|
178
|
+
});
|
|
179
|
+
expect(checkDelayStep({ delayDuration: "2h" }, now)).toEqual({
|
|
180
|
+
type: "delay",
|
|
181
|
+
resumeAt: now + 2 * 60 * 60_000,
|
|
182
|
+
});
|
|
183
|
+
expect(checkDelayStep({ delayDuration: "1w" }, now)).toEqual({
|
|
184
|
+
type: "delay",
|
|
185
|
+
resumeAt: now + 7 * 24 * 60 * 60_000,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("throws when delayDuration is present but invalid (boundary responsibility)", () => {
|
|
190
|
+
// The engine should call checkDelayStep AFTER blueprint validation, so invalid
|
|
191
|
+
// formats reaching here are a programming error and must fail loudly.
|
|
192
|
+
expect(() => checkDelayStep({ delayDuration: "bogus" }, now)).toThrow(
|
|
193
|
+
/invalid duration/i,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -46,16 +46,24 @@ vi.mock("@/lib/db", () => ({
|
|
|
46
46
|
vi.mock("@/lib/db/schema", () => ({
|
|
47
47
|
workflows: {
|
|
48
48
|
id: "workflows.id",
|
|
49
|
+
status: "workflows.status",
|
|
49
50
|
},
|
|
50
51
|
tasks: {
|
|
51
52
|
id: "tasks.id",
|
|
52
53
|
},
|
|
53
54
|
agentLogs: {},
|
|
54
55
|
notifications: {},
|
|
56
|
+
// usageLedger is read by execution-stats.ts in the finally-block fire-and-forget.
|
|
57
|
+
// Without this export the import throws, cascading through the catch handler
|
|
58
|
+
// and causing downstream updateWorkflowState reads to run out of mock entries.
|
|
59
|
+
usageLedger: {
|
|
60
|
+
workflowId: "usageLedger.workflowId",
|
|
61
|
+
},
|
|
55
62
|
}));
|
|
56
63
|
|
|
57
64
|
vi.mock("drizzle-orm", () => ({
|
|
58
65
|
eq: vi.fn((column: string, value: unknown) => ({ column, value })),
|
|
66
|
+
and: vi.fn((...conditions: unknown[]) => ({ and: conditions })),
|
|
59
67
|
}));
|
|
60
68
|
|
|
61
69
|
vi.mock("@/lib/agents/runtime", () => ({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
buildIterationPrompt,
|
|
4
|
+
buildRowIterationPrompt,
|
|
4
5
|
detectCompletionSignal,
|
|
5
6
|
} from "../loop-executor";
|
|
6
7
|
|
|
@@ -52,3 +53,56 @@ describe("detectCompletionSignal", () => {
|
|
|
52
53
|
expect(detectCompletionSignal("LOOP_COMPLETE", [])).toBe(true);
|
|
53
54
|
});
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
describe("buildRowIterationPrompt", () => {
|
|
58
|
+
it("renders the iteration header using row index out of total rows", () => {
|
|
59
|
+
const result = buildRowIterationPrompt(
|
|
60
|
+
"Research {{row}}",
|
|
61
|
+
{ name: "Alice", company: "Acme" },
|
|
62
|
+
"row",
|
|
63
|
+
1,
|
|
64
|
+
3
|
|
65
|
+
);
|
|
66
|
+
expect(result).toContain("Row 1 of 3.");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes the bound row data so the agent can read it", () => {
|
|
70
|
+
const row = { name: "Alice", company: "Acme" };
|
|
71
|
+
const result = buildRowIterationPrompt(
|
|
72
|
+
"Find LinkedIn for the contact above",
|
|
73
|
+
row,
|
|
74
|
+
"row",
|
|
75
|
+
1,
|
|
76
|
+
3
|
|
77
|
+
);
|
|
78
|
+
// Row payload must be visible in the prompt under the bound name
|
|
79
|
+
expect(result).toContain("row");
|
|
80
|
+
expect(result).toContain("Alice");
|
|
81
|
+
expect(result).toContain("Acme");
|
|
82
|
+
expect(result).toContain("Find LinkedIn for the contact above");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("respects a custom itemVariable name", () => {
|
|
86
|
+
const result = buildRowIterationPrompt(
|
|
87
|
+
"Process item",
|
|
88
|
+
{ id: 42, label: "widget" },
|
|
89
|
+
"contact",
|
|
90
|
+
2,
|
|
91
|
+
5
|
|
92
|
+
);
|
|
93
|
+
expect(result).toContain("contact");
|
|
94
|
+
expect(result).toContain("widget");
|
|
95
|
+
expect(result).toContain("Row 2 of 5.");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("does not append the LOOP_COMPLETE instruction (row-driven loops finish when items are exhausted)", () => {
|
|
99
|
+
const result = buildRowIterationPrompt(
|
|
100
|
+
"Do work",
|
|
101
|
+
{ x: 1 },
|
|
102
|
+
"row",
|
|
103
|
+
1,
|
|
104
|
+
1
|
|
105
|
+
);
|
|
106
|
+
expect(result).not.toContain("LOOP_COMPLETE");
|
|
107
|
+
});
|
|
108
|
+
});
|