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,247 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
const resolveProjectRootMock = vi.hoisted(() => vi.fn(async () => '/tmp'));
|
|
7
|
+
const readDashboardStatusMock = vi.hoisted(() =>
|
|
8
|
+
vi.fn(async () => ({ index: { active: [], blocked: [], merged: [], blocked_queue: [] }, features: [] }))
|
|
9
|
+
);
|
|
10
|
+
const readFeatureStateMock = vi.hoisted(() => vi.fn(async () => null));
|
|
11
|
+
const getAopRootMock = vi.hoisted(() => vi.fn(() => process.cwd()));
|
|
12
|
+
const approveFeatureReviewMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, data: { merged: true } })));
|
|
13
|
+
const denyFeatureReviewMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, data: { blocked: true } })));
|
|
14
|
+
const requestFeatureChangesMock = vi.hoisted(() =>
|
|
15
|
+
vi.fn(async () => ({ ok: true, data: { delivered: true } }))
|
|
16
|
+
);
|
|
17
|
+
const execFileMock = vi.hoisted(() => vi.fn());
|
|
18
|
+
|
|
19
|
+
vi.mock('../../../packages/web-dashboard/src/lib/aop-client.js', () => ({
|
|
20
|
+
resolveProjectRoot: resolveProjectRootMock,
|
|
21
|
+
readDashboardStatus: readDashboardStatusMock,
|
|
22
|
+
readFeatureState: readFeatureStateMock,
|
|
23
|
+
getAopRoot: getAopRootMock
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('../../../packages/web-dashboard/src/lib/orchestrator-tools.js', () => ({
|
|
27
|
+
approveFeatureReview: approveFeatureReviewMock,
|
|
28
|
+
denyFeatureReview: denyFeatureReviewMock,
|
|
29
|
+
requestFeatureChanges: requestFeatureChangesMock
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('node:child_process', () => ({
|
|
33
|
+
execFile: (...args: unknown[]) => {
|
|
34
|
+
const callback = args[args.length - 1] as (err: null | Error, result?: { stdout: string; stderr: string }) => void;
|
|
35
|
+
const call = execFileMock(...args.slice(0, -1)) as Promise<{ stdout: string; stderr: string }>;
|
|
36
|
+
void call.then(
|
|
37
|
+
(result) => callback(null, result),
|
|
38
|
+
(error: Error) => callback(error)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
import { POST as actionsPost } from '../../../packages/web-dashboard/src/app/api/actions/route.js';
|
|
44
|
+
import { POST as checkoutPost } from '../../../packages/web-dashboard/src/app/api/features/[id]/checkout/route.js';
|
|
45
|
+
import { GET as statusGet } from '../../../packages/web-dashboard/src/app/api/status/route.js';
|
|
46
|
+
import { GET as projectsGet } from '../../../packages/web-dashboard/src/app/api/projects/route.js';
|
|
47
|
+
|
|
48
|
+
describe('dashboard api integration', () => {
|
|
49
|
+
let repoRoot: string;
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dashboard-api-'));
|
|
53
|
+
await fs.mkdir(path.join(repoRoot, '.git'), { recursive: true });
|
|
54
|
+
await fs.mkdir(path.join(repoRoot, '.aop', 'runtime'), { recursive: true });
|
|
55
|
+
await fs.mkdir(path.join(repoRoot, '.worktrees', 'feature_checkout'), { recursive: true });
|
|
56
|
+
|
|
57
|
+
resolveProjectRootMock.mockReset();
|
|
58
|
+
readDashboardStatusMock.mockReset();
|
|
59
|
+
readFeatureStateMock.mockReset();
|
|
60
|
+
getAopRootMock.mockReset();
|
|
61
|
+
approveFeatureReviewMock.mockReset();
|
|
62
|
+
denyFeatureReviewMock.mockReset();
|
|
63
|
+
requestFeatureChangesMock.mockReset();
|
|
64
|
+
execFileMock.mockReset();
|
|
65
|
+
|
|
66
|
+
resolveProjectRootMock.mockResolvedValue(repoRoot);
|
|
67
|
+
getAopRootMock.mockReturnValue(repoRoot);
|
|
68
|
+
readDashboardStatusMock.mockResolvedValue({
|
|
69
|
+
index: { active: ['feature_checkout'], blocked: [], merged: [], blocked_queue: [] },
|
|
70
|
+
features: [
|
|
71
|
+
{
|
|
72
|
+
id: 'feature_checkout',
|
|
73
|
+
feature_id: 'feature_checkout',
|
|
74
|
+
status: 'ready_to_merge',
|
|
75
|
+
phase: 'ready_to_merge',
|
|
76
|
+
branch: 'feature/feature_checkout',
|
|
77
|
+
worktree_path: path.join(repoRoot, '.worktrees', 'feature_checkout'),
|
|
78
|
+
pr: null
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(async () => {
|
|
85
|
+
await fs.rm(repoRoot, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('GIVEN_review_approve_action_WHEN_posted_THEN_routes_to_feature_ready_to_merge', async () => {
|
|
89
|
+
const response = await actionsPost(
|
|
90
|
+
new Request('http://localhost/api/actions?project=alpha', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
action: 'review.approve',
|
|
94
|
+
feature_id: 'feature_checkout',
|
|
95
|
+
approval_token: 'approved'
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(response.status).toBe(200);
|
|
101
|
+
expect(approveFeatureReviewMock).toHaveBeenCalledWith(
|
|
102
|
+
'feature_checkout',
|
|
103
|
+
'approved',
|
|
104
|
+
'merge_commit',
|
|
105
|
+
undefined,
|
|
106
|
+
repoRoot
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('GIVEN_review_request_changes_WHEN_posted_THEN_routes_to_feature_send_message_flow', async () => {
|
|
111
|
+
const response = await actionsPost(
|
|
112
|
+
new Request('http://localhost/api/actions', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
action: 'review.request_changes',
|
|
116
|
+
feature_id: 'feature_checkout',
|
|
117
|
+
message: 'Please address CI failures'
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(response.status).toBe(200);
|
|
123
|
+
expect(requestFeatureChangesMock).toHaveBeenCalledWith('feature_checkout', 'Please address CI failures', repoRoot);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('GIVEN_review_deny_without_reason_WHEN_posted_THEN_returns_400', async () => {
|
|
127
|
+
const response = await actionsPost(
|
|
128
|
+
new Request('http://localhost/api/actions', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
action: 'review.deny',
|
|
132
|
+
feature_id: 'feature_checkout'
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const body = (await response.json()) as { ok: boolean; error: { code: string } };
|
|
138
|
+
expect(response.status).toBe(400);
|
|
139
|
+
expect(body.ok).toBe(false);
|
|
140
|
+
expect(body.error.code).toBe('reason_required');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('GIVEN_checkout_then_restore_WHEN_feature_branch_exists_THEN_persists_and_restores_checkout_record', async () => {
|
|
144
|
+
readFeatureStateMock.mockResolvedValue({
|
|
145
|
+
id: 'feature_checkout',
|
|
146
|
+
feature_id: 'feature_checkout',
|
|
147
|
+
status: 'ready_to_merge',
|
|
148
|
+
phase: 'ready_to_merge',
|
|
149
|
+
branch: 'feature/feature_checkout',
|
|
150
|
+
worktree_path: path.join(repoRoot, '.worktrees', 'feature_checkout'),
|
|
151
|
+
pr: null
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
execFileMock.mockImplementation(async (_file: string, args: string[]) => {
|
|
155
|
+
const command = args.join(' ');
|
|
156
|
+
if (command.startsWith('show-ref --verify --quiet refs/heads/feature/feature_checkout')) {
|
|
157
|
+
return { stdout: '', stderr: '' };
|
|
158
|
+
}
|
|
159
|
+
if (command === 'status --porcelain') {
|
|
160
|
+
return { stdout: '', stderr: '' };
|
|
161
|
+
}
|
|
162
|
+
if (command === 'rev-parse --abbrev-ref HEAD') {
|
|
163
|
+
return { stdout: 'main\n', stderr: '' };
|
|
164
|
+
}
|
|
165
|
+
if (command === 'checkout feature/feature_checkout') {
|
|
166
|
+
return { stdout: '', stderr: '' };
|
|
167
|
+
}
|
|
168
|
+
if (command === 'checkout main') {
|
|
169
|
+
return { stdout: '', stderr: '' };
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`unexpected git command: ${command}`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const checkoutResponse = await checkoutPost(
|
|
175
|
+
new Request('http://localhost/api/features/feature_checkout/checkout?project=alpha', {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
body: JSON.stringify({ action: 'checkout', stash_changes: false })
|
|
178
|
+
}),
|
|
179
|
+
{ params: { id: 'feature_checkout' } }
|
|
180
|
+
);
|
|
181
|
+
const checkoutBody = (await checkoutResponse.json()) as { ok: boolean; data: { previous_branch: string } };
|
|
182
|
+
expect(checkoutResponse.status).toBe(200);
|
|
183
|
+
expect(checkoutBody.ok).toBe(true);
|
|
184
|
+
expect(checkoutBody.data.previous_branch).toBe('main');
|
|
185
|
+
|
|
186
|
+
const restoreRecordPath = path.join(repoRoot, '.aop', 'runtime', 'checkout-restore.json');
|
|
187
|
+
await expect(fs.access(restoreRecordPath)).resolves.toBeUndefined();
|
|
188
|
+
|
|
189
|
+
const restoreResponse = await checkoutPost(
|
|
190
|
+
new Request('http://localhost/api/features/feature_checkout/checkout?project=alpha', {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
body: JSON.stringify({ action: 'restore' })
|
|
193
|
+
}),
|
|
194
|
+
{ params: { id: 'feature_checkout' } }
|
|
195
|
+
);
|
|
196
|
+
const restoreBody = (await restoreResponse.json()) as { ok: boolean; data: { restored_to: string } };
|
|
197
|
+
expect(restoreResponse.status).toBe(200);
|
|
198
|
+
expect(restoreBody.ok).toBe(true);
|
|
199
|
+
expect(restoreBody.data.restored_to).toBe('main');
|
|
200
|
+
await expect(fs.access(restoreRecordPath)).rejects.toThrow();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('GIVEN_status_query_with_project_WHEN_requested_THEN_uses_project_specific_root', async () => {
|
|
204
|
+
resolveProjectRootMock.mockResolvedValue(path.join(repoRoot, 'project-beta'));
|
|
205
|
+
|
|
206
|
+
const response = await statusGet(new Request('http://localhost/api/status?project=beta'));
|
|
207
|
+
const body = (await response.json()) as { ok: boolean; data: { features: unknown[] } };
|
|
208
|
+
|
|
209
|
+
expect(response.status).toBe(200);
|
|
210
|
+
expect(body.ok).toBe(true);
|
|
211
|
+
expect(resolveProjectRootMock).toHaveBeenCalledWith('beta');
|
|
212
|
+
expect(readDashboardStatusMock).toHaveBeenCalledWith(path.join(repoRoot, 'project-beta'));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('GIVEN_multi_project_config_WHEN_projects_endpoint_called_THEN_returns_switchable_projects', async () => {
|
|
216
|
+
const projectAlpha = path.join(repoRoot, 'project-alpha');
|
|
217
|
+
const projectBeta = path.join(repoRoot, 'project-beta');
|
|
218
|
+
await fs.mkdir(path.join(repoRoot, 'agentic', 'orchestrator', 'schemas'), { recursive: true });
|
|
219
|
+
await fs.copyFile(
|
|
220
|
+
path.resolve(import.meta.dirname, '../../../agentic/orchestrator/schemas/multi-project.schema.json'),
|
|
221
|
+
path.join(repoRoot, 'agentic', 'orchestrator', 'schemas', 'multi-project.schema.json')
|
|
222
|
+
);
|
|
223
|
+
await fs.writeFile(
|
|
224
|
+
path.join(repoRoot, 'agentic', 'orchestrator', 'multi-project.yaml'),
|
|
225
|
+
[
|
|
226
|
+
'version: "1.0"',
|
|
227
|
+
'projects:',
|
|
228
|
+
` - name: alpha`,
|
|
229
|
+
` path: ${projectAlpha}`,
|
|
230
|
+
` - name: beta`,
|
|
231
|
+
` path: ${projectBeta}`
|
|
232
|
+
].join('\n'),
|
|
233
|
+
'utf8'
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const response = await projectsGet();
|
|
237
|
+
const body = (await response.json()) as {
|
|
238
|
+
ok: boolean;
|
|
239
|
+
data: { projects: Array<{ name: string; path: string }> };
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
expect(response.status).toBe(200);
|
|
243
|
+
expect(body.ok).toBe(true);
|
|
244
|
+
expect(body.data.projects.map((project) => project.name)).toEqual(['alpha', 'beta']);
|
|
245
|
+
expect(body.data.projects.map((project) => project.path)).toEqual([projectAlpha, projectBeta]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { readDashboardStatus } from '../../../packages/web-dashboard/src/lib/aop-client.js';
|
|
6
|
+
|
|
7
|
+
async function writeState(repoRoot: string, featureId: string, frontMatter: string): Promise<void> {
|
|
8
|
+
const featureDir = path.join(repoRoot, '.aop', 'features', featureId);
|
|
9
|
+
await fs.mkdir(featureDir, { recursive: true });
|
|
10
|
+
await fs.writeFile(path.join(featureDir, 'state.md'), `---\n${frontMatter}\n---\n`, 'utf8');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('dashboard aop client mapping', () => {
|
|
14
|
+
const tempRoots: string[] = [];
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await Promise.all(tempRoots.map(async (root) => fs.rm(root, { recursive: true, force: true })));
|
|
18
|
+
tempRoots.length = 0;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('GIVEN_active_feature_with_unknown_status_WHEN_readDashboardStatus_THEN_phase_falls_back_to_planning', async () => {
|
|
22
|
+
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
|
|
23
|
+
tempRoots.push(repoRoot);
|
|
24
|
+
await fs.mkdir(path.join(repoRoot, '.aop', 'features'), { recursive: true });
|
|
25
|
+
await fs.writeFile(
|
|
26
|
+
path.join(repoRoot, '.aop', 'features', 'index.json'),
|
|
27
|
+
JSON.stringify({ active: ['feature_a'], blocked: [], merged: [], blocked_queue: [] }),
|
|
28
|
+
'utf8'
|
|
29
|
+
);
|
|
30
|
+
await writeState(
|
|
31
|
+
repoRoot,
|
|
32
|
+
'feature_a',
|
|
33
|
+
[
|
|
34
|
+
'feature_id: feature_a',
|
|
35
|
+
'version: 1',
|
|
36
|
+
'status: unknown_status',
|
|
37
|
+
'branch: feature_a',
|
|
38
|
+
'worktree_path: /tmp/worktrees/feature_a',
|
|
39
|
+
'gate_profile: default',
|
|
40
|
+
'last_updated: 2026-03-03T00:00:00Z'
|
|
41
|
+
].join('\n')
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const payload = await readDashboardStatus(repoRoot);
|
|
45
|
+
expect(payload.features).toHaveLength(1);
|
|
46
|
+
expect(payload.features[0]).toMatchObject({
|
|
47
|
+
feature_id: 'feature_a',
|
|
48
|
+
status: 'unknown_status',
|
|
49
|
+
phase: 'planning'
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('GIVEN_state_with_nested_pr_object_WHEN_readDashboardStatus_THEN_pr_fields_and_merge_score_are_parsed', async () => {
|
|
54
|
+
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
|
|
55
|
+
tempRoots.push(repoRoot);
|
|
56
|
+
await fs.mkdir(path.join(repoRoot, '.aop', 'features'), { recursive: true });
|
|
57
|
+
await fs.writeFile(
|
|
58
|
+
path.join(repoRoot, '.aop', 'features', 'index.json'),
|
|
59
|
+
JSON.stringify({ active: ['feature_pr'], blocked: [], merged: [], blocked_queue: [] }),
|
|
60
|
+
'utf8'
|
|
61
|
+
);
|
|
62
|
+
await writeState(
|
|
63
|
+
repoRoot,
|
|
64
|
+
'feature_pr',
|
|
65
|
+
[
|
|
66
|
+
'feature_id: feature_pr',
|
|
67
|
+
'version: 1',
|
|
68
|
+
'status: ready_to_merge',
|
|
69
|
+
'branch: feature_pr',
|
|
70
|
+
'worktree_path: /tmp/worktrees/feature_pr',
|
|
71
|
+
'gate_profile: default',
|
|
72
|
+
'last_updated: 2026-03-03T00:00:00Z',
|
|
73
|
+
'pr:',
|
|
74
|
+
' number: 42',
|
|
75
|
+
' url: https://github.com/org/repo/pull/42',
|
|
76
|
+
' ci_status: passing',
|
|
77
|
+
' review_decision: approved',
|
|
78
|
+
' merge_ready: true',
|
|
79
|
+
' pending_review_threads: 0',
|
|
80
|
+
' has_conflicts: false',
|
|
81
|
+
' merge_score: 100'
|
|
82
|
+
].join('\n')
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const payload = await readDashboardStatus(repoRoot);
|
|
86
|
+
expect(payload.features[0].pr).toMatchObject({
|
|
87
|
+
number: 42,
|
|
88
|
+
ci_status: 'passing',
|
|
89
|
+
review_decision: 'approved',
|
|
90
|
+
merge_score: 100
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('GIVEN_blocked_queue_feature_without_state_WHEN_readDashboardStatus_THEN_phase_is_blocked', async () => {
|
|
95
|
+
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aop-dash-client-'));
|
|
96
|
+
tempRoots.push(repoRoot);
|
|
97
|
+
await fs.mkdir(path.join(repoRoot, '.aop', 'features'), { recursive: true });
|
|
98
|
+
await fs.writeFile(
|
|
99
|
+
path.join(repoRoot, '.aop', 'features', 'index.json'),
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
active: [],
|
|
102
|
+
blocked: [],
|
|
103
|
+
merged: [],
|
|
104
|
+
blocked_queue: [{ feature_id: 'feature_q', detected_at: '2026-03-03T00:00:00Z', collision_fingerprint: 'abc' }]
|
|
105
|
+
}),
|
|
106
|
+
'utf8'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const payload = await readDashboardStatus(repoRoot);
|
|
110
|
+
expect(payload.features).toHaveLength(1);
|
|
111
|
+
expect(payload.features[0]).toMatchObject({
|
|
112
|
+
feature_id: 'feature_q',
|
|
113
|
+
phase: 'blocked'
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
2
|
+
import { DashboardCommandHandler } from '../src/cli/dashboard-command-handler.js';
|
|
3
|
+
import type { ChildProcess } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
vi.mock('node:child_process', () => ({
|
|
6
|
+
execFile: vi.fn(),
|
|
7
|
+
spawn: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('DashboardCommandHandler', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('GIVEN_foreground_false_WHEN_dashboard_command_THEN_starts_background_process', async () => {
|
|
16
|
+
const { execFile, spawn } = await import('node:child_process');
|
|
17
|
+
const mockChild = { unref: vi.fn(), on: vi.fn() } as unknown as ChildProcess;
|
|
18
|
+
vi.mocked(execFile).mockImplementation((_file, _args, _opts, cb) => {
|
|
19
|
+
cb?.(null, '', '');
|
|
20
|
+
return {} as never;
|
|
21
|
+
});
|
|
22
|
+
vi.mocked(spawn).mockReturnValue(mockChild);
|
|
23
|
+
|
|
24
|
+
const handler = new DashboardCommandHandler();
|
|
25
|
+
const result = await handler.execute({ port: 3000, foreground: false });
|
|
26
|
+
|
|
27
|
+
expect(execFile).toHaveBeenCalledWith(
|
|
28
|
+
'npm',
|
|
29
|
+
['run', '--workspace', '@aop/web-dashboard', 'build'],
|
|
30
|
+
expect.any(Object),
|
|
31
|
+
expect.any(Function)
|
|
32
|
+
);
|
|
33
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
34
|
+
'npm',
|
|
35
|
+
['run', '--workspace', '@aop/web-dashboard', 'start'],
|
|
36
|
+
expect.objectContaining({ detached: true })
|
|
37
|
+
);
|
|
38
|
+
expect(mockChild.unref).toHaveBeenCalled();
|
|
39
|
+
expect(result).toMatchObject({ ok: true, data: { port: 3000, background: true, built: true } });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('GIVEN_foreground_true_WHEN_dashboard_command_THEN_runs_in_foreground', async () => {
|
|
43
|
+
const { spawn } = await import('node:child_process');
|
|
44
|
+
const mockChild = {
|
|
45
|
+
on: vi.fn((event: string, cb: () => void) => {
|
|
46
|
+
if (event === 'close') { setTimeout(cb, 0); }
|
|
47
|
+
return mockChild;
|
|
48
|
+
}),
|
|
49
|
+
} as unknown as ChildProcess;
|
|
50
|
+
vi.mocked(spawn).mockReturnValue(mockChild);
|
|
51
|
+
|
|
52
|
+
const handler = new DashboardCommandHandler();
|
|
53
|
+
const result = await handler.execute({ foreground: true });
|
|
54
|
+
|
|
55
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
56
|
+
'npm',
|
|
57
|
+
['run', '--workspace', '@aop/web-dashboard', 'start'],
|
|
58
|
+
expect.objectContaining({ stdio: 'inherit' })
|
|
59
|
+
);
|
|
60
|
+
expect(result).toMatchObject({ ok: true, data: { port: 3000, mode: 'production' } });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('GIVEN_custom_port_WHEN_dashboard_command_THEN_uses_provided_port', async () => {
|
|
64
|
+
const { execFile, spawn } = await import('node:child_process');
|
|
65
|
+
const mockChild = { unref: vi.fn(), on: vi.fn() } as unknown as ChildProcess;
|
|
66
|
+
vi.mocked(execFile).mockImplementation((_file, _args, _opts, cb) => {
|
|
67
|
+
cb?.(null, '', '');
|
|
68
|
+
return {} as never;
|
|
69
|
+
});
|
|
70
|
+
vi.mocked(spawn).mockReturnValue(mockChild);
|
|
71
|
+
|
|
72
|
+
const handler = new DashboardCommandHandler();
|
|
73
|
+
const result = await handler.execute({ port: 4000, foreground: false });
|
|
74
|
+
|
|
75
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
76
|
+
'npm',
|
|
77
|
+
['run', '--workspace', '@aop/web-dashboard', 'start'],
|
|
78
|
+
expect.objectContaining({ env: expect.objectContaining({ PORT: '4000' }) })
|
|
79
|
+
);
|
|
80
|
+
expect(result).toMatchObject({ ok: true, data: { port: 4000 } });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('GIVEN_dev_mode_WHEN_dashboard_command_THEN_runs_next_dev_in_foreground', async () => {
|
|
84
|
+
const { spawn } = await import('node:child_process');
|
|
85
|
+
const mockChild = {
|
|
86
|
+
on: vi.fn((event: string, cb: () => void) => {
|
|
87
|
+
if (event === 'close') { setTimeout(cb, 0); }
|
|
88
|
+
return mockChild;
|
|
89
|
+
}),
|
|
90
|
+
} as unknown as ChildProcess;
|
|
91
|
+
vi.mocked(spawn).mockReturnValue(mockChild);
|
|
92
|
+
|
|
93
|
+
const handler = new DashboardCommandHandler();
|
|
94
|
+
const result = await handler.execute({ dev: true });
|
|
95
|
+
|
|
96
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
97
|
+
'npm',
|
|
98
|
+
['run', '--workspace', '@aop/web-dashboard', 'dev'],
|
|
99
|
+
expect.objectContaining({ stdio: 'inherit' })
|
|
100
|
+
);
|
|
101
|
+
expect(result).toMatchObject({ ok: true, data: { mode: 'dev' } });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { AopKernel } from '../src/index.js';
|
|
3
|
+
import {
|
|
4
|
+
getUnresolvedDeps,
|
|
5
|
+
detectCircularDependency,
|
|
6
|
+
addToDepBlocked,
|
|
7
|
+
resolveDepBlocked
|
|
8
|
+
} from '../src/application/services/dependency-scheduler-service.js';
|
|
9
|
+
import { makeTempRepo, writeFeatureSpec } from './helpers.js';
|
|
10
|
+
|
|
11
|
+
let repoRoot: string;
|
|
12
|
+
|
|
13
|
+
const ORCH_CTX = { actor_type: 'orchestrator', actor_id: 'test' };
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
repoRoot = await makeTempRepo(process.cwd());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('N4: Dependency-Aware Feature Scheduling', () => {
|
|
20
|
+
describe('getUnresolvedDeps', () => {
|
|
21
|
+
it('returns deps not yet merged', () => {
|
|
22
|
+
expect(getUnresolvedDeps(['feat-a', 'feat-b'], ['feat-a'])).toEqual(['feat-b']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns empty when all deps are merged', () => {
|
|
26
|
+
expect(getUnresolvedDeps(['feat-a', 'feat-b'], ['feat-a', 'feat-b'])).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns all deps when nothing is merged', () => {
|
|
30
|
+
expect(getUnresolvedDeps(['feat-a', 'feat-b'], [])).toEqual(['feat-a', 'feat-b']);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('detectCircularDependency', () => {
|
|
35
|
+
it('returns null for linear dependency chain', () => {
|
|
36
|
+
const deps: Record<string, string[]> = { a: ['b'], b: ['c'], c: [] };
|
|
37
|
+
expect(detectCircularDependency('a', (id) => deps[id] ?? [])).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('detects a direct cycle', () => {
|
|
41
|
+
const deps: Record<string, string[]> = { a: ['b'], b: ['a'] };
|
|
42
|
+
expect(detectCircularDependency('a', (id) => deps[id] ?? [])).not.toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('detects an indirect cycle', () => {
|
|
46
|
+
const deps: Record<string, string[]> = { a: ['b'], b: ['c'], c: ['a'] };
|
|
47
|
+
expect(detectCircularDependency('a', (id) => deps[id] ?? [])).not.toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns null for feature with no deps', () => {
|
|
51
|
+
expect(detectCircularDependency('solo', (_id) => [])).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('addToDepBlocked / resolveDepBlocked', () => {
|
|
56
|
+
it('adds a new dep_blocked entry', () => {
|
|
57
|
+
const index: Record<string, unknown> = { dep_blocked: [] };
|
|
58
|
+
addToDepBlocked(index, 'feat-child', ['feat-parent']);
|
|
59
|
+
expect(index.dep_blocked).toEqual([{ feature_id: 'feat-child', depends_on_unresolved: ['feat-parent'] }]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('updates existing dep_blocked entry for the same feature', () => {
|
|
63
|
+
const index: Record<string, unknown> = { dep_blocked: [{ feature_id: 'feat-child', depends_on_unresolved: ['feat-a', 'feat-b'] }] };
|
|
64
|
+
addToDepBlocked(index, 'feat-child', ['feat-b']);
|
|
65
|
+
const entries = index.dep_blocked as Array<{ feature_id: string; depends_on_unresolved: string[] }>;
|
|
66
|
+
expect(entries).toHaveLength(1);
|
|
67
|
+
expect(entries[0].depends_on_unresolved).toEqual(['feat-b']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('resolveDepBlocked removes entries that are now fully resolved', () => {
|
|
71
|
+
const index: Record<string, unknown> = {
|
|
72
|
+
dep_blocked: [{ feature_id: 'feat-child', depends_on_unresolved: ['feat-parent'] }]
|
|
73
|
+
};
|
|
74
|
+
const promoted = resolveDepBlocked(index, 'feat-parent');
|
|
75
|
+
expect(promoted).toEqual(['feat-child']);
|
|
76
|
+
expect(index.dep_blocked).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('resolveDepBlocked reduces unresolved list when multiple deps exist', () => {
|
|
80
|
+
const index: Record<string, unknown> = {
|
|
81
|
+
dep_blocked: [{ feature_id: 'feat-child', depends_on_unresolved: ['feat-a', 'feat-b'] }]
|
|
82
|
+
};
|
|
83
|
+
const promoted = resolveDepBlocked(index, 'feat-a');
|
|
84
|
+
expect(promoted).toEqual([]);
|
|
85
|
+
const entries = index.dep_blocked as Array<{ feature_id: string; depends_on_unresolved: string[] }>;
|
|
86
|
+
expect(entries[0].depends_on_unresolved).toEqual(['feat-b']);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Kernel integration: plan.submit dependency enforcement', () => {
|
|
91
|
+
const validPlan = (featureId: string) => ({
|
|
92
|
+
feature_id: featureId,
|
|
93
|
+
plan_version: 1,
|
|
94
|
+
summary: 'test plan',
|
|
95
|
+
allowed_areas: ['src'],
|
|
96
|
+
forbidden_areas: [],
|
|
97
|
+
base_ref: 'HEAD',
|
|
98
|
+
files: { create: [], modify: [], delete: [] },
|
|
99
|
+
contracts: { openapi: 'none', events: 'none', db: 'none' },
|
|
100
|
+
acceptance_criteria: ['works'],
|
|
101
|
+
gate_profile: 'default'
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects plan when dependency is not merged', async () => {
|
|
105
|
+
const kernel = new AopKernel(repoRoot);
|
|
106
|
+
await kernel.ensureLoaded();
|
|
107
|
+
await writeFeatureSpec(repoRoot, 'feat-parent');
|
|
108
|
+
await writeFeatureSpec(repoRoot, 'feat-child');
|
|
109
|
+
await kernel.invoke('feature.init', { feature_id: 'feat-parent' }, ORCH_CTX);
|
|
110
|
+
await kernel.invoke('feature.init', { feature_id: 'feat-child' }, ORCH_CTX);
|
|
111
|
+
|
|
112
|
+
// Set depends_on in feat-child's state
|
|
113
|
+
await kernel.updateState('feat-child', null, async (fm: Record<string, unknown>) => ({
|
|
114
|
+
frontMatter: { ...fm, depends_on: ['feat-parent'] }
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
const result = await kernel.invoke('plan.submit', { feature_id: 'feat-child', plan_json: validPlan('feat-child') }, ORCH_CTX);
|
|
118
|
+
expect(result.ok).toBe(false);
|
|
119
|
+
expect((result as { ok: false; error: { code: string } }).error.code).toBe('dependency_unresolved');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('accepts plan when dependency is already merged', async () => {
|
|
123
|
+
const kernel = new AopKernel(repoRoot);
|
|
124
|
+
await kernel.ensureLoaded();
|
|
125
|
+
await writeFeatureSpec(repoRoot, 'feat-merged-dep');
|
|
126
|
+
await writeFeatureSpec(repoRoot, 'feat-child2');
|
|
127
|
+
await kernel.invoke('feature.init', { feature_id: 'feat-merged-dep' }, ORCH_CTX);
|
|
128
|
+
await kernel.invoke('feature.init', { feature_id: 'feat-child2' }, ORCH_CTX);
|
|
129
|
+
|
|
130
|
+
// Mark feat-merged-dep as merged directly in index
|
|
131
|
+
await kernel.withIndexLock(async () => {
|
|
132
|
+
const idx = await kernel.readIndex() as Record<string, unknown>;
|
|
133
|
+
idx.merged = [...(Array.isArray(idx.merged) ? idx.merged : []), 'feat-merged-dep'];
|
|
134
|
+
await kernel.writeIndex(idx);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Set depends_on in feat-child2's state
|
|
138
|
+
await kernel.updateState('feat-child2', null, async (fm: Record<string, unknown>) => ({
|
|
139
|
+
frontMatter: { ...fm, depends_on: ['feat-merged-dep'] }
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
const result = await kernel.invoke('plan.submit', { feature_id: 'feat-child2', plan_json: validPlan('feat-child2') }, ORCH_CTX);
|
|
143
|
+
expect(result.ok).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('adds feature to dep_blocked in index when dependency is unresolved', async () => {
|
|
147
|
+
const kernel = new AopKernel(repoRoot);
|
|
148
|
+
await kernel.ensureLoaded();
|
|
149
|
+
await writeFeatureSpec(repoRoot, 'feat-blocker');
|
|
150
|
+
await writeFeatureSpec(repoRoot, 'feat-waiter');
|
|
151
|
+
await kernel.invoke('feature.init', { feature_id: 'feat-blocker' }, ORCH_CTX);
|
|
152
|
+
await kernel.invoke('feature.init', { feature_id: 'feat-waiter' }, ORCH_CTX);
|
|
153
|
+
|
|
154
|
+
await kernel.updateState('feat-waiter', null, async (fm: Record<string, unknown>) => ({
|
|
155
|
+
frontMatter: { ...fm, depends_on: ['feat-blocker'] }
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
await kernel.invoke('plan.submit', { feature_id: 'feat-waiter', plan_json: validPlan('feat-waiter') }, ORCH_CTX);
|
|
159
|
+
|
|
160
|
+
const index = await kernel.readIndex() as Record<string, unknown>;
|
|
161
|
+
const depBlocked = index.dep_blocked as Array<{ feature_id: string; depends_on_unresolved: string[] }>;
|
|
162
|
+
expect(depBlocked.some((e) => e.feature_id === 'feat-waiter')).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('DependencySchedulerService branch coverage', () => {
|
|
168
|
+
it('GIVEN_index_without_dep_blocked_WHEN_addToDepBlocked_called_THEN_creates_new_array', () => {
|
|
169
|
+
const index: Record<string, unknown> = {};
|
|
170
|
+
addToDepBlocked(index, 'feat-a', ['dep-1']);
|
|
171
|
+
expect(index.dep_blocked).toEqual([{ feature_id: 'feat-a', depends_on_unresolved: ['dep-1'] }]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('GIVEN_index_without_dep_blocked_WHEN_resolveDepBlocked_called_THEN_returns_empty', () => {
|
|
175
|
+
const index: Record<string, unknown> = {};
|
|
176
|
+
const promoted = resolveDepBlocked(index, 'some-feature');
|
|
177
|
+
expect(promoted).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('GIVEN_cycle_with_only_one_node_self_referencing_WHEN_detectCircular_called_THEN_detects_cycle', () => {
|
|
181
|
+
const result = detectCircularDependency('self', (id) => id === 'self' ? ['self'] : []);
|
|
182
|
+
expect(result).not.toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('GIVEN_empty_index_merged_WHEN_getUnresolvedDeps_called_THEN_returns_all_deps', () => {
|
|
186
|
+
const result = getUnresolvedDeps(['dep-a', 'dep-b'], []);
|
|
187
|
+
expect(result).toEqual(['dep-a', 'dep-b']);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { AopKernel } from '../src/
|
|
2
|
+
import { AopKernel } from '../src/index.js';
|
|
3
3
|
import { makeTempRepo } from './helpers.js';
|
|
4
4
|
|
|
5
5
|
describe('Orchestrator Epoch Tracking and Recovery', () => {
|
|
@@ -102,9 +102,9 @@ describe('Orchestrator Epoch Tracking and Recovery', () => {
|
|
|
102
102
|
increment_epoch: true
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
await kernel.
|
|
105
|
+
const lease = await kernel.readRunLease();
|
|
106
|
+
lease.lease_expires_at = new Date(Date.now() - 60000).toISOString();
|
|
107
|
+
await kernel.writeRunLease(lease);
|
|
108
108
|
|
|
109
109
|
const takeoverResult = await kernel.acquireRunLease({
|
|
110
110
|
run_id: 'test-run-5-takeover',
|