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.
- package/README.md +74 -49
- package/package.json +3 -2
- package/public/readme/cost-usage-list.png +0 -0
- package/public/readme/dashboard-bulk-select.png +0 -0
- package/public/readme/dashboard-card-edit.png +0 -0
- package/public/readme/dashboard-create-form-ai-applied.png +0 -0
- package/public/readme/dashboard-create-form-ai-assist.png +0 -0
- package/public/readme/dashboard-create-form-empty.png +0 -0
- package/public/readme/dashboard-create-form-filled.png +0 -0
- package/public/readme/dashboard-filtered.png +0 -0
- package/public/readme/dashboard-list.png +0 -0
- package/public/readme/dashboard-workflow-confirm.png +0 -0
- package/public/readme/home-below-fold.png +0 -0
- package/public/readme/home-list.png +0 -0
- package/public/readme/inbox-list.png +0 -0
- package/public/readme/playbook-list.png +0 -0
- package/public/readme/profiles-list.png +0 -0
- package/public/readme/settings-list.png +0 -0
- package/public/readme/workflows-list.png +0 -0
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -20
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/api/tasks/[id]/route.ts +54 -3
- package/src/app/api/workflows/[id]/route.ts +43 -4
- package/src/app/api/workflows/[id]/status/route.ts +70 -2
- package/src/app/api/workflows/from-assist/route.ts +6 -32
- package/src/app/costs/page.tsx +53 -43
- package/src/app/dashboard/page.tsx +59 -21
- package/src/app/documents/[id]/page.tsx +10 -8
- package/src/app/globals.css +11 -0
- package/src/app/page.tsx +60 -3
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/[id]/page.tsx +22 -2
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/dashboard/greeting.tsx +3 -1
- package/src/components/dashboard/priority-queue.tsx +58 -9
- package/src/components/dashboard/stats-cards.tsx +16 -2
- package/src/components/documents/document-chip-bar.tsx +183 -0
- package/src/components/documents/document-content-renderer.tsx +146 -0
- package/src/components/documents/document-detail-view.tsx +16 -239
- package/src/components/documents/image-zoom-view.tsx +60 -0
- package/src/components/documents/smart-extracted-text.tsx +47 -0
- package/src/components/documents/utils.ts +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/inbox-list.tsx +4 -5
- package/src/components/notifications/notification-item.tsx +73 -6
- package/src/components/notifications/pending-approval-host.tsx +63 -14
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +225 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-browser.tsx +1 -0
- package/src/components/profiles/profile-card.tsx +16 -8
- package/src/components/profiles/profile-detail-view.tsx +12 -4
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +4 -2
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
- package/src/components/tasks/ai-assist-panel.tsx +108 -78
- package/src/components/tasks/content-preview.tsx +2 -1
- package/src/components/tasks/kanban-board.tsx +57 -5
- package/src/components/tasks/kanban-column.tsx +34 -23
- package/src/components/tasks/task-bento-cell.tsx +50 -0
- package/src/components/tasks/task-bento-grid.tsx +155 -0
- package/src/components/tasks/task-card.tsx +14 -16
- package/src/components/tasks/task-chip-bar.tsx +207 -0
- package/src/components/tasks/task-detail-view.tsx +42 -190
- package/src/components/tasks/task-result-renderer.tsx +33 -0
- package/src/components/workflows/blueprint-gallery.tsx +19 -12
- package/src/components/workflows/blueprint-preview.tsx +8 -1
- package/src/components/workflows/loop-status-view.tsx +2 -4
- package/src/components/workflows/swarm-dashboard.tsx +2 -3
- package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
- package/src/components/workflows/workflow-full-output.tsx +80 -0
- package/src/components/workflows/workflow-kanban-card.tsx +121 -0
- package/src/components/workflows/workflow-list.tsx +47 -42
- package/src/components/workflows/workflow-status-view.tsx +163 -16
- package/src/lib/agents/learned-context.ts +27 -15
- package/src/lib/agents/learning-session.ts +354 -0
- package/src/lib/agents/pattern-extractor.ts +19 -0
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/constants/card-icons.tsx +202 -0
- package/src/lib/constants/prose-styles.ts +7 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +3 -0
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +107 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/documents/context-builder.ts +41 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/queries/chart-data.ts +20 -1
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +2 -2
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +1 -1
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -95
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/engine.ts +75 -61
- package/src/lib/workflows/types.ts +2 -0
- package/tsconfig.json +2 -1
- 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
|
+
}
|