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,94 @@
|
|
|
1
|
+
export async function registerNodeInstrumentation() {
|
|
2
|
+
try {
|
|
3
|
+
// Instance bootstrap — creates local branch, handles dev-mode gates, consent flow.
|
|
4
|
+
// Runs BEFORE other startup so instance config is available downstream.
|
|
5
|
+
// Safe in the canonical stagent dev repo thanks to STAGENT_DEV_MODE=true
|
|
6
|
+
// in .env.local plus the .git/stagent-dev-mode sentinel file.
|
|
7
|
+
const { ensureInstance } = await import("@/lib/instance/bootstrap");
|
|
8
|
+
const instanceResult = await ensureInstance();
|
|
9
|
+
if (instanceResult.skipped) {
|
|
10
|
+
console.log(`[instance] bootstrap skipped: ${instanceResult.skipped}`);
|
|
11
|
+
} else {
|
|
12
|
+
for (const step of instanceResult.steps) {
|
|
13
|
+
if (step.status === "failed") {
|
|
14
|
+
console.error(`[instance] ${step.step} failed: ${step.reason}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Run pending Drizzle migrations (DROP TABLE, CREATE INDEX, etc.)
|
|
20
|
+
// that can't be handled by bootstrap's IF NOT EXISTS pattern.
|
|
21
|
+
// Runs here (not in db/index.ts) to avoid SQLITE_BUSY during next build.
|
|
22
|
+
await runPendingMigrations();
|
|
23
|
+
|
|
24
|
+
// Instance upgrade poller — hourly `git fetch` to detect upstream commits.
|
|
25
|
+
// Skipped in dev mode; lightweight; uses advisory lock to prevent overlap.
|
|
26
|
+
const { startUpgradePoller } = await import("@/lib/instance/upgrade-poller");
|
|
27
|
+
startUpgradePoller();
|
|
28
|
+
|
|
29
|
+
const { startScheduler } = await import("@/lib/schedules/scheduler");
|
|
30
|
+
startScheduler();
|
|
31
|
+
|
|
32
|
+
const { startChannelPoller } = await import("@/lib/channels/poller");
|
|
33
|
+
startChannelPoller();
|
|
34
|
+
|
|
35
|
+
const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
|
|
36
|
+
startAutoBackup();
|
|
37
|
+
|
|
38
|
+
// History retention cleanup — prunes old agent_logs and usage_ledger
|
|
39
|
+
startHistoryCleanup();
|
|
40
|
+
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error("Instrumentation startup failed:", err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function startHistoryCleanup() {
|
|
47
|
+
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000;
|
|
48
|
+
const RETENTION_DAYS = 365;
|
|
49
|
+
|
|
50
|
+
async function cleanup() {
|
|
51
|
+
const { db } = await import("@/lib/db");
|
|
52
|
+
const { agentLogs, usageLedger } = await import("@/lib/db/schema");
|
|
53
|
+
const { lt } = await import("drizzle-orm");
|
|
54
|
+
|
|
55
|
+
const cutoff = new Date(Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000);
|
|
56
|
+
db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
|
|
57
|
+
db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cleanup().catch(() => {});
|
|
61
|
+
setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runPendingMigrations() {
|
|
65
|
+
const { join } = await import("path");
|
|
66
|
+
const { existsSync } = await import("fs");
|
|
67
|
+
const { getAppRoot } = await import("@/lib/utils/app-root");
|
|
68
|
+
|
|
69
|
+
const appRoot = getAppRoot(import.meta.dirname, 1);
|
|
70
|
+
const migrationsDir = join(appRoot, "src", "lib", "db", "migrations");
|
|
71
|
+
if (!existsSync(migrationsDir)) return; // npx distribution — no migration files
|
|
72
|
+
|
|
73
|
+
const { sqlite } = await import("@/lib/db");
|
|
74
|
+
const { drizzle } = await import("drizzle-orm/better-sqlite3");
|
|
75
|
+
const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
|
|
76
|
+
const {
|
|
77
|
+
hasLegacyStagentTables,
|
|
78
|
+
hasMigrationHistory,
|
|
79
|
+
markAllMigrationsApplied,
|
|
80
|
+
bootstrapStagentDatabase,
|
|
81
|
+
} = await import("@/lib/db/bootstrap");
|
|
82
|
+
|
|
83
|
+
const needsLegacyRecovery =
|
|
84
|
+
hasLegacyStagentTables(sqlite) && !hasMigrationHistory(sqlite);
|
|
85
|
+
|
|
86
|
+
if (needsLegacyRecovery) {
|
|
87
|
+
bootstrapStagentDatabase(sqlite);
|
|
88
|
+
markAllMigrationsApplied(sqlite, migrationsDir);
|
|
89
|
+
console.log("[db] Recovered legacy database — all migrations stamped.");
|
|
90
|
+
} else {
|
|
91
|
+
const db = drizzle(sqlite);
|
|
92
|
+
migrate(db, { migrationsFolder: migrationsDir });
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/instrumentation.ts
CHANGED
|
@@ -1,51 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
try {
|
|
5
|
-
// License manager — initialize from DB (creates default row if needed)
|
|
6
|
-
const { licenseManager } = await import("@/lib/license/manager");
|
|
7
|
-
licenseManager.initialize();
|
|
8
|
-
licenseManager.startValidationTimer();
|
|
9
|
-
|
|
10
|
-
const { startScheduler } = await import("@/lib/schedules/scheduler");
|
|
11
|
-
startScheduler();
|
|
12
|
-
|
|
13
|
-
const { startChannelPoller } = await import("@/lib/channels/poller");
|
|
14
|
-
startChannelPoller();
|
|
15
|
-
|
|
16
|
-
const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
|
|
17
|
-
startAutoBackup();
|
|
18
|
-
|
|
19
|
-
// History retention cleanup — prunes old agent_logs and usage_ledger
|
|
20
|
-
// based on tier retention limit (Community: 30 days)
|
|
21
|
-
startHistoryCleanup(licenseManager);
|
|
22
|
-
|
|
23
|
-
// Telemetry batch flush (opt-in, every 5 minutes)
|
|
24
|
-
const { startTelemetryFlush } = await import("@/lib/telemetry/queue");
|
|
25
|
-
startTelemetryFlush();
|
|
26
|
-
} catch (err) {
|
|
27
|
-
console.error("Instrumentation startup failed:", err);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function startHistoryCleanup(licenseManager: { getLimit: (r: "historyRetentionDays") => number }) {
|
|
33
|
-
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
|
34
|
-
|
|
35
|
-
async function cleanup() {
|
|
36
|
-
const retentionDays = licenseManager.getLimit("historyRetentionDays");
|
|
37
|
-
if (!Number.isFinite(retentionDays)) return; // Unlimited retention
|
|
38
|
-
|
|
39
|
-
const { db } = await import("@/lib/db");
|
|
40
|
-
const { agentLogs, usageLedger } = await import("@/lib/db/schema");
|
|
41
|
-
const { lt } = await import("drizzle-orm");
|
|
42
|
-
|
|
43
|
-
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
|
|
44
|
-
db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
|
|
45
|
-
db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
|
|
1
|
+
export function register() {
|
|
2
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") {
|
|
3
|
+
return;
|
|
46
4
|
}
|
|
47
5
|
|
|
48
|
-
|
|
49
|
-
cleanup().catch(() => {});
|
|
50
|
-
setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
|
|
6
|
+
return require("./instrumentation-node").registerNodeInstrumentation();
|
|
51
7
|
}
|
|
@@ -141,10 +141,16 @@ vi.mock("@/lib/agents/browser-mcp", () => ({
|
|
|
141
141
|
isExaTool: vi.fn().mockReturnValue(false),
|
|
142
142
|
isExaReadOnly: vi.fn().mockReturnValue(false),
|
|
143
143
|
}));
|
|
144
|
+
vi.mock("@/lib/chat/stagent-tools", () => ({
|
|
145
|
+
createToolServer: vi.fn((_projectId?: string | null) => ({
|
|
146
|
+
asMcpServer: () => ({ __mockStagentServer: true }),
|
|
147
|
+
})),
|
|
148
|
+
}));
|
|
144
149
|
|
|
145
150
|
// Static imports (works because vi.mock is hoisted)
|
|
146
151
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
147
152
|
import { executeClaudeTask, resumeClaudeTask } from "../claude-agent";
|
|
153
|
+
import { createToolServer } from "@/lib/chat/stagent-tools";
|
|
148
154
|
|
|
149
155
|
const mockQuery = vi.mocked(query);
|
|
150
156
|
|
|
@@ -238,6 +244,72 @@ describe("executeClaudeTask", () => {
|
|
|
238
244
|
expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
|
|
239
245
|
});
|
|
240
246
|
|
|
247
|
+
it("A-stagent-1: injects stagent MCP server into query mcpServers", async () => {
|
|
248
|
+
mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
|
|
249
|
+
mockQuery.mockReturnValue(
|
|
250
|
+
createMockStream([
|
|
251
|
+
{ type: "result", result: "done" },
|
|
252
|
+
]) as unknown as ReturnType<typeof query>
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await executeClaudeTask("task-1");
|
|
256
|
+
|
|
257
|
+
const queryCall = mockQuery.mock.calls[0][0] as {
|
|
258
|
+
options: { mcpServers?: Record<string, unknown> };
|
|
259
|
+
};
|
|
260
|
+
expect(queryCall.options.mcpServers).toBeDefined();
|
|
261
|
+
expect(queryCall.options.mcpServers!.stagent).toEqual({ __mockStagentServer: true });
|
|
262
|
+
expect(vi.mocked(createToolServer)).toHaveBeenCalledWith("proj-7");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("A-stagent-2: prepends mcp__stagent__* when profile has allowedTools", async () => {
|
|
266
|
+
mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
|
|
267
|
+
mockGetProfile.mockReturnValueOnce({
|
|
268
|
+
id: "restricted",
|
|
269
|
+
name: "Restricted",
|
|
270
|
+
systemPrompt: "",
|
|
271
|
+
allowedTools: ["Read", "Grep"],
|
|
272
|
+
});
|
|
273
|
+
mockQuery.mockReturnValue(
|
|
274
|
+
createMockStream([
|
|
275
|
+
{ type: "result", result: "done" },
|
|
276
|
+
]) as unknown as ReturnType<typeof query>
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
await executeClaudeTask("task-1");
|
|
280
|
+
|
|
281
|
+
const queryCall = mockQuery.mock.calls[0][0] as {
|
|
282
|
+
options: { allowedTools?: string[] };
|
|
283
|
+
};
|
|
284
|
+
expect(queryCall.options.allowedTools).toBeDefined();
|
|
285
|
+
expect(queryCall.options.allowedTools).toContain("mcp__stagent__*");
|
|
286
|
+
expect(queryCall.options.allowedTools).toContain("Read");
|
|
287
|
+
expect(queryCall.options.allowedTools).toContain("Grep");
|
|
288
|
+
// Duplicates not added when profile didn't already include the pattern
|
|
289
|
+
const stagentCount = queryCall.options.allowedTools!.filter(
|
|
290
|
+
(t) => t === "mcp__stagent__*"
|
|
291
|
+
).length;
|
|
292
|
+
expect(stagentCount).toBe(1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("A-stagent-3: omits allowedTools when profile has none (preset defaults preserved)", async () => {
|
|
296
|
+
mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
|
|
297
|
+
// Default mockGetProfile returns allowedTools: undefined, so ctx.payload.allowedTools
|
|
298
|
+
// will also be undefined — the query() call should NOT include an allowedTools option.
|
|
299
|
+
mockQuery.mockReturnValue(
|
|
300
|
+
createMockStream([
|
|
301
|
+
{ type: "result", result: "done" },
|
|
302
|
+
]) as unknown as ReturnType<typeof query>
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
await executeClaudeTask("task-1");
|
|
306
|
+
|
|
307
|
+
const queryCall = mockQuery.mock.calls[0][0] as {
|
|
308
|
+
options: { allowedTools?: string[] };
|
|
309
|
+
};
|
|
310
|
+
expect(queryCall.options.allowedTools).toBeUndefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
241
313
|
it("A3: captures sessionId from init message and re-calls setExecution", async () => {
|
|
242
314
|
mockWhere.mockResolvedValueOnce([makeTask()]);
|
|
243
315
|
mockQuery.mockReturnValue(
|
|
@@ -337,6 +409,51 @@ describe("executeClaudeTask", () => {
|
|
|
337
409
|
expect(callOptions.maxTurns).toBeDefined();
|
|
338
410
|
expect(callOptions.maxBudgetUsd).toBeDefined();
|
|
339
411
|
});
|
|
412
|
+
|
|
413
|
+
it("A8: waits for learned-pattern extraction before final cleanup", async () => {
|
|
414
|
+
let resolveAnalysis: (() => void) | null = null;
|
|
415
|
+
mockWhere.mockResolvedValueOnce([makeTask()]);
|
|
416
|
+
mockQuery.mockReturnValue(
|
|
417
|
+
createMockStream([{ type: "result", result: "done" }]) as unknown as ReturnType<typeof query>
|
|
418
|
+
);
|
|
419
|
+
mockAnalyzeForLearnedPatterns.mockReturnValueOnce(
|
|
420
|
+
new Promise((resolve) => {
|
|
421
|
+
resolveAnalysis = () => resolve(null);
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const runPromise = executeClaudeTask("task-1");
|
|
426
|
+
await vi.waitFor(() => {
|
|
427
|
+
expect(mockAnalyzeForLearnedPatterns).toHaveBeenCalledWith("task-1", "general");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(mockRemoveExecution).not.toHaveBeenCalled();
|
|
431
|
+
|
|
432
|
+
resolveAnalysis?.();
|
|
433
|
+
await runPromise;
|
|
434
|
+
|
|
435
|
+
expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("A9: logs learned-pattern extraction failures without failing the task", async () => {
|
|
439
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
440
|
+
mockWhere.mockResolvedValueOnce([makeTask()]);
|
|
441
|
+
mockQuery.mockReturnValue(
|
|
442
|
+
createMockStream([{ type: "result", result: "done" }]) as unknown as ReturnType<typeof query>
|
|
443
|
+
);
|
|
444
|
+
mockAnalyzeForLearnedPatterns.mockRejectedValueOnce(new Error("extract failed"));
|
|
445
|
+
|
|
446
|
+
await executeClaudeTask("task-1");
|
|
447
|
+
|
|
448
|
+
expect(mockSet).toHaveBeenCalledWith(
|
|
449
|
+
expect.objectContaining({ status: "completed", result: "done" })
|
|
450
|
+
);
|
|
451
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
452
|
+
"[self-improvement] pattern extraction failed:",
|
|
453
|
+
expect.any(Error)
|
|
454
|
+
);
|
|
455
|
+
errorSpy.mockRestore();
|
|
456
|
+
});
|
|
340
457
|
});
|
|
341
458
|
|
|
342
459
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -527,6 +644,88 @@ describe("resumeClaudeTask", () => {
|
|
|
527
644
|
expect.objectContaining({ event: "error" })
|
|
528
645
|
);
|
|
529
646
|
});
|
|
647
|
+
|
|
648
|
+
it("C5: waits for learned-pattern extraction before final cleanup on resume", async () => {
|
|
649
|
+
let resolveAnalysis: (() => void) | null = null;
|
|
650
|
+
mockWhere.mockResolvedValueOnce([
|
|
651
|
+
makeTask({ sessionId: "sess-123", resumeCount: 0 }),
|
|
652
|
+
]);
|
|
653
|
+
mockQuery.mockReturnValue(
|
|
654
|
+
createMockStream([{ type: "result", result: "resumed ok" }]) as unknown as ReturnType<typeof query>
|
|
655
|
+
);
|
|
656
|
+
mockAnalyzeForLearnedPatterns.mockReturnValueOnce(
|
|
657
|
+
new Promise((resolve) => {
|
|
658
|
+
resolveAnalysis = () => resolve(null);
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
const runPromise = resumeClaudeTask("task-1");
|
|
663
|
+
await vi.waitFor(() => {
|
|
664
|
+
expect(mockAnalyzeForLearnedPatterns).toHaveBeenCalledWith("task-1", "general");
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
expect(mockRemoveExecution).not.toHaveBeenCalled();
|
|
668
|
+
|
|
669
|
+
resolveAnalysis?.();
|
|
670
|
+
await runPromise;
|
|
671
|
+
|
|
672
|
+
expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it("R-stagent-1: injects stagent MCP server into query mcpServers on resume", async () => {
|
|
676
|
+
mockWhere.mockResolvedValueOnce([
|
|
677
|
+
makeTask({
|
|
678
|
+
projectId: "proj-7",
|
|
679
|
+
sessionId: "session-abc",
|
|
680
|
+
resumeCount: 1,
|
|
681
|
+
}),
|
|
682
|
+
]);
|
|
683
|
+
mockQuery.mockReturnValue(
|
|
684
|
+
createMockStream([
|
|
685
|
+
{ type: "result", result: "resumed and done" },
|
|
686
|
+
]) as unknown as ReturnType<typeof query>
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
await resumeClaudeTask("task-1");
|
|
690
|
+
|
|
691
|
+
const queryCall = mockQuery.mock.calls[0][0] as {
|
|
692
|
+
options: { mcpServers?: Record<string, unknown>; resume?: string };
|
|
693
|
+
};
|
|
694
|
+
expect(queryCall.options.resume).toBe("session-abc");
|
|
695
|
+
expect(queryCall.options.mcpServers).toBeDefined();
|
|
696
|
+
expect(queryCall.options.mcpServers!.stagent).toEqual({ __mockStagentServer: true });
|
|
697
|
+
expect(vi.mocked(createToolServer)).toHaveBeenCalledWith("proj-7");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("R-stagent-2: prepends mcp__stagent__* on resume when profile has allowedTools", async () => {
|
|
701
|
+
mockWhere.mockResolvedValueOnce([
|
|
702
|
+
makeTask({
|
|
703
|
+
projectId: "proj-7",
|
|
704
|
+
sessionId: "session-abc",
|
|
705
|
+
resumeCount: 1,
|
|
706
|
+
}),
|
|
707
|
+
]);
|
|
708
|
+
mockGetProfile.mockReturnValueOnce({
|
|
709
|
+
id: "restricted",
|
|
710
|
+
name: "Restricted",
|
|
711
|
+
systemPrompt: "",
|
|
712
|
+
allowedTools: ["Read", "Grep"],
|
|
713
|
+
});
|
|
714
|
+
mockQuery.mockReturnValue(
|
|
715
|
+
createMockStream([
|
|
716
|
+
{ type: "result", result: "resumed and done" },
|
|
717
|
+
]) as unknown as ReturnType<typeof query>
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
await resumeClaudeTask("task-1");
|
|
721
|
+
|
|
722
|
+
const queryCall = mockQuery.mock.calls[0][0] as {
|
|
723
|
+
options: { allowedTools?: string[] };
|
|
724
|
+
};
|
|
725
|
+
expect(queryCall.options.allowedTools).toContain("mcp__stagent__*");
|
|
726
|
+
expect(queryCall.options.allowedTools).toContain("Read");
|
|
727
|
+
expect(queryCall.options.allowedTools![0]).toBe("mcp__stagent__*");
|
|
728
|
+
});
|
|
530
729
|
});
|
|
531
730
|
|
|
532
731
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach
|
|
2
|
-
|
|
3
|
-
vi.mock("@/lib/license/manager", () => ({
|
|
4
|
-
licenseManager: {
|
|
5
|
-
getLimit: vi.fn().mockReturnValue(Infinity),
|
|
6
|
-
getTier: vi.fn().mockReturnValue("scale"),
|
|
7
|
-
},
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
vi.mock("@/lib/license/notifications", () => ({
|
|
11
|
-
createTierLimitNotification: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
}));
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
13
2
|
|
|
14
3
|
import {
|
|
15
4
|
getExecution,
|
|
16
5
|
setExecution,
|
|
17
6
|
removeExecution,
|
|
18
7
|
getAllExecutions,
|
|
19
|
-
ParallelLimitError,
|
|
20
8
|
} from "@/lib/agents/execution-manager";
|
|
21
9
|
|
|
22
10
|
function makeExecution(taskId: string) {
|
|
@@ -73,18 +61,4 @@ describe("execution-manager", () => {
|
|
|
73
61
|
it("removing non-existent task does not throw", () => {
|
|
74
62
|
expect(() => removeExecution("nonexistent")).not.toThrow();
|
|
75
63
|
});
|
|
76
|
-
|
|
77
|
-
it("throws ParallelLimitError when limit is reached", async () => {
|
|
78
|
-
const { licenseManager } = await import("@/lib/license/manager");
|
|
79
|
-
(licenseManager.getLimit as ReturnType<typeof vi.fn>).mockReturnValue(2);
|
|
80
|
-
(licenseManager.getTier as ReturnType<typeof vi.fn>).mockReturnValue("community");
|
|
81
|
-
|
|
82
|
-
setExecution("task-1", makeExecution("task-1"));
|
|
83
|
-
setExecution("task-2", makeExecution("task-2"));
|
|
84
|
-
|
|
85
|
-
expect(() => setExecution("task-3", makeExecution("task-3"))).toThrow(ParallelLimitError);
|
|
86
|
-
|
|
87
|
-
// Restore unlimited for other tests
|
|
88
|
-
(licenseManager.getLimit as ReturnType<typeof vi.fn>).mockReturnValue(Infinity);
|
|
89
|
-
});
|
|
90
64
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { tasks, projects } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { writeTerminalFailureReason } from "../claude-agent";
|
|
7
|
+
|
|
8
|
+
function seedRunningTask(): string {
|
|
9
|
+
const pid = randomUUID();
|
|
10
|
+
const tid = randomUUID();
|
|
11
|
+
const now = new Date();
|
|
12
|
+
db.insert(projects)
|
|
13
|
+
.values({ id: pid, name: "p", status: "active", createdAt: now, updatedAt: now })
|
|
14
|
+
.run();
|
|
15
|
+
db.insert(tasks)
|
|
16
|
+
.values({
|
|
17
|
+
id: tid,
|
|
18
|
+
projectId: pid,
|
|
19
|
+
title: "t",
|
|
20
|
+
status: "running",
|
|
21
|
+
priority: 2,
|
|
22
|
+
resumeCount: 0,
|
|
23
|
+
createdAt: now,
|
|
24
|
+
updatedAt: now,
|
|
25
|
+
})
|
|
26
|
+
.run();
|
|
27
|
+
return tid;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("writeTerminalFailureReason", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
db.delete(tasks).run();
|
|
33
|
+
db.delete(projects).run();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("writes 'turn_limit_exceeded' on turn limit errors", async () => {
|
|
37
|
+
const tid = seedRunningTask();
|
|
38
|
+
await writeTerminalFailureReason(
|
|
39
|
+
tid,
|
|
40
|
+
new Error("Agent exhausted its turn limit (42 turns used)"),
|
|
41
|
+
);
|
|
42
|
+
const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
|
|
43
|
+
expect(row?.failureReason).toBe("turn_limit_exceeded");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("writes 'aborted' on AbortError", async () => {
|
|
47
|
+
const tid = seedRunningTask();
|
|
48
|
+
const err = new Error("aborted");
|
|
49
|
+
err.name = "AbortError";
|
|
50
|
+
await writeTerminalFailureReason(tid, err);
|
|
51
|
+
const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
|
|
52
|
+
expect(row?.failureReason).toBe("aborted");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("writes 'sdk_error' for unknown errors", async () => {
|
|
56
|
+
const tid = seedRunningTask();
|
|
57
|
+
await writeTerminalFailureReason(tid, new Error("something weird"));
|
|
58
|
+
const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
|
|
59
|
+
expect(row?.failureReason).toBe("sdk_error");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("writes 'rate_limited' on 429 errors", async () => {
|
|
63
|
+
const tid = seedRunningTask();
|
|
64
|
+
await writeTerminalFailureReason(tid, new Error("HTTP 429 rate limit"));
|
|
65
|
+
const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
|
|
66
|
+
expect(row?.failureReason).toBe("rate_limited");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -88,17 +88,6 @@ vi.mock("@/lib/constants/settings", () => ({
|
|
|
88
88
|
},
|
|
89
89
|
}));
|
|
90
90
|
|
|
91
|
-
vi.mock("@/lib/license/limit-check", () => ({
|
|
92
|
-
checkLimit: vi.fn().mockReturnValue({ allowed: true, current: 0, limit: 10, tier: "community", requiredTier: "community" }),
|
|
93
|
-
}));
|
|
94
|
-
|
|
95
|
-
vi.mock("@/lib/license/limit-queries", () => ({
|
|
96
|
-
getContextVersionCount: vi.fn().mockReturnValue(0),
|
|
97
|
-
}));
|
|
98
|
-
|
|
99
|
-
vi.mock("@/lib/license/notifications", () => ({
|
|
100
|
-
createTierLimitNotification: vi.fn().mockResolvedValue(undefined),
|
|
101
|
-
}));
|
|
102
91
|
|
|
103
92
|
// ─── Import under test ────────────────────────────────────────────────
|
|
104
93
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
mockAll,
|
|
5
|
+
mockLimit,
|
|
6
|
+
mockOrderBy,
|
|
7
|
+
mockWhere,
|
|
8
|
+
mockFrom,
|
|
9
|
+
mockSelect,
|
|
10
|
+
mockValues,
|
|
11
|
+
mockInsert,
|
|
12
|
+
mockSetWhere,
|
|
13
|
+
mockSet,
|
|
14
|
+
mockUpdate,
|
|
15
|
+
mockGetActiveLearnedContext,
|
|
16
|
+
mockCheckContextSize,
|
|
17
|
+
mockSummarizeContext,
|
|
18
|
+
} = vi.hoisted(() => {
|
|
19
|
+
const mockAll = vi.fn();
|
|
20
|
+
const mockLimit = vi.fn().mockReturnValue({ all: mockAll });
|
|
21
|
+
const mockOrderBy = vi.fn().mockReturnValue({ limit: mockLimit });
|
|
22
|
+
const mockWhere = vi.fn().mockReturnValue({ all: mockAll, orderBy: mockOrderBy });
|
|
23
|
+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
|
|
24
|
+
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
|
|
25
|
+
const mockValues = vi.fn().mockResolvedValue(undefined);
|
|
26
|
+
const mockInsert = vi.fn().mockReturnValue({ values: mockValues });
|
|
27
|
+
const mockSetWhere = vi.fn().mockResolvedValue(undefined);
|
|
28
|
+
const mockSet = vi.fn().mockReturnValue({ where: mockSetWhere });
|
|
29
|
+
const mockUpdate = vi.fn().mockReturnValue({ set: mockSet });
|
|
30
|
+
return {
|
|
31
|
+
mockAll,
|
|
32
|
+
mockLimit,
|
|
33
|
+
mockOrderBy,
|
|
34
|
+
mockWhere,
|
|
35
|
+
mockFrom,
|
|
36
|
+
mockSelect,
|
|
37
|
+
mockValues,
|
|
38
|
+
mockInsert,
|
|
39
|
+
mockSetWhere,
|
|
40
|
+
mockSet,
|
|
41
|
+
mockUpdate,
|
|
42
|
+
mockGetActiveLearnedContext: vi.fn(),
|
|
43
|
+
mockCheckContextSize: vi.fn(),
|
|
44
|
+
mockSummarizeContext: vi.fn(),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
vi.mock("@/lib/db", () => ({
|
|
49
|
+
db: {
|
|
50
|
+
select: mockSelect,
|
|
51
|
+
insert: mockInsert,
|
|
52
|
+
update: mockUpdate,
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
vi.mock("@/lib/db/schema", () => ({
|
|
57
|
+
learnedContext: {
|
|
58
|
+
id: "id",
|
|
59
|
+
profileId: "profile_id",
|
|
60
|
+
changeType: "change_type",
|
|
61
|
+
version: "version",
|
|
62
|
+
},
|
|
63
|
+
notifications: {
|
|
64
|
+
id: "id",
|
|
65
|
+
toolInput: "tool_input",
|
|
66
|
+
type: "type",
|
|
67
|
+
response: "response",
|
|
68
|
+
},
|
|
69
|
+
tasks: {
|
|
70
|
+
workflowId: "workflow_id",
|
|
71
|
+
id: "id",
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
vi.mock("drizzle-orm", () => ({
|
|
76
|
+
eq: vi.fn((_col: string, val: unknown) => ({ val })),
|
|
77
|
+
and: vi.fn((...conditions: unknown[]) => conditions),
|
|
78
|
+
desc: vi.fn((col: string) => ({ desc: col })),
|
|
79
|
+
isNull: vi.fn((col: string) => ({ isNull: col })),
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
vi.mock("../learned-context", () => ({
|
|
83
|
+
getActiveLearnedContext: mockGetActiveLearnedContext,
|
|
84
|
+
checkContextSize: mockCheckContextSize,
|
|
85
|
+
summarizeContext: mockSummarizeContext,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
import { batchApproveProposals } from "../learning-session";
|
|
89
|
+
|
|
90
|
+
describe("batchApproveProposals", () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.clearAllMocks();
|
|
93
|
+
mockSelect.mockReturnValue({ from: mockFrom });
|
|
94
|
+
mockFrom.mockReturnValue({ where: mockWhere });
|
|
95
|
+
mockWhere.mockReturnValue({ all: mockAll, orderBy: mockOrderBy });
|
|
96
|
+
mockOrderBy.mockReturnValue({ limit: mockLimit });
|
|
97
|
+
mockLimit.mockReturnValue({ all: mockAll });
|
|
98
|
+
mockInsert.mockReturnValue({ values: mockValues });
|
|
99
|
+
mockUpdate.mockReturnValue({ set: mockSet });
|
|
100
|
+
mockSet.mockReturnValue({ where: mockSetWhere });
|
|
101
|
+
mockValues.mockResolvedValue(undefined);
|
|
102
|
+
mockSetWhere.mockResolvedValue(undefined);
|
|
103
|
+
mockGetActiveLearnedContext.mockReturnValue("Existing context");
|
|
104
|
+
mockCheckContextSize.mockReturnValue({
|
|
105
|
+
currentSize: 9000,
|
|
106
|
+
limit: 8000,
|
|
107
|
+
needsSummarization: true,
|
|
108
|
+
});
|
|
109
|
+
mockSummarizeContext.mockResolvedValue(undefined);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("resolves without waiting for summarization and checks each profile once", async () => {
|
|
113
|
+
let releaseSummaries: (() => void) | null = null;
|
|
114
|
+
mockSummarizeContext.mockReturnValueOnce(
|
|
115
|
+
new Promise<void>((resolve) => {
|
|
116
|
+
releaseSummaries = resolve;
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
mockAll
|
|
121
|
+
.mockReturnValueOnce([
|
|
122
|
+
{
|
|
123
|
+
id: "proposal-1",
|
|
124
|
+
profileId: "general",
|
|
125
|
+
proposedAdditions: "First addition",
|
|
126
|
+
sourceTaskId: "task-1",
|
|
127
|
+
proposalNotificationId: null,
|
|
128
|
+
},
|
|
129
|
+
])
|
|
130
|
+
.mockReturnValueOnce([{ version: 2 }])
|
|
131
|
+
.mockReturnValueOnce([
|
|
132
|
+
{
|
|
133
|
+
id: "proposal-2",
|
|
134
|
+
profileId: "general",
|
|
135
|
+
proposedAdditions: "Second addition",
|
|
136
|
+
sourceTaskId: "task-2",
|
|
137
|
+
proposalNotificationId: null,
|
|
138
|
+
},
|
|
139
|
+
])
|
|
140
|
+
.mockReturnValueOnce([{ version: 3 }])
|
|
141
|
+
.mockReturnValueOnce([
|
|
142
|
+
{
|
|
143
|
+
id: "batch-notif-1",
|
|
144
|
+
toolInput: JSON.stringify({ proposalIds: ["proposal-1", "proposal-2"] }),
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const result = await batchApproveProposals(["proposal-1", "proposal-2"]);
|
|
149
|
+
|
|
150
|
+
expect(result).toBe(2);
|
|
151
|
+
expect(mockCheckContextSize).toHaveBeenCalledTimes(1);
|
|
152
|
+
expect(mockCheckContextSize).toHaveBeenCalledWith("general");
|
|
153
|
+
expect(mockSummarizeContext).toHaveBeenCalledTimes(1);
|
|
154
|
+
expect(mockSummarizeContext).toHaveBeenCalledWith("general");
|
|
155
|
+
|
|
156
|
+
releaseSummaries?.();
|
|
157
|
+
});
|
|
158
|
+
});
|