stagent 0.1.11 → 0.1.13

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 (145) hide show
  1. package/README.md +74 -49
  2. package/package.json +3 -2
  3. package/public/readme/cost-usage-list.png +0 -0
  4. package/public/readme/dashboard-bulk-select.png +0 -0
  5. package/public/readme/dashboard-card-edit.png +0 -0
  6. package/public/readme/dashboard-create-form-ai-applied.png +0 -0
  7. package/public/readme/dashboard-create-form-ai-assist.png +0 -0
  8. package/public/readme/dashboard-create-form-empty.png +0 -0
  9. package/public/readme/dashboard-create-form-filled.png +0 -0
  10. package/public/readme/dashboard-filtered.png +0 -0
  11. package/public/readme/dashboard-list.png +0 -0
  12. package/public/readme/dashboard-workflow-confirm.png +0 -0
  13. package/public/readme/home-below-fold.png +0 -0
  14. package/public/readme/home-list.png +0 -0
  15. package/public/readme/inbox-list.png +0 -0
  16. package/public/readme/playbook-list.png +0 -0
  17. package/public/readme/profiles-list.png +0 -0
  18. package/public/readme/settings-list.png +0 -0
  19. package/public/readme/workflows-list.png +0 -0
  20. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  21. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  22. package/src/__tests__/e2e/helpers.ts +286 -0
  23. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  24. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  25. package/src/__tests__/e2e/setup.ts +156 -0
  26. package/src/__tests__/e2e/single-task.test.ts +170 -0
  27. package/src/app/api/command-palette/recent/route.ts +41 -18
  28. package/src/app/api/context/batch/route.ts +44 -0
  29. package/src/app/api/permissions/presets/route.ts +80 -0
  30. package/src/app/api/playbook/status/route.ts +15 -0
  31. package/src/app/api/profiles/route.ts +23 -20
  32. package/src/app/api/settings/pricing/route.ts +15 -0
  33. package/src/app/api/tasks/[id]/route.ts +54 -3
  34. package/src/app/api/workflows/[id]/route.ts +43 -4
  35. package/src/app/api/workflows/[id]/status/route.ts +70 -2
  36. package/src/app/api/workflows/from-assist/route.ts +6 -32
  37. package/src/app/costs/page.tsx +53 -43
  38. package/src/app/dashboard/page.tsx +59 -21
  39. package/src/app/documents/[id]/page.tsx +10 -8
  40. package/src/app/globals.css +11 -0
  41. package/src/app/page.tsx +60 -3
  42. package/src/app/playbook/[slug]/page.tsx +76 -0
  43. package/src/app/playbook/page.tsx +54 -0
  44. package/src/app/profiles/page.tsx +7 -4
  45. package/src/app/settings/page.tsx +2 -2
  46. package/src/app/tasks/[id]/page.tsx +22 -2
  47. package/src/components/costs/cost-dashboard.tsx +226 -320
  48. package/src/components/dashboard/activity-feed.tsx +6 -2
  49. package/src/components/dashboard/greeting.tsx +3 -1
  50. package/src/components/dashboard/priority-queue.tsx +58 -9
  51. package/src/components/dashboard/stats-cards.tsx +16 -2
  52. package/src/components/documents/document-chip-bar.tsx +183 -0
  53. package/src/components/documents/document-content-renderer.tsx +146 -0
  54. package/src/components/documents/document-detail-view.tsx +16 -239
  55. package/src/components/documents/image-zoom-view.tsx +60 -0
  56. package/src/components/documents/smart-extracted-text.tsx +47 -0
  57. package/src/components/documents/utils.ts +70 -0
  58. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  59. package/src/components/notifications/inbox-list.tsx +4 -5
  60. package/src/components/notifications/notification-item.tsx +73 -6
  61. package/src/components/notifications/pending-approval-host.tsx +63 -14
  62. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  63. package/src/components/playbook/journey-card.tsx +110 -0
  64. package/src/components/playbook/playbook-action-button.tsx +22 -0
  65. package/src/components/playbook/playbook-browser.tsx +143 -0
  66. package/src/components/playbook/playbook-card.tsx +102 -0
  67. package/src/components/playbook/playbook-detail-view.tsx +225 -0
  68. package/src/components/playbook/playbook-homepage.tsx +142 -0
  69. package/src/components/playbook/playbook-toc.tsx +90 -0
  70. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  71. package/src/components/playbook/related-docs.tsx +30 -0
  72. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  73. package/src/components/profiles/context-proposal-review.tsx +7 -3
  74. package/src/components/profiles/learned-context-panel.tsx +116 -8
  75. package/src/components/profiles/profile-browser.tsx +1 -0
  76. package/src/components/profiles/profile-card.tsx +16 -8
  77. package/src/components/profiles/profile-detail-view.tsx +12 -4
  78. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  79. package/src/components/settings/api-key-form.tsx +5 -43
  80. package/src/components/settings/auth-config-section.tsx +10 -6
  81. package/src/components/settings/auth-status-badge.tsx +8 -0
  82. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  83. package/src/components/settings/connection-test-control.tsx +63 -0
  84. package/src/components/settings/permissions-section.tsx +85 -75
  85. package/src/components/settings/permissions-sections.tsx +24 -0
  86. package/src/components/settings/presets-section.tsx +159 -0
  87. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  88. package/src/components/shared/app-sidebar.tsx +4 -2
  89. package/src/components/shared/command-palette.tsx +30 -0
  90. package/src/components/shared/light-markdown.tsx +134 -0
  91. package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
  92. package/src/components/tasks/ai-assist-panel.tsx +108 -78
  93. package/src/components/tasks/content-preview.tsx +2 -1
  94. package/src/components/tasks/kanban-board.tsx +57 -5
  95. package/src/components/tasks/kanban-column.tsx +34 -23
  96. package/src/components/tasks/task-bento-cell.tsx +50 -0
  97. package/src/components/tasks/task-bento-grid.tsx +155 -0
  98. package/src/components/tasks/task-card.tsx +14 -16
  99. package/src/components/tasks/task-chip-bar.tsx +207 -0
  100. package/src/components/tasks/task-detail-view.tsx +42 -190
  101. package/src/components/tasks/task-result-renderer.tsx +33 -0
  102. package/src/components/workflows/blueprint-gallery.tsx +19 -12
  103. package/src/components/workflows/blueprint-preview.tsx +8 -1
  104. package/src/components/workflows/loop-status-view.tsx +2 -4
  105. package/src/components/workflows/swarm-dashboard.tsx +2 -3
  106. package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
  107. package/src/components/workflows/workflow-full-output.tsx +80 -0
  108. package/src/components/workflows/workflow-kanban-card.tsx +121 -0
  109. package/src/components/workflows/workflow-list.tsx +47 -42
  110. package/src/components/workflows/workflow-status-view.tsx +163 -16
  111. package/src/lib/agents/learned-context.ts +27 -15
  112. package/src/lib/agents/learning-session.ts +354 -0
  113. package/src/lib/agents/pattern-extractor.ts +19 -0
  114. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  115. package/src/lib/agents/profiles/sort.ts +7 -0
  116. package/src/lib/constants/card-icons.tsx +202 -0
  117. package/src/lib/constants/prose-styles.ts +7 -0
  118. package/src/lib/constants/settings.ts +1 -0
  119. package/src/lib/constants/task-status.ts +3 -0
  120. package/src/lib/db/schema.ts +3 -0
  121. package/src/lib/docs/adoption.ts +105 -0
  122. package/src/lib/docs/journey-tracker.ts +21 -0
  123. package/src/lib/docs/reader.ts +107 -0
  124. package/src/lib/docs/types.ts +54 -0
  125. package/src/lib/docs/usage-stage.ts +60 -0
  126. package/src/lib/documents/context-builder.ts +41 -0
  127. package/src/lib/notifications/actionable.ts +18 -10
  128. package/src/lib/queries/chart-data.ts +20 -1
  129. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  130. package/src/lib/settings/budget-guardrails.ts +213 -85
  131. package/src/lib/settings/permission-presets.ts +150 -0
  132. package/src/lib/settings/runtime-setup.ts +71 -0
  133. package/src/lib/usage/__tests__/ledger.test.ts +2 -2
  134. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  135. package/src/lib/usage/ledger.ts +1 -1
  136. package/src/lib/usage/pricing-registry.ts +570 -0
  137. package/src/lib/usage/pricing.ts +15 -95
  138. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  139. package/src/lib/utils/learned-context-history.ts +150 -0
  140. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  141. package/src/lib/validators/settings.ts +3 -9
  142. package/src/lib/workflows/engine.ts +75 -61
  143. package/src/lib/workflows/types.ts +2 -0
  144. package/tsconfig.json +2 -1
  145. package/src/components/documents/document-preview.tsx +0 -68
@@ -0,0 +1,286 @@
1
+ /**
2
+ * E2E test helpers — HTTP client utilities for calling Stagent API endpoints.
3
+ *
4
+ * These helpers call the live Next.js dev/prod server. The base URL defaults
5
+ * to http://localhost:3000 and can be overridden via E2E_BASE_URL env var.
6
+ */
7
+
8
+ export const BASE_URL = process.env.E2E_BASE_URL ?? "http://localhost:3000";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Generic fetch wrapper
12
+ // ---------------------------------------------------------------------------
13
+
14
+ interface ApiResponse<T = unknown> {
15
+ status: number;
16
+ ok: boolean;
17
+ data: T;
18
+ }
19
+
20
+ async function api<T = unknown>(
21
+ path: string,
22
+ options?: RequestInit
23
+ ): Promise<ApiResponse<T>> {
24
+ const res = await fetch(`${BASE_URL}${path}`, {
25
+ ...options,
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ ...options?.headers,
29
+ },
30
+ });
31
+ const data = (await res.json().catch(() => null)) as T;
32
+ return { status: res.status, ok: res.ok, data };
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Projects
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export interface ProjectPayload {
40
+ name: string;
41
+ description?: string;
42
+ workingDirectory?: string;
43
+ }
44
+
45
+ export async function createProject(payload: ProjectPayload) {
46
+ return api<{ id: string; name: string }>(
47
+ "/api/projects",
48
+ { method: "POST", body: JSON.stringify(payload) }
49
+ );
50
+ }
51
+
52
+ export async function deleteProject(id: string) {
53
+ return api(`/api/projects/${id}`, { method: "DELETE" });
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Tasks
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export interface TaskPayload {
61
+ title: string;
62
+ description?: string;
63
+ projectId?: string;
64
+ priority?: number;
65
+ assignedAgent?: string;
66
+ agentProfile?: string;
67
+ }
68
+
69
+ export interface TaskRow {
70
+ id: string;
71
+ title: string;
72
+ status: string;
73
+ result: string | null;
74
+ assignedAgent: string | null;
75
+ agentProfile: string | null;
76
+ projectId: string | null;
77
+ createdAt: string;
78
+ updatedAt: string;
79
+ }
80
+
81
+ export async function createTask(payload: TaskPayload) {
82
+ return api<TaskRow>("/api/tasks", {
83
+ method: "POST",
84
+ body: JSON.stringify(payload),
85
+ });
86
+ }
87
+
88
+ export async function getTask(id: string) {
89
+ return api<TaskRow>(`/api/tasks/${id}`);
90
+ }
91
+
92
+ export async function updateTask(id: string, payload: Partial<TaskPayload & { status: string }>) {
93
+ return api<TaskRow>(`/api/tasks/${id}`, {
94
+ method: "PATCH",
95
+ body: JSON.stringify(payload),
96
+ });
97
+ }
98
+
99
+ export async function deleteTask(id: string) {
100
+ return api(`/api/tasks/${id}`, { method: "DELETE" });
101
+ }
102
+
103
+ export async function executeTask(id: string) {
104
+ return api<{ message: string }>(`/api/tasks/${id}/execute`, {
105
+ method: "POST",
106
+ });
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Workflows
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export interface WorkflowStep {
114
+ id: string;
115
+ name: string;
116
+ prompt: string;
117
+ requiresApproval?: boolean;
118
+ dependsOn?: string[];
119
+ assignedAgent?: string;
120
+ agentProfile?: string;
121
+ }
122
+
123
+ export interface WorkflowPayload {
124
+ name: string;
125
+ projectId?: string;
126
+ definition: {
127
+ pattern: string;
128
+ steps: WorkflowStep[];
129
+ };
130
+ }
131
+
132
+ export interface WorkflowRow {
133
+ id: string;
134
+ name: string;
135
+ status: string;
136
+ definition: string;
137
+ projectId: string | null;
138
+ createdAt: string;
139
+ updatedAt: string;
140
+ }
141
+
142
+ export async function createWorkflow(payload: WorkflowPayload) {
143
+ return api<WorkflowRow>("/api/workflows", {
144
+ method: "POST",
145
+ body: JSON.stringify(payload),
146
+ });
147
+ }
148
+
149
+ export async function getWorkflow(id: string) {
150
+ return api<WorkflowRow>(`/api/workflows/${id}`);
151
+ }
152
+
153
+ export async function executeWorkflow(id: string) {
154
+ return api<{ status: string; workflowId: string }>(
155
+ `/api/workflows/${id}/execute`,
156
+ { method: "POST" }
157
+ );
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Blueprints
162
+ // ---------------------------------------------------------------------------
163
+
164
+ export async function listBlueprints() {
165
+ return api<unknown[]>("/api/blueprints");
166
+ }
167
+
168
+ export async function instantiateBlueprint(
169
+ blueprintId: string,
170
+ variables: Record<string, string>,
171
+ projectId?: string
172
+ ) {
173
+ return api<{ workflow: WorkflowRow }>(
174
+ `/api/blueprints/${blueprintId}/instantiate`,
175
+ {
176
+ method: "POST",
177
+ body: JSON.stringify({ variables, projectId }),
178
+ }
179
+ );
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Profiles
184
+ // ---------------------------------------------------------------------------
185
+
186
+ export async function listProfiles() {
187
+ return api<unknown[]>("/api/profiles");
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Runtime connectivity
192
+ // ---------------------------------------------------------------------------
193
+
194
+ export async function checkRuntimeConnectivity(runtimeId: string) {
195
+ return api<{ connected: boolean; method?: string }>(
196
+ `/api/settings/connectivity?runtime=${runtimeId}`
197
+ );
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Polling helpers
202
+ // ---------------------------------------------------------------------------
203
+
204
+ const POLL_INTERVAL_MS = 3_000;
205
+ const DEFAULT_TIMEOUT_MS = 120_000;
206
+
207
+ const TERMINAL_TASK_STATUSES = new Set([
208
+ "completed",
209
+ "failed",
210
+ "cancelled",
211
+ ]);
212
+
213
+ const TERMINAL_WORKFLOW_STATUSES = new Set([
214
+ "completed",
215
+ "failed",
216
+ "cancelled",
217
+ ]);
218
+
219
+ export async function pollTaskUntilDone(
220
+ taskId: string,
221
+ timeoutMs = DEFAULT_TIMEOUT_MS
222
+ ): Promise<TaskRow> {
223
+ const start = Date.now();
224
+ while (Date.now() - start < timeoutMs) {
225
+ const { data } = await getTask(taskId);
226
+ if (data && TERMINAL_TASK_STATUSES.has(data.status)) {
227
+ return data;
228
+ }
229
+ await sleep(POLL_INTERVAL_MS);
230
+ }
231
+ throw new Error(
232
+ `Task ${taskId} did not reach a terminal status within ${timeoutMs}ms`
233
+ );
234
+ }
235
+
236
+ export async function pollWorkflowUntilDone(
237
+ workflowId: string,
238
+ timeoutMs = DEFAULT_TIMEOUT_MS
239
+ ): Promise<WorkflowRow> {
240
+ const start = Date.now();
241
+ while (Date.now() - start < timeoutMs) {
242
+ const { data } = await getWorkflow(workflowId);
243
+ if (data && TERMINAL_WORKFLOW_STATUSES.has(data.status)) {
244
+ return data;
245
+ }
246
+ await sleep(POLL_INTERVAL_MS);
247
+ }
248
+ throw new Error(
249
+ `Workflow ${workflowId} did not reach a terminal status within ${timeoutMs}ms`
250
+ );
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Utilities
255
+ // ---------------------------------------------------------------------------
256
+
257
+ export function sleep(ms: number): Promise<void> {
258
+ return new Promise((resolve) => setTimeout(resolve, ms));
259
+ }
260
+
261
+ /**
262
+ * Check if a runtime is available by calling the connectivity endpoint.
263
+ * Returns true if the runtime responds as connected.
264
+ */
265
+ export async function isRuntimeAvailable(
266
+ runtimeId: string
267
+ ): Promise<boolean> {
268
+ try {
269
+ const { ok, data } = await checkRuntimeConnectivity(runtimeId);
270
+ return ok && !!data?.connected;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Check if the Stagent server is reachable.
278
+ */
279
+ export async function isServerReachable(): Promise<boolean> {
280
+ try {
281
+ const res = await fetch(`${BASE_URL}/api/projects`);
282
+ return res.ok;
283
+ } catch {
284
+ return false;
285
+ }
286
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * E2E: Parallel workflow execution.
3
+ *
4
+ * Tests that parallel workflows run branches concurrently and
5
+ * synthesis steps wait for all dependencies before executing.
6
+ */
7
+
8
+ import {
9
+ setupE2E,
10
+ teardownE2E,
11
+ testProjectId,
12
+ claudeAvailable,
13
+ codexAvailable,
14
+ } from "./setup";
15
+ import {
16
+ createWorkflow,
17
+ executeWorkflow,
18
+ pollWorkflowUntilDone,
19
+ } from "./helpers";
20
+
21
+ beforeAll(async () => {
22
+ await setupE2E();
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await teardownE2E();
27
+ });
28
+
29
+ describe("Parallel Workflow — Claude Code", () => {
30
+ it.skipIf(!claudeAvailable)(
31
+ "runs branches concurrently with synthesis",
32
+ async () => {
33
+ const { ok, data: workflow } = await createWorkflow({
34
+ name: "E2E Parallel Test",
35
+ projectId: testProjectId,
36
+ definition: {
37
+ pattern: "parallel",
38
+ steps: [
39
+ {
40
+ id: "metrics",
41
+ name: "Code Metrics",
42
+ prompt:
43
+ "Count the number of TypeScript files and total lines of code in the project.",
44
+ agentProfile: "general",
45
+ },
46
+ {
47
+ id: "deps",
48
+ name: "Dependency Check",
49
+ prompt:
50
+ "List all dependencies and devDependencies from package.json with their versions.",
51
+ agentProfile: "general",
52
+ },
53
+ {
54
+ id: "synthesize",
55
+ name: "Summary Report",
56
+ prompt:
57
+ "Combine the code metrics and dependency information into a brief project summary.",
58
+ agentProfile: "document-writer",
59
+ dependsOn: ["metrics", "deps"],
60
+ },
61
+ ],
62
+ },
63
+ });
64
+ expect(ok).toBe(true);
65
+
66
+ const exec = await executeWorkflow(workflow!.id);
67
+ expect(exec.status).toBe(202);
68
+
69
+ const result = await pollWorkflowUntilDone(workflow!.id);
70
+ expect(result.status).toBe("completed");
71
+ }
72
+ );
73
+ });
74
+
75
+ describe("Parallel Workflow — Codex", () => {
76
+ it.skipIf(!codexAvailable)(
77
+ "runs parallel branches via Codex runtime",
78
+ async () => {
79
+ const { ok, data: workflow } = await createWorkflow({
80
+ name: "E2E Codex Parallel Test",
81
+ projectId: testProjectId,
82
+ definition: {
83
+ pattern: "parallel",
84
+ steps: [
85
+ {
86
+ id: "files",
87
+ name: "List Files",
88
+ prompt: "List all files in the project directory.",
89
+ assignedAgent: "codex",
90
+ agentProfile: "general",
91
+ },
92
+ {
93
+ id: "structure",
94
+ name: "Describe Structure",
95
+ prompt: "Describe the project directory structure and purpose of each file.",
96
+ assignedAgent: "codex",
97
+ agentProfile: "general",
98
+ },
99
+ {
100
+ id: "combine",
101
+ name: "Combined Report",
102
+ prompt:
103
+ "Combine the file list and structure description into a single overview.",
104
+ assignedAgent: "codex",
105
+ agentProfile: "document-writer",
106
+ dependsOn: ["files", "structure"],
107
+ },
108
+ ],
109
+ },
110
+ });
111
+ expect(ok).toBe(true);
112
+
113
+ const exec = await executeWorkflow(workflow!.id);
114
+ expect(exec.status).toBe(202);
115
+
116
+ const result = await pollWorkflowUntilDone(workflow!.id);
117
+ expect(result.status).toBe("completed");
118
+ }
119
+ );
120
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * E2E: Sequence workflow execution.
3
+ *
4
+ * Tests that multi-step sequence workflows execute steps in order,
5
+ * pass context between steps, and produce combined results.
6
+ */
7
+
8
+ import {
9
+ setupE2E,
10
+ teardownE2E,
11
+ testProjectId,
12
+ claudeAvailable,
13
+ codexAvailable,
14
+ } from "./setup";
15
+ import {
16
+ createWorkflow,
17
+ executeWorkflow,
18
+ pollWorkflowUntilDone,
19
+ createTask,
20
+ getTask,
21
+ } from "./helpers";
22
+
23
+ beforeAll(async () => {
24
+ await setupE2E();
25
+ });
26
+
27
+ afterAll(async () => {
28
+ await teardownE2E();
29
+ });
30
+
31
+ describe("Sequence Workflow — Claude Code", () => {
32
+ it.skipIf(!claudeAvailable)(
33
+ "executes steps in order with context passing",
34
+ async () => {
35
+ const { ok, data: workflow } = await createWorkflow({
36
+ name: "E2E Sequence Test",
37
+ projectId: testProjectId,
38
+ definition: {
39
+ pattern: "sequence",
40
+ steps: [
41
+ {
42
+ id: "analyze",
43
+ name: "Analyze Code",
44
+ prompt:
45
+ "Analyze the TypeScript code in the project. List the main functions and any bugs you find.",
46
+ agentProfile: "general",
47
+ },
48
+ {
49
+ id: "suggest",
50
+ name: "Suggest Tests",
51
+ prompt:
52
+ "Based on the analysis from the previous step, suggest specific test cases that would catch the bugs identified.",
53
+ agentProfile: "code-reviewer",
54
+ dependsOn: ["analyze"],
55
+ },
56
+ ],
57
+ },
58
+ });
59
+ expect(ok).toBe(true);
60
+
61
+ const exec = await executeWorkflow(workflow!.id);
62
+ expect(exec.status).toBe(202);
63
+
64
+ const result = await pollWorkflowUntilDone(workflow!.id);
65
+ expect(result.status).toBe("completed");
66
+ }
67
+ );
68
+ });
69
+
70
+ describe("Sequence Workflow — Codex", () => {
71
+ it.skipIf(!codexAvailable)(
72
+ "executes sequence steps via Codex runtime",
73
+ async () => {
74
+ const { ok, data: workflow } = await createWorkflow({
75
+ name: "E2E Codex Sequence Test",
76
+ projectId: testProjectId,
77
+ definition: {
78
+ pattern: "sequence",
79
+ steps: [
80
+ {
81
+ id: "describe",
82
+ name: "Describe Code",
83
+ prompt:
84
+ "Describe the TypeScript code in the project. List the main functions.",
85
+ assignedAgent: "codex",
86
+ agentProfile: "general",
87
+ },
88
+ {
89
+ id: "review",
90
+ name: "Review Code",
91
+ prompt:
92
+ "Based on the description from the previous step, review the code for bugs.",
93
+ assignedAgent: "codex",
94
+ agentProfile: "code-reviewer",
95
+ dependsOn: ["describe"],
96
+ },
97
+ ],
98
+ },
99
+ });
100
+ expect(ok).toBe(true);
101
+
102
+ const exec = await executeWorkflow(workflow!.id);
103
+ expect(exec.status).toBe(202);
104
+
105
+ const result = await pollWorkflowUntilDone(workflow!.id);
106
+ expect(result.status).toBe("completed");
107
+ }
108
+ );
109
+ });
@@ -0,0 +1,156 @@
1
+ /**
2
+ * E2E test setup — creates a test project and sandbox, tears down after all tests.
3
+ *
4
+ * This file is imported by test files that need a shared project context.
5
+ * It does NOT run as a vitest setupFile — each test suite imports it explicitly.
6
+ */
7
+
8
+ import { mkdirSync, rmSync, writeFileSync, existsSync } from "fs";
9
+ import { join } from "path";
10
+ import { tmpdir } from "os";
11
+ import {
12
+ createProject,
13
+ deleteProject,
14
+ isServerReachable,
15
+ isRuntimeAvailable,
16
+ } from "./helpers";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Shared test state
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export let testProjectId = "";
23
+ export let sandboxDir = "";
24
+ export let claudeAvailable = false;
25
+ export let codexAvailable = false;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Sandbox files — minimal TypeScript project for agents to analyze
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const SANDBOX_FILES: Record<string, string> = {
32
+ "package.json": JSON.stringify(
33
+ {
34
+ name: "stagent-e2e-sandbox",
35
+ version: "1.0.0",
36
+ scripts: { build: "tsc" },
37
+ devDependencies: { typescript: "^5.5.0" },
38
+ },
39
+ null,
40
+ 2
41
+ ),
42
+ "tsconfig.json": JSON.stringify(
43
+ {
44
+ compilerOptions: {
45
+ target: "ES2022",
46
+ module: "ESNext",
47
+ moduleResolution: "bundler",
48
+ outDir: "dist",
49
+ strict: true,
50
+ },
51
+ include: ["src"],
52
+ },
53
+ null,
54
+ 2
55
+ ),
56
+ "src/index.ts": `
57
+ export interface Task {
58
+ id: number;
59
+ title: string;
60
+ completed: boolean;
61
+ }
62
+
63
+ const tasks: Task[] = [];
64
+
65
+ export function addTask(title: string): Task {
66
+ // Deliberate bug: ID based on array length → duplicates after deletion
67
+ const task: Task = { id: tasks.length, title, completed: false };
68
+ tasks.push(task);
69
+ return task;
70
+ }
71
+
72
+ export function completeTask(id: number): boolean {
73
+ const task = tasks.find((t) => t.id === id);
74
+ if (task) {
75
+ task.completed = true;
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ export function getIncompleteTasks(): Task[] {
82
+ return tasks.filter((t) => !t.completed);
83
+ }
84
+ `.trimStart(),
85
+ "src/utils.ts": `
86
+ export function formatDate(date: Date): string {
87
+ // Deliberate bug: getMonth() is zero-based
88
+ return \`\${date.getFullYear()}-\${date.getMonth()}-\${date.getDate()}\`;
89
+ }
90
+
91
+ export function parseCSV(csv: string): string[][] {
92
+ // Deliberate bug: naive parsing — no quoted field support
93
+ return csv.split("\\n").map((line) => line.split(","));
94
+ }
95
+
96
+ export function slugify(text: string): string {
97
+ return text
98
+ .toLowerCase()
99
+ .replace(/\\s+/g, "-")
100
+ .replace(/[^a-z0-9-]/g, "");
101
+ }
102
+ `.trimStart(),
103
+ };
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Setup & Teardown
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export async function setupE2E(): Promise<void> {
110
+ // 1. Check server reachability
111
+ const reachable = await isServerReachable();
112
+ if (!reachable) {
113
+ throw new Error(
114
+ "Stagent server is not reachable at the configured URL. " +
115
+ "Start the dev server with `npm run dev` before running E2E tests."
116
+ );
117
+ }
118
+
119
+ // 2. Create sandbox directory with test files
120
+ sandboxDir = join(tmpdir(), `stagent-e2e-${Date.now()}`);
121
+ mkdirSync(join(sandboxDir, "src"), { recursive: true });
122
+
123
+ for (const [relativePath, content] of Object.entries(SANDBOX_FILES)) {
124
+ const fullPath = join(sandboxDir, relativePath);
125
+ const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
126
+ mkdirSync(dir, { recursive: true });
127
+ writeFileSync(fullPath, content, "utf-8");
128
+ }
129
+
130
+ // 3. Create test project pointing at the sandbox
131
+ const { ok, data } = await createProject({
132
+ name: `E2E Test ${new Date().toISOString().slice(0, 19)}`,
133
+ description: "Automated E2E test project — safe to delete",
134
+ workingDirectory: sandboxDir,
135
+ });
136
+ if (!ok || !data?.id) {
137
+ throw new Error("Failed to create E2E test project");
138
+ }
139
+ testProjectId = data.id;
140
+
141
+ // 4. Detect runtime availability
142
+ claudeAvailable = await isRuntimeAvailable("claude-code");
143
+ codexAvailable = await isRuntimeAvailable("openai-codex-app-server");
144
+ }
145
+
146
+ export async function teardownE2E(): Promise<void> {
147
+ // Clean up test project
148
+ if (testProjectId) {
149
+ await deleteProject(testProjectId).catch(() => {});
150
+ }
151
+
152
+ // Clean up sandbox directory
153
+ if (sandboxDir && existsSync(sandboxDir)) {
154
+ rmSync(sandboxDir, { recursive: true, force: true });
155
+ }
156
+ }