agentic-orchestrator 0.1.2 → 0.1.4
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/.claude/settings.local.json +15 -0
- package/CLAUDE.md +126 -0
- package/README.md +166 -25
- package/agentic/orchestrator/adapters.yaml +3 -0
- package/agentic/orchestrator/gates.yaml +47 -0
- package/agentic/orchestrator/policy.yaml +89 -0
- package/agentic/orchestrator/schemas/adapters.schema.json +12 -0
- package/agentic/orchestrator/schemas/gates.schema.json +6 -1
- package/agentic/orchestrator/schemas/index.schema.json +14 -0
- package/agentic/orchestrator/schemas/multi-project.schema.json +41 -0
- package/agentic/orchestrator/schemas/policy.schema.json +449 -52
- package/agentic/orchestrator/schemas/state.schema.json +16 -0
- package/agentic/orchestrator/tools/catalog.json +68 -0
- package/agentic/orchestrator/tools/schemas/input/cost.get.input.schema.json +10 -0
- package/agentic/orchestrator/tools/schemas/input/cost.record.input.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/input/feature.send_message.input.schema.json +11 -0
- package/agentic/orchestrator/tools/schemas/input/performance.get_analytics.input.schema.json +10 -0
- package/agentic/orchestrator/tools/schemas/input/performance.record_outcome.input.schema.json +18 -0
- package/agentic/orchestrator/tools/schemas/output/cost.get.output.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/output/cost.record.output.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/output/feature.ready_to_merge.output.schema.json +7 -0
- package/agentic/orchestrator/tools/schemas/output/feature.send_message.output.schema.json +23 -0
- package/agentic/orchestrator/tools/schemas/output/performance.get_analytics.output.schema.json +46 -0
- package/agentic/orchestrator/tools/schemas/output/performance.record_outcome.output.schema.json +10 -0
- package/agentic/orchestrator/tools.md +5 -0
- package/apps/control-plane/scripts/validate-architecture-rules.mjs +28 -2
- package/apps/control-plane/scripts/validate-docker-mcp-contract.mjs +12 -0
- package/apps/control-plane/scripts/validate-mcp-contracts.ts +92 -0
- package/apps/control-plane/src/application/adapters/adapter-registry.ts +169 -0
- package/apps/control-plane/src/application/multi-project-loader.ts +119 -0
- package/apps/control-plane/src/application/services/activity-monitor-service.ts +199 -0
- package/apps/control-plane/src/application/services/cost-tracking-service.ts +82 -0
- package/apps/control-plane/src/application/services/dependency-scheduler-service.ts +86 -0
- package/apps/control-plane/src/application/services/feature-deletion-service.ts +8 -7
- package/apps/control-plane/src/application/services/gate-interpolation-service.ts +15 -0
- package/apps/control-plane/src/application/services/gate-service.ts +38 -2
- package/apps/control-plane/src/application/services/instance-isolation-service.ts +18 -0
- package/apps/control-plane/src/application/services/issue-tracker-service.ts +469 -0
- package/apps/control-plane/src/application/services/merge-service.ts +67 -3
- package/apps/control-plane/src/application/services/notifier-service.ts +295 -0
- package/apps/control-plane/src/application/services/performance-analytics-service.ts +122 -0
- package/apps/control-plane/src/application/services/plan-service.ts +51 -0
- package/apps/control-plane/src/application/services/pr-monitor-service.ts +262 -0
- package/apps/control-plane/src/application/services/reactions-service.ts +175 -0
- package/apps/control-plane/src/application/services/reporting-service.ts +17 -2
- package/apps/control-plane/src/application/services/run-lease-service.ts +16 -38
- package/apps/control-plane/src/application/tools/tool-metadata.ts +4 -1
- package/apps/control-plane/src/cli/aop.ts +1 -1
- package/apps/control-plane/src/cli/attach-command-handler.ts +120 -0
- package/apps/control-plane/src/cli/cleanup-command-handler.ts +190 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +69 -3
- package/apps/control-plane/src/cli/dashboard-command-handler.ts +57 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +163 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +609 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +1 -0
- package/apps/control-plane/src/cli/retry-command-handler.ts +138 -0
- package/apps/control-plane/src/cli/run-command-handler.ts +115 -3
- package/apps/control-plane/src/cli/send-command-handler.ts +65 -0
- package/apps/control-plane/src/cli/status-command-handler.ts +102 -2
- package/apps/control-plane/src/cli/types.ts +26 -1
- package/apps/control-plane/src/core/constants.ts +8 -2
- package/apps/control-plane/src/core/error-codes.ts +3 -1
- package/apps/control-plane/src/core/gates.ts +170 -50
- package/apps/control-plane/src/core/kernel.ts +280 -5
- package/apps/control-plane/src/core/path-layout.ts +12 -0
- package/apps/control-plane/src/core/tool-caller.ts +36 -0
- package/apps/control-plane/src/core/workspace-hooks.ts +87 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +258 -9
- package/apps/control-plane/src/providers/providers.ts +235 -14
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +129 -8
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +123 -5
- package/apps/control-plane/src/supervisor/run-coordinator.ts +143 -6
- package/apps/control-plane/src/supervisor/runtime.ts +135 -6
- package/apps/control-plane/src/supervisor/types.ts +12 -21
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +8 -0
- package/apps/control-plane/test/activity-monitor.spec.ts +294 -0
- package/apps/control-plane/test/adapter-registry.spec.ts +132 -0
- package/apps/control-plane/test/batch-operations.spec.ts +112 -0
- package/apps/control-plane/test/bootstrap-attach.spec.ts +102 -0
- package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +252 -0
- package/apps/control-plane/test/bootstrap.spec.ts +560 -0
- package/apps/control-plane/test/cleanup-command.spec.ts +301 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +404 -1
- package/apps/control-plane/test/cli.unit.spec.ts +182 -1
- package/apps/control-plane/test/collision-queue.spec.ts +104 -1
- package/apps/control-plane/test/core-utils.spec.ts +175 -2
- package/apps/control-plane/test/cost-tracking.spec.ts +143 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +247 -0
- package/apps/control-plane/test/dashboard-client.spec.ts +116 -0
- package/apps/control-plane/test/dashboard-command.spec.ts +103 -0
- package/apps/control-plane/test/dependency-scheduler.spec.ts +189 -0
- package/apps/control-plane/test/epoch-tracking.spec.ts +4 -4
- package/apps/control-plane/test/feature-deletion-service.spec.ts +422 -0
- package/apps/control-plane/test/feature-lifecycle.spec.ts +202 -0
- package/apps/control-plane/test/git-spawn-error.spec.ts +24 -0
- package/apps/control-plane/test/incremental-gates.spec.ts +137 -0
- package/apps/control-plane/test/init-wizard.spec.ts +506 -0
- package/apps/control-plane/test/instance-isolation.spec.ts +83 -0
- package/apps/control-plane/test/issue-tracker.spec.ts +890 -0
- package/apps/control-plane/test/kernel.coverage.spec.ts +3 -5
- package/apps/control-plane/test/kernel.coverage2.spec.ts +871 -0
- package/apps/control-plane/test/kernel.spec.ts +13 -11
- package/apps/control-plane/test/lock-service.spec.ts +508 -0
- package/apps/control-plane/test/mcp-helpers.spec.ts +176 -0
- package/apps/control-plane/test/mcp.spec.ts +50 -15
- package/apps/control-plane/test/merge-service.spec.ts +67 -4
- package/apps/control-plane/test/multi-project.spec.ts +372 -0
- package/apps/control-plane/test/notifier-service.spec.ts +388 -0
- package/apps/control-plane/test/parallel-gates.spec.ts +312 -0
- package/apps/control-plane/test/patch-service.spec.ts +253 -0
- package/apps/control-plane/test/performance-analytics.spec.ts +338 -0
- package/apps/control-plane/test/planning-wave-executor.spec.ts +168 -0
- package/apps/control-plane/test/pr-monitor.spec.ts +385 -0
- package/apps/control-plane/test/providers.spec.ts +344 -1
- package/apps/control-plane/test/reactions.spec.ts +392 -0
- package/apps/control-plane/test/resume-command.spec.ts +390 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +481 -2
- package/apps/control-plane/test/schema-date-time.spec.ts +46 -0
- package/apps/control-plane/test/service-retry-paths.spec.ts +30 -0
- package/apps/control-plane/test/services.spec.ts +95 -2
- package/apps/control-plane/test/session-management.spec.ts +450 -0
- package/apps/control-plane/test/spec-ingestion.spec.ts +190 -0
- package/apps/control-plane/test/supervisor-collaborators.spec.ts +699 -2
- package/apps/control-plane/test/supervisor.spec.ts +36 -30
- package/apps/control-plane/test/supervisor.unit.spec.ts +405 -0
- package/apps/control-plane/test/worker-decision-loop.spec.ts +57 -0
- package/apps/control-plane/test/workspace-hooks.spec.ts +177 -0
- package/apps/control-plane/vitest.config.ts +21 -5
- package/dist/apps/control-plane/application/adapters/adapter-registry.d.ts +44 -0
- package/dist/apps/control-plane/application/adapters/adapter-registry.js +76 -0
- package/dist/apps/control-plane/application/adapters/adapter-registry.js.map +1 -0
- package/dist/apps/control-plane/application/multi-project-loader.d.ts +31 -0
- package/dist/apps/control-plane/application/multi-project-loader.js +82 -0
- package/dist/apps/control-plane/application/multi-project-loader.js.map +1 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +43 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js +132 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +28 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js +48 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.d.ts +26 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.js +75 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +2 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.js +6 -7
- package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/gate-interpolation-service.d.ts +7 -0
- package/dist/apps/control-plane/application/services/gate-interpolation-service.js +7 -0
- package/dist/apps/control-plane/application/services/gate-interpolation-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/gate-service.js +32 -2
- package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/instance-isolation-service.d.ts +11 -0
- package/dist/apps/control-plane/application/services/instance-isolation-service.js +17 -0
- package/dist/apps/control-plane/application/services/instance-isolation-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +65 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js +358 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/merge-service.d.ts +4 -0
- package/dist/apps/control-plane/application/services/merge-service.js +44 -2
- package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/notifier-service.d.ts +74 -0
- package/dist/apps/control-plane/application/services/notifier-service.js +212 -0
- package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +39 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js +75 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/plan-service.d.ts +1 -0
- package/dist/apps/control-plane/application/services/plan-service.js +53 -0
- package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +44 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js +192 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/reactions-service.d.ts +67 -0
- package/dist/apps/control-plane/application/services/reactions-service.js +114 -0
- package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/reporting-service.d.ts +1 -0
- package/dist/apps/control-plane/application/services/reporting-service.js +13 -2
- package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/run-lease-service.d.ts +2 -0
- package/dist/apps/control-plane/application/services/run-lease-service.js +14 -38
- package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
- package/dist/apps/control-plane/application/tools/tool-metadata.js +3 -1
- package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
- package/dist/apps/control-plane/cli/aop.d.ts +1 -1
- package/dist/apps/control-plane/cli/aop.js +1 -1
- package/dist/apps/control-plane/cli/attach-command-handler.d.ts +12 -0
- package/dist/apps/control-plane/cli/attach-command-handler.js +98 -0
- package/dist/apps/control-plane/cli/attach-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.d.ts +12 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.js +162 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +73 -3
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/dashboard-command-handler.d.ts +7 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js +45 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/help-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/help-command-handler.js +146 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/init-command-handler.d.ts +26 -0
- package/dist/apps/control-plane/cli/init-command-handler.js +517 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/resume-command-handler.js +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/retry-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/retry-command-handler.js +111 -0
- package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/run-command-handler.d.ts +5 -0
- package/dist/apps/control-plane/cli/run-command-handler.js +82 -3
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/send-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/send-command-handler.js +55 -0
- package/dist/apps/control-plane/cli/send-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/status-command-handler.d.ts +12 -1
- package/dist/apps/control-plane/cli/status-command-handler.js +55 -2
- package/dist/apps/control-plane/cli/status-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +26 -1
- package/dist/apps/control-plane/cli/types.js +15 -1
- package/dist/apps/control-plane/cli/types.js.map +1 -1
- package/dist/apps/control-plane/core/constants.d.ts +6 -0
- package/dist/apps/control-plane/core/constants.js +8 -2
- package/dist/apps/control-plane/core/constants.js.map +1 -1
- package/dist/apps/control-plane/core/error-codes.d.ts +2 -0
- package/dist/apps/control-plane/core/error-codes.js +3 -1
- package/dist/apps/control-plane/core/error-codes.js.map +1 -1
- package/dist/apps/control-plane/core/gates.d.ts +4 -0
- package/dist/apps/control-plane/core/gates.js +140 -43
- package/dist/apps/control-plane/core/gates.js.map +1 -1
- package/dist/apps/control-plane/core/kernel.d.ts +50 -1
- package/dist/apps/control-plane/core/kernel.js +220 -7
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/path-layout.d.ts +3 -0
- package/dist/apps/control-plane/core/path-layout.js +9 -0
- package/dist/apps/control-plane/core/path-layout.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +32 -0
- package/dist/apps/control-plane/core/tool-caller.js +2 -0
- package/dist/apps/control-plane/core/tool-caller.js.map +1 -0
- package/dist/apps/control-plane/core/workspace-hooks.d.ts +20 -0
- package/dist/apps/control-plane/core/workspace-hooks.js +69 -0
- package/dist/apps/control-plane/core/workspace-hooks.js.map +1 -0
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +245 -9
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +42 -3
- package/dist/apps/control-plane/providers/providers.js +216 -5
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +115 -6
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +109 -5
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +15 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +132 -6
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/runtime.js +110 -6
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +9 -16
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +5 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/eslint.config.mjs +2 -1
- package/package.json +12 -2
- package/packages/web-dashboard/next-env.d.ts +5 -0
- package/packages/web-dashboard/next.config.js +7 -0
- package/packages/web-dashboard/package.json +26 -0
- package/packages/web-dashboard/src/app/api/actions/route.ts +64 -0
- package/packages/web-dashboard/src/app/api/events/route.ts +51 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +256 -0
- package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +10 -0
- package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +25 -0
- package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/route.ts +16 -0
- package/packages/web-dashboard/src/app/api/projects/route.ts +31 -0
- package/packages/web-dashboard/src/app/api/status/route.ts +15 -0
- package/packages/web-dashboard/src/app/globals.css +2 -0
- package/packages/web-dashboard/src/app/layout.tsx +15 -0
- package/packages/web-dashboard/src/app/page.tsx +393 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +244 -0
- package/packages/web-dashboard/src/lib/multi-project-config.ts +116 -0
- package/packages/web-dashboard/src/lib/orchestrator-tools.ts +284 -0
- package/packages/web-dashboard/src/lib/types.ts +58 -0
- package/packages/web-dashboard/tsconfig.json +40 -0
- package/packages/web-dashboard/vitest.config.ts +6 -0
- package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +1764 -0
- package/spec-files/outstanding/agentic_orchestrator_enterprise_governance_dashboard_spec.md +348 -0
- package/spec-files/outstanding/agentic_orchestrator_knowledge_canary_spec.md +344 -0
- package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +374 -0
- package/spec-files/outstanding/agentic_orchestrator_performance_improvements_spec.md +1059 -0
- package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +466 -0
- package/spec-files/outstanding/agentic_orchestrator_quality_adoption_execution_spec.md +198 -0
- package/spec-files/outstanding/agentic_orchestrator_validator_hardening_spec.md +365 -0
- package/spec-files/progress.md +481 -52
- /package/spec-files/{agentic_orchestrator_cli_delete_command_spec.md → completed/agentic_orchestrator_cli_delete_command_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_dot_aop_generated_artifacts_spec.md → completed/agentic_orchestrator_dot_aop_generated_artifacts_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_mcp_formalization_spec.md → completed/agentic_orchestrator_mcp_formalization_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_oop_refactor_spec.md → completed/agentic_orchestrator_oop_refactor_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_single_global_orchestrator_spec.md → completed/agentic_orchestrator_single_global_orchestrator_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_spec.md → completed/agentic_orchestrator_spec.md} +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ActivityMonitorService } from '../src/application/services/activity-monitor-service.js';
|
|
3
|
+
import type { ActivitySnapshot } from '../src/application/services/activity-monitor-service.js';
|
|
4
|
+
import { TOOLS } from '../src/core/constants.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_THRESHOLD = 300_000;
|
|
7
|
+
const RECENT_TIMESTAMP = new Date(Date.now() - 60_000).toISOString(); // 1 minute ago
|
|
8
|
+
const STALE_TIMESTAMP = new Date(Date.now() - 600_000).toISOString(); // 10 minutes ago
|
|
9
|
+
|
|
10
|
+
function makeToolCaller(frontMatter: Record<string, unknown>) {
|
|
11
|
+
return {
|
|
12
|
+
callTool: vi.fn(async (_role: string, toolName: string) => {
|
|
13
|
+
if (toolName === TOOLS.FEATURE_STATE_GET) {
|
|
14
|
+
return { ok: true, data: { front_matter: { version: 1, ...frontMatter } } };
|
|
15
|
+
}
|
|
16
|
+
return { ok: true, data: {} };
|
|
17
|
+
})
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeNotifier() {
|
|
22
|
+
return { notify: vi.fn(async () => undefined) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('ActivityMonitorService', () => {
|
|
26
|
+
it('GIVEN feature with recent heartbeat WHEN getActivityState called THEN returns active', async () => {
|
|
27
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: RECENT_TIMESTAMP });
|
|
28
|
+
const service = new ActivityMonitorService({
|
|
29
|
+
toolCaller: toolCaller as never,
|
|
30
|
+
idleThresholdMs: DEFAULT_THRESHOLD
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const snapshot = await service.getActivityState('feature_a');
|
|
34
|
+
|
|
35
|
+
expect(snapshot.state).toBe('active');
|
|
36
|
+
expect(snapshot.featureId).toBe('feature_a');
|
|
37
|
+
expect(snapshot.lastEventAt).toBe(RECENT_TIMESTAMP);
|
|
38
|
+
expect(snapshot.detectedVia).toBe('jsonl');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('GIVEN feature with stale heartbeat WHEN getActivityState called THEN returns idle', async () => {
|
|
42
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: STALE_TIMESTAMP });
|
|
43
|
+
const service = new ActivityMonitorService({
|
|
44
|
+
toolCaller: toolCaller as never,
|
|
45
|
+
idleThresholdMs: DEFAULT_THRESHOLD
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const snapshot = await service.getActivityState('feature_b');
|
|
49
|
+
|
|
50
|
+
expect(snapshot.state).toBe('idle');
|
|
51
|
+
expect(snapshot.lastEventAt).toBe(STALE_TIMESTAMP);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('GIVEN feature with status=failed WHEN getActivityState called THEN returns exited', async () => {
|
|
55
|
+
const toolCaller = makeToolCaller({ status: 'failed' });
|
|
56
|
+
const service = new ActivityMonitorService({ toolCaller: toolCaller as never });
|
|
57
|
+
|
|
58
|
+
const snapshot = await service.getActivityState('feature_c');
|
|
59
|
+
|
|
60
|
+
expect(snapshot.state).toBe('exited');
|
|
61
|
+
expect(snapshot.lastEventAt).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('GIVEN feature with status=blocked WHEN getActivityState called THEN returns blocked', async () => {
|
|
65
|
+
const toolCaller = makeToolCaller({ status: 'blocked' });
|
|
66
|
+
const service = new ActivityMonitorService({ toolCaller: toolCaller as never });
|
|
67
|
+
|
|
68
|
+
const snapshot = await service.getActivityState('feature_d');
|
|
69
|
+
|
|
70
|
+
expect(snapshot.state).toBe('blocked');
|
|
71
|
+
expect(snapshot.lastEventAt).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('GIVEN feature with no timestamp WHEN getActivityState called THEN returns unknown', async () => {
|
|
75
|
+
const toolCaller = makeToolCaller({ status: 'planning' });
|
|
76
|
+
const service = new ActivityMonitorService({ toolCaller: toolCaller as never });
|
|
77
|
+
|
|
78
|
+
const snapshot = await service.getActivityState('feature_e');
|
|
79
|
+
|
|
80
|
+
expect(snapshot.state).toBe('unknown');
|
|
81
|
+
expect(snapshot.lastEventAt).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('GIVEN agent_stuck reaction enabled WHEN stuck detected THEN notifier called', async () => {
|
|
85
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: STALE_TIMESTAMP });
|
|
86
|
+
const notifier = makeNotifier();
|
|
87
|
+
const service = new ActivityMonitorService({
|
|
88
|
+
toolCaller: toolCaller as never,
|
|
89
|
+
notifier: notifier as never,
|
|
90
|
+
idleThresholdMs: DEFAULT_THRESHOLD,
|
|
91
|
+
reactionsPolicy: {
|
|
92
|
+
agent_stuck: { enabled: true, action: 'notify_only', idle_threshold_ms: DEFAULT_THRESHOLD, escalate_after: 2 }
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const stuck = await service.checkAndNotifyStuck('feature_f');
|
|
97
|
+
|
|
98
|
+
expect(stuck).toBe(true);
|
|
99
|
+
expect(notifier.notify).toHaveBeenCalledWith(
|
|
100
|
+
'agent_stuck',
|
|
101
|
+
expect.objectContaining({ feature_id: 'feature_f' })
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('GIVEN agent_stuck reaction disabled WHEN stuck detected THEN notifier not called', async () => {
|
|
106
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: STALE_TIMESTAMP });
|
|
107
|
+
const notifier = makeNotifier();
|
|
108
|
+
const service = new ActivityMonitorService({
|
|
109
|
+
toolCaller: toolCaller as never,
|
|
110
|
+
notifier: notifier as never,
|
|
111
|
+
idleThresholdMs: DEFAULT_THRESHOLD,
|
|
112
|
+
reactionsPolicy: {
|
|
113
|
+
agent_stuck: { enabled: false, action: 'notify_only', idle_threshold_ms: DEFAULT_THRESHOLD, escalate_after: 2 }
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const stuck = await service.checkAndNotifyStuck('feature_g');
|
|
118
|
+
|
|
119
|
+
expect(stuck).toBe(true);
|
|
120
|
+
expect(notifier.notify).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('GIVEN formatForStatus called WHEN state is idle THEN string includes idle', () => {
|
|
124
|
+
const service = new ActivityMonitorService({
|
|
125
|
+
toolCaller: { callTool: vi.fn() } as never
|
|
126
|
+
});
|
|
127
|
+
const snapshot: ActivitySnapshot = {
|
|
128
|
+
featureId: 'feature_h',
|
|
129
|
+
state: 'idle',
|
|
130
|
+
lastEventAt: new Date(Date.now() - 10 * 60_000).toISOString(),
|
|
131
|
+
detectedVia: 'jsonl'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const str = service.formatForStatus(snapshot);
|
|
135
|
+
|
|
136
|
+
expect(str).toContain('idle');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('GIVEN formatForStatus called WHEN state has no timestamp THEN returns state name', () => {
|
|
140
|
+
const service = new ActivityMonitorService({
|
|
141
|
+
toolCaller: { callTool: vi.fn() } as never
|
|
142
|
+
});
|
|
143
|
+
const snapshot: ActivitySnapshot = {
|
|
144
|
+
featureId: 'feature_i',
|
|
145
|
+
state: 'unknown',
|
|
146
|
+
lastEventAt: null,
|
|
147
|
+
detectedVia: 'unknown'
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const str = service.formatForStatus(snapshot);
|
|
151
|
+
|
|
152
|
+
expect(str).toBe('unknown');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('GIVEN checkAndNotifyStuck called WHEN feature is not idle THEN returns false', async () => {
|
|
156
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: RECENT_TIMESTAMP });
|
|
157
|
+
const notifier = makeNotifier();
|
|
158
|
+
const service = new ActivityMonitorService({
|
|
159
|
+
toolCaller: toolCaller as never,
|
|
160
|
+
notifier: notifier as never,
|
|
161
|
+
idleThresholdMs: DEFAULT_THRESHOLD,
|
|
162
|
+
reactionsPolicy: {
|
|
163
|
+
agent_stuck: { enabled: true, action: 'notify_only', idle_threshold_ms: DEFAULT_THRESHOLD, escalate_after: 2 }
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const stuck = await service.checkAndNotifyStuck('feature_j');
|
|
168
|
+
|
|
169
|
+
expect(stuck).toBe(false);
|
|
170
|
+
expect(notifier.notify).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('GIVEN_codex_rpc_detector_WHEN_last_run_is_recent_THEN_detected_via_process_and_state_active', async () => {
|
|
174
|
+
const toolCaller = makeToolCaller({ status: 'building', last_run_at: RECENT_TIMESTAMP });
|
|
175
|
+
const service = new ActivityMonitorService({
|
|
176
|
+
toolCaller: toolCaller as never,
|
|
177
|
+
detectorName: 'codex-rpc'
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const snapshot = await service.getActivityState('feature_codex');
|
|
181
|
+
|
|
182
|
+
expect(snapshot.state).toBe('active');
|
|
183
|
+
expect(snapshot.detectedVia).toBe('process');
|
|
184
|
+
expect(snapshot.lastEventAt).toBe(RECENT_TIMESTAMP);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('GIVEN_waiting_input_status_reason_WHEN_getActivityState_called_THEN_returns_waiting_input', async () => {
|
|
188
|
+
const toolCaller = makeToolCaller({
|
|
189
|
+
status: 'building',
|
|
190
|
+
status_reason: 'waiting_input:user_confirmation',
|
|
191
|
+
last_heartbeat_at: RECENT_TIMESTAMP
|
|
192
|
+
});
|
|
193
|
+
const service = new ActivityMonitorService({
|
|
194
|
+
toolCaller: toolCaller as never
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const snapshot = await service.getActivityState('feature_wait');
|
|
198
|
+
|
|
199
|
+
expect(snapshot.state).toBe('waiting_input');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('GIVEN_checkAndNotifyStuck_WHEN_snapshot_changes_THEN_activity_snapshot_persisted_to_state', async () => {
|
|
203
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: STALE_TIMESTAMP });
|
|
204
|
+
const service = new ActivityMonitorService({
|
|
205
|
+
toolCaller: toolCaller as never
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await service.checkAndNotifyStuck('feature_patch');
|
|
209
|
+
|
|
210
|
+
expect(toolCaller.callTool).toHaveBeenCalledWith(
|
|
211
|
+
'orchestrator',
|
|
212
|
+
TOOLS.FEATURE_STATE_PATCH,
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
feature_id: 'feature_patch',
|
|
215
|
+
patch: expect.objectContaining({
|
|
216
|
+
front_matter: expect.objectContaining({
|
|
217
|
+
activity_state: 'idle',
|
|
218
|
+
activity_detected_via: 'jsonl'
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('ActivityMonitorService detectedVia and resolveLastEvent branches', () => {
|
|
227
|
+
it('GIVEN_codex_rpc_detector_with_ledger_entry_WHEN_getActivityState_called_THEN_detectedVia_is_ledger', async () => {
|
|
228
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: RECENT_TIMESTAMP });
|
|
229
|
+
const operationLedger = { getLastOperationAt: vi.fn(() => RECENT_TIMESTAMP) };
|
|
230
|
+
const service = new ActivityMonitorService({
|
|
231
|
+
toolCaller: toolCaller as never,
|
|
232
|
+
detectorName: 'codex-rpc',
|
|
233
|
+
operationLedger
|
|
234
|
+
});
|
|
235
|
+
const snapshot = await service.getActivityState('feature_ledger');
|
|
236
|
+
expect(snapshot.detectedVia).toBe('ledger');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('GIVEN_process_heuristic_detector_WHEN_getActivityState_called_THEN_detectedVia_is_process', async () => {
|
|
240
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: RECENT_TIMESTAMP });
|
|
241
|
+
const service = new ActivityMonitorService({
|
|
242
|
+
toolCaller: toolCaller as never,
|
|
243
|
+
detectorName: 'process-heuristic'
|
|
244
|
+
});
|
|
245
|
+
const snapshot = await service.getActivityState('feature_proc');
|
|
246
|
+
expect(snapshot.detectedVia).toBe('process');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('GIVEN_codex_rpc_without_ledger_WHEN_getActivityState_called_THEN_detectedVia_is_process', async () => {
|
|
250
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: RECENT_TIMESTAMP });
|
|
251
|
+
const operationLedger = { getLastOperationAt: vi.fn(() => null) };
|
|
252
|
+
const service = new ActivityMonitorService({
|
|
253
|
+
toolCaller: toolCaller as never,
|
|
254
|
+
detectorName: 'codex-rpc',
|
|
255
|
+
operationLedger
|
|
256
|
+
});
|
|
257
|
+
const snapshot = await service.getActivityState('feature_norpc');
|
|
258
|
+
expect(snapshot.detectedVia).toBe('process');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('GIVEN_process_heuristic_with_last_run_at_WHEN_getActivityState_THEN_uses_last_run_at', async () => {
|
|
262
|
+
const toolCaller = makeToolCaller({ status: 'building', last_run_at: RECENT_TIMESTAMP, last_heartbeat_at: undefined });
|
|
263
|
+
const service = new ActivityMonitorService({
|
|
264
|
+
toolCaller: toolCaller as never,
|
|
265
|
+
detectorName: 'process-heuristic'
|
|
266
|
+
});
|
|
267
|
+
const snapshot = await service.getActivityState('feature_run_at');
|
|
268
|
+
expect(snapshot.lastEventAt).toBe(RECENT_TIMESTAMP);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('GIVEN_unknown_detector_name_WHEN_getActivityState_called_THEN_detectedVia_is_unknown', async () => {
|
|
272
|
+
const toolCaller = makeToolCaller({ status: 'building', last_heartbeat_at: RECENT_TIMESTAMP });
|
|
273
|
+
const service = new ActivityMonitorService({
|
|
274
|
+
toolCaller: toolCaller as never,
|
|
275
|
+
detectorName: 'some-unknown-detector'
|
|
276
|
+
});
|
|
277
|
+
const snapshot = await service.getActivityState('feature_unknown_detector');
|
|
278
|
+
expect(snapshot.detectedVia).toBe('unknown');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('GIVEN_state_patch_throws_WHEN_checkAndNotifyStuck_called_THEN_silently_continues', async () => {
|
|
282
|
+
const toolCaller = {
|
|
283
|
+
callTool: vi.fn(async (_role: string, toolName: string) => {
|
|
284
|
+
if (toolName === TOOLS.FEATURE_STATE_GET) {
|
|
285
|
+
return { ok: true, data: { front_matter: { version: 1, status: 'building', last_heartbeat_at: STALE_TIMESTAMP } } };
|
|
286
|
+
}
|
|
287
|
+
throw new Error('patch failed');
|
|
288
|
+
})
|
|
289
|
+
};
|
|
290
|
+
const service = new ActivityMonitorService({ toolCaller: toolCaller as never });
|
|
291
|
+
// Should not throw even when persistSnapshot fails - still returns stuck status
|
|
292
|
+
await expect(service.checkAndNotifyStuck('feature_patch_fail')).resolves.not.toThrow();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
DefaultAdapterRegistry,
|
|
4
|
+
AGENT_PROVIDER_SLOT,
|
|
5
|
+
globalAdapterRegistry,
|
|
6
|
+
REGISTERED_PROVIDER_NAMES
|
|
7
|
+
} from '../src/application/adapters/adapter-registry.js';
|
|
8
|
+
import { SUPPORTED_PROVIDERS } from '../src/providers/providers.js';
|
|
9
|
+
import { ERROR_CODES } from '../src/core/error-codes.js';
|
|
10
|
+
|
|
11
|
+
describe('DefaultAdapterRegistry', () => {
|
|
12
|
+
describe('GIVEN_empty_registry_WHEN_has_is_called_THEN_returns_false', () => {
|
|
13
|
+
it('returns false for any slot/name', () => {
|
|
14
|
+
const registry = new DefaultAdapterRegistry();
|
|
15
|
+
expect(registry.has('agent-provider', 'codex')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('GIVEN_empty_registry_WHEN_list_is_called_THEN_returns_empty_array', () => {
|
|
20
|
+
it('returns empty array', () => {
|
|
21
|
+
const registry = new DefaultAdapterRegistry();
|
|
22
|
+
expect(registry.list('agent-provider')).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('GIVEN_registered_adapter_WHEN_has_is_called_THEN_returns_true', () => {
|
|
27
|
+
it('finds the registered adapter', () => {
|
|
28
|
+
const registry = new DefaultAdapterRegistry();
|
|
29
|
+
const slot = { name: 'test-slot' };
|
|
30
|
+
registry.register(slot, { slot: 'test-slot', name: 'my-adapter' }, () => ({ value: 42 }));
|
|
31
|
+
expect(registry.has('test-slot', 'my-adapter')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('GIVEN_registered_adapter_WHEN_list_is_called_THEN_returns_manifest', () => {
|
|
36
|
+
it('lists the manifest', () => {
|
|
37
|
+
const registry = new DefaultAdapterRegistry();
|
|
38
|
+
const slot = { name: 'test-slot' };
|
|
39
|
+
const manifest = { slot: 'test-slot', name: 'my-adapter' };
|
|
40
|
+
registry.register(slot, manifest, () => ({ value: 42 }));
|
|
41
|
+
expect(registry.list('test-slot')).toEqual([manifest]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('GIVEN_registered_adapter_WHEN_resolve_is_called_THEN_factory_is_invoked', () => {
|
|
46
|
+
it('calls factory with config and returns result', () => {
|
|
47
|
+
const registry = new DefaultAdapterRegistry();
|
|
48
|
+
const slot = { name: 'compute-slot' };
|
|
49
|
+
registry.register(slot, { slot: 'compute-slot', name: 'fast' }, (config) => ({
|
|
50
|
+
speed: (config as { speed: number }).speed
|
|
51
|
+
}));
|
|
52
|
+
const result = registry.resolve(slot, 'fast', { speed: 100 });
|
|
53
|
+
expect(result).toEqual({ speed: 100 });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('GIVEN_unknown_adapter_WHEN_resolve_is_called_THEN_throws_structured_error', () => {
|
|
58
|
+
it('throws with UNSUPPORTED_AGENT_PROVIDER code', () => {
|
|
59
|
+
const registry = new DefaultAdapterRegistry();
|
|
60
|
+
const slot = { name: 'missing-slot' };
|
|
61
|
+
expect(() => registry.resolve(slot, 'ghost', {})).toThrowError(
|
|
62
|
+
expect.objectContaining({ code: ERROR_CODES.UNSUPPORTED_AGENT_PROVIDER })
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('includes slot and name in error details', () => {
|
|
67
|
+
const registry = new DefaultAdapterRegistry();
|
|
68
|
+
const slot = { name: 'missing-slot' };
|
|
69
|
+
let caught: unknown;
|
|
70
|
+
try {
|
|
71
|
+
registry.resolve(slot, 'ghost', {});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
caught = e;
|
|
74
|
+
}
|
|
75
|
+
expect((caught as { details: Record<string, unknown> }).details).toMatchObject({
|
|
76
|
+
slot: 'missing-slot',
|
|
77
|
+
name: 'ghost'
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('GIVEN_multiple_adapters_in_same_slot_WHEN_list_is_called_THEN_all_manifests_returned', () => {
|
|
83
|
+
it('returns all registered manifests', () => {
|
|
84
|
+
const registry = new DefaultAdapterRegistry();
|
|
85
|
+
const slot = { name: 'multi-slot' };
|
|
86
|
+
registry.register(slot, { slot: 'multi-slot', name: 'a' }, () => 'a');
|
|
87
|
+
registry.register(slot, { slot: 'multi-slot', name: 'b' }, () => 'b');
|
|
88
|
+
registry.register(slot, { slot: 'multi-slot', name: 'c' }, () => 'c');
|
|
89
|
+
expect(registry.list('multi-slot').map((m) => m.name)).toEqual(
|
|
90
|
+
expect.arrayContaining(['a', 'b', 'c'])
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('AGENT_PROVIDER_SLOT', () => {
|
|
97
|
+
describe('GIVEN_global_registry_WHEN_listing_providers_THEN_all_built_ins_present', () => {
|
|
98
|
+
const expectedProviders = ['codex', 'claude', 'gemini', 'kiro-cli', 'copilot', 'custom'];
|
|
99
|
+
|
|
100
|
+
it.each(expectedProviders)('contains built-in provider: %s', (name) => {
|
|
101
|
+
expect(globalAdapterRegistry.has(AGENT_PROVIDER_SLOT.name, name)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('lists exactly the expected built-in providers', () => {
|
|
105
|
+
const names = globalAdapterRegistry.list(AGENT_PROVIDER_SLOT.name).map((m) => m.name);
|
|
106
|
+
expect(names.sort()).toEqual(expectedProviders.sort());
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('GIVEN_global_registry_WHEN_resolving_provider_THEN_returns_contract', () => {
|
|
111
|
+
it('resolve returns object with correct name', () => {
|
|
112
|
+
const result = globalAdapterRegistry.resolve(AGENT_PROVIDER_SLOT, 'codex', {});
|
|
113
|
+
expect(result).toMatchObject({ name: 'codex' });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('REGISTERED_PROVIDER_NAMES parity with SUPPORTED_PROVIDERS', () => {
|
|
119
|
+
describe('GIVEN_adapter_registry_and_providers_WHEN_comparing_sets_THEN_they_match', () => {
|
|
120
|
+
it('SUPPORTED_PROVIDERS contains all names from adapter registry', () => {
|
|
121
|
+
for (const name of REGISTERED_PROVIDER_NAMES) {
|
|
122
|
+
expect(SUPPORTED_PROVIDERS.has(name)).toBe(true);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('adapter registry contains all names from SUPPORTED_PROVIDERS', () => {
|
|
127
|
+
for (const name of SUPPORTED_PROVIDERS) {
|
|
128
|
+
expect(REGISTERED_PROVIDER_NAMES.has(name)).toBe(true);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { StatusCommandHandler } from '../src/cli/status-command-handler.js';
|
|
3
|
+
import { TOOLS } from '../src/core/constants.js';
|
|
4
|
+
import type { RunCommandContext } from '../src/cli/run-command-handler.js';
|
|
5
|
+
|
|
6
|
+
const fsReadFileMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
const specResolverResolveMock = vi.hoisted(() => vi.fn());
|
|
8
|
+
|
|
9
|
+
vi.mock('node:fs/promises', () => ({
|
|
10
|
+
default: {
|
|
11
|
+
readFile: fsReadFileMock
|
|
12
|
+
}
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('../src/cli/spec-input-resolver.js', () => ({
|
|
16
|
+
SpecInputResolver: vi.fn().mockImplementation(() => ({
|
|
17
|
+
resolve: specResolverResolveMock
|
|
18
|
+
}))
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { RunCommandHandler } from '../src/cli/run-command-handler.js';
|
|
22
|
+
|
|
23
|
+
function makeContext(toolCallImpl = vi.fn(async () => ({ ok: true, data: {} }))): RunCommandContext {
|
|
24
|
+
return {
|
|
25
|
+
repoRoot: '/repo',
|
|
26
|
+
env: {},
|
|
27
|
+
runId: 'run:test',
|
|
28
|
+
transport: 'inprocess' as const,
|
|
29
|
+
options: { command: 'run', batch: true, folder_input: '/specs' },
|
|
30
|
+
kernel: { getAgentsConfig: () => ({}) } as never,
|
|
31
|
+
toolClient: { call: toolCallImpl } as never
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('BatchOperations', () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('GIVEN_folder_with_specs_WHEN_batch_run_all_new_THEN_all_created', async () => {
|
|
41
|
+
specResolverResolveMock.mockResolvedValue([
|
|
42
|
+
'/repo/.aop/features/feature_a/spec.md',
|
|
43
|
+
'/repo/.aop/features/feature_b/spec.md'
|
|
44
|
+
]);
|
|
45
|
+
fsReadFileMock.mockResolvedValue(JSON.stringify({ active: [], blocked: [] }));
|
|
46
|
+
|
|
47
|
+
const toolCallMock = vi.fn(async () => ({ ok: true, data: {} }));
|
|
48
|
+
const handler = new RunCommandHandler(0);
|
|
49
|
+
const result = await handler.execute(makeContext(toolCallMock));
|
|
50
|
+
|
|
51
|
+
expect(result).toMatchObject({
|
|
52
|
+
ok: true,
|
|
53
|
+
data: { command: 'batch-run', created: 2, skipped: 0, failed: 0 }
|
|
54
|
+
});
|
|
55
|
+
expect(toolCallMock).toHaveBeenCalledWith(
|
|
56
|
+
TOOLS.FEATURE_INIT,
|
|
57
|
+
expect.objectContaining({ feature_id: 'feature_a' }),
|
|
58
|
+
expect.any(Object)
|
|
59
|
+
);
|
|
60
|
+
expect(toolCallMock).toHaveBeenCalledWith(
|
|
61
|
+
TOOLS.FEATURE_INIT,
|
|
62
|
+
expect.objectContaining({ feature_id: 'feature_b' }),
|
|
63
|
+
expect.any(Object)
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('GIVEN_folder_with_specs_WHEN_batch_run_some_active_THEN_active_skipped', async () => {
|
|
68
|
+
specResolverResolveMock.mockResolvedValue([
|
|
69
|
+
'/repo/.aop/features/feature_a/spec.md',
|
|
70
|
+
'/repo/.aop/features/feature_b/spec.md'
|
|
71
|
+
]);
|
|
72
|
+
fsReadFileMock.mockResolvedValue(JSON.stringify({ active: ['feature_a'], blocked: [] }));
|
|
73
|
+
|
|
74
|
+
const toolCallMock = vi.fn(async () => ({ ok: true, data: {} }));
|
|
75
|
+
const handler = new RunCommandHandler(0);
|
|
76
|
+
const result = await handler.execute(makeContext(toolCallMock));
|
|
77
|
+
|
|
78
|
+
expect(result).toMatchObject({
|
|
79
|
+
ok: true,
|
|
80
|
+
data: { command: 'batch-run', created: 1, skipped: 1, failed: 0 }
|
|
81
|
+
});
|
|
82
|
+
expect(toolCallMock).not.toHaveBeenCalledWith(
|
|
83
|
+
TOOLS.FEATURE_INIT,
|
|
84
|
+
expect.objectContaining({ feature_id: 'feature_a' }),
|
|
85
|
+
expect.any(Object)
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('GIVEN_status_with_summary_flag_WHEN_called_THEN_compact_output', async () => {
|
|
90
|
+
const dashboardCall = vi.fn(async () => ({
|
|
91
|
+
ok: true,
|
|
92
|
+
data: {
|
|
93
|
+
features: [
|
|
94
|
+
{ feature_id: 'feature_a', status: 'planning' },
|
|
95
|
+
{ feature_id: 'feature_b', status: 'building' }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
const handler = new StatusCommandHandler({ call: dashboardCall } as never, 'run:test');
|
|
100
|
+
const result = await handler.execute({ summary: true });
|
|
101
|
+
|
|
102
|
+
expect(result).toMatchObject({
|
|
103
|
+
ok: true,
|
|
104
|
+
data: {
|
|
105
|
+
summary: [
|
|
106
|
+
{ feature_id: 'feature_a', status: 'planning', activity: 'unknown' },
|
|
107
|
+
{ feature_id: 'feature_b', status: 'building', activity: 'unknown' }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused bootstrap tests for the Attach command success path (lines 321-323 in bootstrap.ts).
|
|
3
|
+
* Uses a mocked AttachCommandHandler to avoid needing a full provider setup.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
const ensureLoadedMock = vi.hoisted(() => vi.fn(async () => undefined));
|
|
8
|
+
const createToolingRuntimeMock = vi.hoisted(() => vi.fn(async () => ({ runtime: 'ok' })));
|
|
9
|
+
const resolveToolClientMock = vi.hoisted(() => vi.fn());
|
|
10
|
+
const toolCallMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, data: {} })));
|
|
11
|
+
|
|
12
|
+
vi.mock('node:readline/promises', () => ({
|
|
13
|
+
default: {
|
|
14
|
+
createInterface: vi.fn(() => ({
|
|
15
|
+
question: vi.fn(async () => ''),
|
|
16
|
+
close: vi.fn()
|
|
17
|
+
}))
|
|
18
|
+
}
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('../src/core/kernel.js', () => {
|
|
22
|
+
class AopKernel {
|
|
23
|
+
private readonly repoRoot: string;
|
|
24
|
+
constructor(repoRoot: string) { this.repoRoot = repoRoot; }
|
|
25
|
+
async ensureLoaded() { await ensureLoadedMock(); }
|
|
26
|
+
getRepoRoot() { return this.repoRoot; }
|
|
27
|
+
getAgentsConfig() { return {}; }
|
|
28
|
+
getRbacPolicy() { return {}; }
|
|
29
|
+
getPolicySnapshot() { return {}; }
|
|
30
|
+
setProvider() {}
|
|
31
|
+
}
|
|
32
|
+
return { AopKernel };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock('../src/mcp/runtime-factory.js', () => ({
|
|
36
|
+
createToolingRuntime: createToolingRuntimeMock,
|
|
37
|
+
resolveToolClient: resolveToolClientMock
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock('../src/cli/init-command-handler.js', () => ({
|
|
41
|
+
InitCommandHandler: vi.fn().mockImplementation(() => ({
|
|
42
|
+
execute: vi.fn(async () => ({ ok: true, data: { command: 'init', created: [], updated: [], skipped: [], validation_warnings: [], next_steps: [] } }))
|
|
43
|
+
}))
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
vi.mock('../src/cli/dashboard-command-handler.js', () => ({
|
|
47
|
+
DashboardCommandHandler: vi.fn().mockImplementation(() => ({
|
|
48
|
+
execute: vi.fn(async () => ({ ok: true, data: { message: 'started', port: 3000 } }))
|
|
49
|
+
}))
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('../src/cli/run-command-handler.js', () => ({
|
|
53
|
+
RunCommandHandler: vi.fn().mockImplementation(() => ({
|
|
54
|
+
execute: vi.fn(async () => ({ ok: true, data: { status: 'running' } }))
|
|
55
|
+
}))
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock('../src/cli/resume-command-handler.js', () => ({
|
|
59
|
+
ResumeCommandHandler: vi.fn().mockImplementation(() => ({
|
|
60
|
+
execute: vi.fn(async () => ({ ok: true, data: { resumed: false } }))
|
|
61
|
+
}))
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
vi.mock('../src/cli/attach-command-handler.js', () => ({
|
|
65
|
+
AttachCommandHandler: vi.fn().mockImplementation(() => ({
|
|
66
|
+
execute: vi.fn(async () => ({
|
|
67
|
+
ok: true,
|
|
68
|
+
data: { command: 'attach', feature_id: 'feat_x', status: 'building', role: 'builder', session_id: 'sess-1', attached: true }
|
|
69
|
+
}))
|
|
70
|
+
}))
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
describe('bootstrap runCli attach success path', () => {
|
|
74
|
+
let stdoutSpy: ReturnType<typeof vi.spyOn>;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
resolveToolClientMock.mockReturnValue({ call: toolCallMock });
|
|
78
|
+
toolCallMock.mockClear();
|
|
79
|
+
ensureLoadedMock.mockClear();
|
|
80
|
+
createToolingRuntimeMock.mockClear();
|
|
81
|
+
stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
stdoutSpy.mockRestore();
|
|
86
|
+
vi.clearAllMocks();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('GIVEN_attach_command_with_feature_id_WHEN_executed_THEN_returns_exit_code_0', async () => {
|
|
90
|
+
const { runCli } = await import('../src/interfaces/cli/bootstrap.js');
|
|
91
|
+
const code = await runCli(['attach', '--feature-id', 'feat_x'], {
|
|
92
|
+
cwd: '/tmp',
|
|
93
|
+
env: { ...process.env, AOP_AGENT_PROVIDER: 'custom' }
|
|
94
|
+
});
|
|
95
|
+
// Attach handler mock returns successfully → printPayload → return 0
|
|
96
|
+
expect(code).toBe(0);
|
|
97
|
+
const writes = stdoutSpy.mock.calls.flatMap((call) => {
|
|
98
|
+
try { return [JSON.parse(String(call[0]))]; } catch { return []; }
|
|
99
|
+
});
|
|
100
|
+
expect(writes[0]).toMatchObject({ ok: true, data: { command: 'attach' } });
|
|
101
|
+
});
|
|
102
|
+
});
|