stagent 0.1.11 → 0.1.12
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 +35 -4
- package/package.json +3 -2
- 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/costs/page.tsx +53 -43
- 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/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/notification-item.tsx +6 -3
- package/src/components/notifications/pending-approval-host.tsx +57 -11
- 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 +223 -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-detail-view.tsx +6 -3
- 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 +2 -0
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/workflows/loop-status-view.tsx +8 -4
- package/src/components/workflows/workflow-status-view.tsx +16 -9
- package/src/lib/agents/learned-context.ts +27 -15
- package/src/lib/agents/learning-session.ts +234 -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/settings.ts +1 -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 +102 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/notifications/actionable.ts +18 -10
- 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 +18 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E: Single task execution across profiles and runtimes.
|
|
3
|
+
*
|
|
4
|
+
* Tests that individual tasks execute and produce results via both
|
|
5
|
+
* Claude Code and Codex runtimes with different agent profiles.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
setupE2E,
|
|
10
|
+
teardownE2E,
|
|
11
|
+
testProjectId,
|
|
12
|
+
claudeAvailable,
|
|
13
|
+
codexAvailable,
|
|
14
|
+
} from "./setup";
|
|
15
|
+
import {
|
|
16
|
+
createTask,
|
|
17
|
+
executeTask,
|
|
18
|
+
pollTaskUntilDone,
|
|
19
|
+
updateTask,
|
|
20
|
+
} from "./helpers";
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
await setupE2E();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await teardownE2E();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Claude Code runtime
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
describe("Single Task — Claude Code", () => {
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
if (!claudeAvailable) {
|
|
37
|
+
console.warn("Skipping Claude Code tests — runtime not available");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it.skipIf(!claudeAvailable)(
|
|
42
|
+
"general profile describes code",
|
|
43
|
+
async () => {
|
|
44
|
+
const { ok, data: task } = await createTask({
|
|
45
|
+
title: "Describe the TypeScript code in src/",
|
|
46
|
+
description:
|
|
47
|
+
"Read the TypeScript files in the project and describe what the code does.",
|
|
48
|
+
projectId: testProjectId,
|
|
49
|
+
agentProfile: "general",
|
|
50
|
+
});
|
|
51
|
+
expect(ok).toBe(true);
|
|
52
|
+
|
|
53
|
+
// Queue → execute
|
|
54
|
+
await updateTask(task!.id, { status: "queued" });
|
|
55
|
+
const exec = await executeTask(task!.id);
|
|
56
|
+
expect(exec.status).toBe(202);
|
|
57
|
+
|
|
58
|
+
// Poll until done
|
|
59
|
+
const result = await pollTaskUntilDone(task!.id);
|
|
60
|
+
expect(result.status).toBe("completed");
|
|
61
|
+
expect(result.result).toBeTruthy();
|
|
62
|
+
expect(result.result!.length).toBeGreaterThan(50);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
it.skipIf(!claudeAvailable)(
|
|
67
|
+
"code-reviewer profile finds bugs",
|
|
68
|
+
async () => {
|
|
69
|
+
const { ok, data: task } = await createTask({
|
|
70
|
+
title: "Review code for bugs",
|
|
71
|
+
description:
|
|
72
|
+
"Review all TypeScript files in the project. Find bugs and report them with severity levels.",
|
|
73
|
+
projectId: testProjectId,
|
|
74
|
+
agentProfile: "code-reviewer",
|
|
75
|
+
});
|
|
76
|
+
expect(ok).toBe(true);
|
|
77
|
+
|
|
78
|
+
await updateTask(task!.id, { status: "queued" });
|
|
79
|
+
const exec = await executeTask(task!.id);
|
|
80
|
+
expect(exec.status).toBe(202);
|
|
81
|
+
|
|
82
|
+
const result = await pollTaskUntilDone(task!.id);
|
|
83
|
+
expect(result.status).toBe("completed");
|
|
84
|
+
expect(result.result).toBeTruthy();
|
|
85
|
+
// Code reviewer should find at least some issues
|
|
86
|
+
expect(result.result!.length).toBeGreaterThan(100);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
it.skipIf(!claudeAvailable)(
|
|
91
|
+
"document-writer profile generates overview",
|
|
92
|
+
async () => {
|
|
93
|
+
const { ok, data: task } = await createTask({
|
|
94
|
+
title: "Write a technical overview document",
|
|
95
|
+
description:
|
|
96
|
+
"Generate a technical overview of this project including structure, modules, and dependencies.",
|
|
97
|
+
projectId: testProjectId,
|
|
98
|
+
agentProfile: "document-writer",
|
|
99
|
+
});
|
|
100
|
+
expect(ok).toBe(true);
|
|
101
|
+
|
|
102
|
+
await updateTask(task!.id, { status: "queued" });
|
|
103
|
+
const exec = await executeTask(task!.id);
|
|
104
|
+
expect(exec.status).toBe(202);
|
|
105
|
+
|
|
106
|
+
const result = await pollTaskUntilDone(task!.id);
|
|
107
|
+
expect(result.status).toBe("completed");
|
|
108
|
+
expect(result.result).toBeTruthy();
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Codex runtime
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
describe("Single Task — Codex", () => {
|
|
118
|
+
beforeAll(() => {
|
|
119
|
+
if (!codexAvailable) {
|
|
120
|
+
console.warn("Skipping Codex tests — runtime not available");
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it.skipIf(!codexAvailable)(
|
|
125
|
+
"general profile describes code via Codex",
|
|
126
|
+
async () => {
|
|
127
|
+
const { ok, data: task } = await createTask({
|
|
128
|
+
title: "Describe the TypeScript code in src/",
|
|
129
|
+
description:
|
|
130
|
+
"Read the TypeScript files in the project and describe what the code does.",
|
|
131
|
+
projectId: testProjectId,
|
|
132
|
+
assignedAgent: "codex",
|
|
133
|
+
agentProfile: "general",
|
|
134
|
+
});
|
|
135
|
+
expect(ok).toBe(true);
|
|
136
|
+
|
|
137
|
+
await updateTask(task!.id, { status: "queued" });
|
|
138
|
+
const exec = await executeTask(task!.id);
|
|
139
|
+
expect(exec.status).toBe(202);
|
|
140
|
+
|
|
141
|
+
const result = await pollTaskUntilDone(task!.id);
|
|
142
|
+
expect(result.status).toBe("completed");
|
|
143
|
+
expect(result.result).toBeTruthy();
|
|
144
|
+
expect(result.result!.length).toBeGreaterThan(50);
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
it.skipIf(!codexAvailable)(
|
|
149
|
+
"code-reviewer profile finds bugs via Codex",
|
|
150
|
+
async () => {
|
|
151
|
+
const { ok, data: task } = await createTask({
|
|
152
|
+
title: "Review code for bugs",
|
|
153
|
+
description:
|
|
154
|
+
"Review all TypeScript files in the project. Find bugs and report them with severity levels.",
|
|
155
|
+
projectId: testProjectId,
|
|
156
|
+
assignedAgent: "codex",
|
|
157
|
+
agentProfile: "code-reviewer",
|
|
158
|
+
});
|
|
159
|
+
expect(ok).toBe(true);
|
|
160
|
+
|
|
161
|
+
await updateTask(task!.id, { status: "queued" });
|
|
162
|
+
const exec = await executeTask(task!.id);
|
|
163
|
+
expect(exec.status).toBe(202);
|
|
164
|
+
|
|
165
|
+
const result = await pollTaskUntilDone(task!.id);
|
|
166
|
+
expect(result.status).toBe("completed");
|
|
167
|
+
expect(result.result).toBeTruthy();
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
});
|
|
@@ -2,30 +2,53 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
3
|
import { projects, tasks } from "@/lib/db/schema";
|
|
4
4
|
import { desc } from "drizzle-orm";
|
|
5
|
+
import { getManifest } from "@/lib/docs/reader";
|
|
5
6
|
|
|
6
7
|
export async function GET() {
|
|
7
|
-
const recentProjects = await
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
const [recentProjects, recentTasks] = await Promise.all([
|
|
9
|
+
db
|
|
10
|
+
.select({
|
|
11
|
+
id: projects.id,
|
|
12
|
+
name: projects.name,
|
|
13
|
+
status: projects.status,
|
|
14
|
+
})
|
|
15
|
+
.from(projects)
|
|
16
|
+
.orderBy(desc(projects.updatedAt))
|
|
17
|
+
.limit(5),
|
|
18
|
+
db
|
|
19
|
+
.select({
|
|
20
|
+
id: tasks.id,
|
|
21
|
+
title: tasks.title,
|
|
22
|
+
status: tasks.status,
|
|
23
|
+
})
|
|
24
|
+
.from(tasks)
|
|
25
|
+
.orderBy(desc(tasks.updatedAt))
|
|
26
|
+
.limit(5),
|
|
27
|
+
]);
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
// Read playbook items from manifest
|
|
30
|
+
let playbook: { slug: string; title: string; tags: string[] }[] = [];
|
|
31
|
+
try {
|
|
32
|
+
const manifest = getManifest();
|
|
33
|
+
playbook = [
|
|
34
|
+
...manifest.sections.map((s) => ({
|
|
35
|
+
slug: s.slug,
|
|
36
|
+
title: s.title,
|
|
37
|
+
tags: s.tags,
|
|
38
|
+
})),
|
|
39
|
+
...manifest.journeys.map((j) => ({
|
|
40
|
+
slug: j.slug,
|
|
41
|
+
title: j.title,
|
|
42
|
+
tags: [j.persona, j.difficulty],
|
|
43
|
+
})),
|
|
44
|
+
];
|
|
45
|
+
} catch {
|
|
46
|
+
// docs/manifest.json may not exist — graceful fallback
|
|
47
|
+
}
|
|
26
48
|
|
|
27
49
|
return NextResponse.json({
|
|
28
50
|
projects: recentProjects,
|
|
29
51
|
tasks: recentTasks,
|
|
52
|
+
playbook,
|
|
30
53
|
});
|
|
31
54
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
batchApproveProposals,
|
|
5
|
+
batchRejectProposals,
|
|
6
|
+
} from "@/lib/agents/learning-session";
|
|
7
|
+
|
|
8
|
+
const batchSchema = z.object({
|
|
9
|
+
proposalIds: z.array(z.string().min(1)).min(1),
|
|
10
|
+
action: z.enum(["approve", "reject"]),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* POST /api/context/batch — batch approve or reject context proposals.
|
|
15
|
+
*
|
|
16
|
+
* Used by the batch proposal review UI after workflow completion.
|
|
17
|
+
* Accepts an array of learned_context row IDs and an action.
|
|
18
|
+
*/
|
|
19
|
+
export async function POST(req: NextRequest) {
|
|
20
|
+
try {
|
|
21
|
+
const body = await req.json();
|
|
22
|
+
const parsed = batchSchema.safeParse(body);
|
|
23
|
+
|
|
24
|
+
if (!parsed.success) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: "proposalIds (string[]) and action ('approve'|'reject') are required" },
|
|
27
|
+
{ status: 400 }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { proposalIds, action } = parsed.data;
|
|
32
|
+
|
|
33
|
+
const count =
|
|
34
|
+
action === "approve"
|
|
35
|
+
? await batchApproveProposals(proposalIds)
|
|
36
|
+
: await batchRejectProposals(proposalIds);
|
|
37
|
+
|
|
38
|
+
return NextResponse.json({ success: true, action, count });
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
const message =
|
|
41
|
+
err instanceof Error ? err.message : "Batch operation failed";
|
|
42
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
43
|
+
}
|
|
44
|
+
}
|