hungry-ghost-hive 0.47.4 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/base-agent.d.ts +11 -11
- package/dist/agents/base-agent.d.ts.map +1 -1
- package/dist/agents/base-agent.js +25 -25
- package/dist/agents/base-agent.js.map +1 -1
- package/dist/agents/base-agent.test.js +2 -1
- package/dist/agents/base-agent.test.js.map +1 -1
- package/dist/agents/intermediate.d.ts +2 -0
- package/dist/agents/intermediate.d.ts.map +1 -1
- package/dist/agents/intermediate.js +25 -18
- package/dist/agents/intermediate.js.map +1 -1
- package/dist/agents/junior.d.ts +2 -0
- package/dist/agents/junior.d.ts.map +1 -1
- package/dist/agents/junior.js +25 -18
- package/dist/agents/junior.js.map +1 -1
- package/dist/agents/qa.d.ts +2 -0
- package/dist/agents/qa.d.ts.map +1 -1
- package/dist/agents/qa.js +47 -38
- package/dist/agents/qa.js.map +1 -1
- package/dist/agents/senior.d.ts +2 -0
- package/dist/agents/senior.d.ts.map +1 -1
- package/dist/agents/senior.js +40 -27
- package/dist/agents/senior.js.map +1 -1
- package/dist/agents/tech-lead.d.ts +2 -0
- package/dist/agents/tech-lead.d.ts.map +1 -1
- package/dist/agents/tech-lead.js +37 -31
- package/dist/agents/tech-lead.js.map +1 -1
- package/dist/cli/commands/add-repo.js +2 -2
- package/dist/cli/commands/add-repo.js.map +1 -1
- package/dist/cli/commands/add-repo.test.js +1 -1
- package/dist/cli/commands/add-repo.test.js.map +1 -1
- package/dist/cli/commands/agents.d.ts.map +1 -1
- package/dist/cli/commands/agents.js +12 -10
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/agents.test.js +7 -7
- package/dist/cli/commands/agents.test.js.map +1 -1
- package/dist/cli/commands/approach.js +2 -2
- package/dist/cli/commands/approach.js.map +1 -1
- package/dist/cli/commands/approvals.js +7 -7
- package/dist/cli/commands/approvals.js.map +1 -1
- package/dist/cli/commands/approvals.test.js +8 -8
- package/dist/cli/commands/approvals.test.js.map +1 -1
- package/dist/cli/commands/assign.js +4 -4
- package/dist/cli/commands/assign.js.map +1 -1
- package/dist/cli/commands/assign.test.js +18 -16
- package/dist/cli/commands/assign.test.js.map +1 -1
- package/dist/cli/commands/cleanup.d.ts.map +1 -1
- package/dist/cli/commands/cleanup.js +8 -8
- package/dist/cli/commands/cleanup.js.map +1 -1
- package/dist/cli/commands/cleanup.test.js +5 -1
- package/dist/cli/commands/cleanup.test.js.map +1 -1
- package/dist/cli/commands/escalations.js +9 -7
- package/dist/cli/commands/escalations.js.map +1 -1
- package/dist/cli/commands/escalations.test.js +2 -2
- package/dist/cli/commands/escalations.test.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +48 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +4 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/manager/agent-monitoring.d.ts +2 -2
- package/dist/cli/commands/manager/agent-monitoring.d.ts.map +1 -1
- package/dist/cli/commands/manager/agent-monitoring.js +1 -1
- package/dist/cli/commands/manager/agent-monitoring.js.map +1 -1
- package/dist/cli/commands/manager/auditor-lifecycle.js +3 -3
- package/dist/cli/commands/manager/auditor-lifecycle.js.map +1 -1
- package/dist/cli/commands/manager/auditor-lifecycle.test.js +21 -14
- package/dist/cli/commands/manager/auditor-lifecycle.test.js.map +1 -1
- package/dist/cli/commands/manager/auto-reject-comment-only-reviews.test.js +28 -23
- package/dist/cli/commands/manager/auto-reject-comment-only-reviews.test.js.map +1 -1
- package/dist/cli/commands/manager/escalation-handler.d.ts +2 -2
- package/dist/cli/commands/manager/escalation-handler.d.ts.map +1 -1
- package/dist/cli/commands/manager/escalation-handler.js +11 -10
- package/dist/cli/commands/manager/escalation-handler.js.map +1 -1
- package/dist/cli/commands/manager/escalation-handler.test.js +8 -8
- package/dist/cli/commands/manager/escalation-handler.test.js.map +1 -1
- package/dist/cli/commands/manager/feature-sign-off.js +7 -7
- package/dist/cli/commands/manager/feature-sign-off.js.map +1 -1
- package/dist/cli/commands/manager/feature-sign-off.test.js +40 -31
- package/dist/cli/commands/manager/feature-sign-off.test.js.map +1 -1
- package/dist/cli/commands/manager/feature-test-result.d.ts.map +1 -1
- package/dist/cli/commands/manager/feature-test-result.js +12 -13
- package/dist/cli/commands/manager/feature-test-result.js.map +1 -1
- package/dist/cli/commands/manager/handoff-recovery.d.ts.map +1 -1
- package/dist/cli/commands/manager/handoff-recovery.js +14 -15
- package/dist/cli/commands/manager/handoff-recovery.js.map +1 -1
- package/dist/cli/commands/manager/index.d.ts.map +1 -1
- package/dist/cli/commands/manager/index.js +26 -26
- package/dist/cli/commands/manager/index.js.map +1 -1
- package/dist/cli/commands/manager/index.test.js +3 -3
- package/dist/cli/commands/manager/index.test.js.map +1 -1
- package/dist/cli/commands/manager/merged-story-cleanup.d.ts +2 -2
- package/dist/cli/commands/manager/merged-story-cleanup.d.ts.map +1 -1
- package/dist/cli/commands/manager/merged-story-cleanup.js +6 -7
- package/dist/cli/commands/manager/merged-story-cleanup.js.map +1 -1
- package/dist/cli/commands/manager/merged-story-cleanup.test.js +27 -18
- package/dist/cli/commands/manager/merged-story-cleanup.test.js.map +1 -1
- package/dist/cli/commands/manager/pr-sync-orchestrator.d.ts.map +1 -1
- package/dist/cli/commands/manager/pr-sync-orchestrator.js +46 -38
- package/dist/cli/commands/manager/pr-sync-orchestrator.js.map +1 -1
- package/dist/cli/commands/manager/qa-review-handler.d.ts.map +1 -1
- package/dist/cli/commands/manager/qa-review-handler.js +25 -22
- package/dist/cli/commands/manager/qa-review-handler.js.map +1 -1
- package/dist/cli/commands/manager/spin-down.d.ts.map +1 -1
- package/dist/cli/commands/manager/spin-down.js +23 -19
- package/dist/cli/commands/manager/spin-down.js.map +1 -1
- package/dist/cli/commands/manager/stale-escalations.d.ts +2 -3
- package/dist/cli/commands/manager/stale-escalations.d.ts.map +1 -1
- package/dist/cli/commands/manager/stale-escalations.js.map +1 -1
- package/dist/cli/commands/manager/stuck-story-helpers.js +8 -8
- package/dist/cli/commands/manager/stuck-story-helpers.js.map +1 -1
- package/dist/cli/commands/manager/stuck-story-processor.d.ts +2 -2
- package/dist/cli/commands/manager/stuck-story-processor.d.ts.map +1 -1
- package/dist/cli/commands/manager/stuck-story-processor.js +23 -22
- package/dist/cli/commands/manager/stuck-story-processor.js.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.js +6 -6
- package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
- package/dist/cli/commands/manager/types.d.ts +2 -3
- package/dist/cli/commands/manager/types.d.ts.map +1 -1
- package/dist/cli/commands/manager/types.js.map +1 -1
- package/dist/cli/commands/msg.test.js +2 -2
- package/dist/cli/commands/msg.test.js.map +1 -1
- package/dist/cli/commands/my-stories.d.ts.map +1 -1
- package/dist/cli/commands/my-stories.js +17 -18
- package/dist/cli/commands/my-stories.js.map +1 -1
- package/dist/cli/commands/my-stories.test.js +2 -2
- package/dist/cli/commands/my-stories.test.js.map +1 -1
- package/dist/cli/commands/nuke.test.js +1 -1
- package/dist/cli/commands/nuke.test.js.map +1 -1
- package/dist/cli/commands/pr.js +32 -32
- package/dist/cli/commands/pr.js.map +1 -1
- package/dist/cli/commands/pr.test.js +10 -6
- package/dist/cli/commands/pr.test.js.map +1 -1
- package/dist/cli/commands/progress.d.ts.map +1 -1
- package/dist/cli/commands/progress.js +4 -5
- package/dist/cli/commands/progress.js.map +1 -1
- package/dist/cli/commands/progress.test.js +1 -1
- package/dist/cli/commands/progress.test.js.map +1 -1
- package/dist/cli/commands/req-headless.test.d.ts +2 -0
- package/dist/cli/commands/req-headless.test.d.ts.map +1 -0
- package/dist/cli/commands/req-headless.test.js +128 -0
- package/dist/cli/commands/req-headless.test.js.map +1 -0
- package/dist/cli/commands/req-spawn.test.js +5 -1
- package/dist/cli/commands/req-spawn.test.js.map +1 -1
- package/dist/cli/commands/req.d.ts.map +1 -1
- package/dist/cli/commands/req.js +24 -21
- package/dist/cli/commands/req.js.map +1 -1
- package/dist/cli/commands/req.test.js +31 -0
- package/dist/cli/commands/req.test.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +7 -8
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/resume.test.js +1 -1
- package/dist/cli/commands/resume.test.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +42 -40
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/status.test.js +1 -1
- package/dist/cli/commands/status.test.js.map +1 -1
- package/dist/cli/commands/stories.js +9 -9
- package/dist/cli/commands/stories.js.map +1 -1
- package/dist/cli/commands/stories.test.js +2 -2
- package/dist/cli/commands/stories.test.js.map +1 -1
- package/dist/cli/commands/teams.js +11 -11
- package/dist/cli/commands/teams.js.map +1 -1
- package/dist/cli/commands/teams.test.js +2 -2
- package/dist/cli/commands/teams.test.js.map +1 -1
- package/dist/cli/dashboard/index.d.ts +2 -2
- package/dist/cli/dashboard/index.d.ts.map +1 -1
- package/dist/cli/dashboard/index.js +29 -20
- package/dist/cli/dashboard/index.js.map +1 -1
- package/dist/cli/dashboard/index.test.js +34 -32
- package/dist/cli/dashboard/index.test.js.map +1 -1
- package/dist/cli/dashboard/panels/activity.d.ts +3 -3
- package/dist/cli/dashboard/panels/activity.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/activity.js +1 -1
- package/dist/cli/dashboard/panels/activity.js.map +1 -1
- package/dist/cli/dashboard/panels/agents.d.ts +3 -3
- package/dist/cli/dashboard/panels/agents.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/agents.js +2 -2
- package/dist/cli/dashboard/panels/agents.js.map +1 -1
- package/dist/cli/dashboard/panels/escalations.d.ts +3 -3
- package/dist/cli/dashboard/panels/escalations.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/escalations.js +1 -1
- package/dist/cli/dashboard/panels/escalations.js.map +1 -1
- package/dist/cli/dashboard/panels/merge-queue.d.ts +3 -3
- package/dist/cli/dashboard/panels/merge-queue.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/merge-queue.js +1 -1
- package/dist/cli/dashboard/panels/merge-queue.js.map +1 -1
- package/dist/cli/dashboard/panels/pipeline.d.ts +3 -3
- package/dist/cli/dashboard/panels/pipeline.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/pipeline.js +1 -1
- package/dist/cli/dashboard/panels/pipeline.js.map +1 -1
- package/dist/config/schema.d.ts +85 -82
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/connectors/project-management/operations.d.ts +7 -7
- package/dist/connectors/project-management/operations.d.ts.map +1 -1
- package/dist/connectors/project-management/operations.js +2 -3
- package/dist/connectors/project-management/operations.js.map +1 -1
- package/dist/context-files/index.test.js +1 -0
- package/dist/context-files/index.test.js.map +1 -1
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +7 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/postgres-provider.d.ts +43 -0
- package/dist/db/postgres-provider.d.ts.map +1 -0
- package/dist/db/postgres-provider.integration.test.d.ts +2 -0
- package/dist/db/postgres-provider.integration.test.d.ts.map +1 -0
- package/dist/db/postgres-provider.integration.test.js +399 -0
- package/dist/db/postgres-provider.integration.test.js.map +1 -0
- package/dist/db/postgres-provider.js +315 -0
- package/dist/db/postgres-provider.js.map +1 -0
- package/dist/db/postgres-provider.test.d.ts +2 -0
- package/dist/db/postgres-provider.test.d.ts.map +1 -0
- package/dist/db/postgres-provider.test.js +72 -0
- package/dist/db/postgres-provider.test.js.map +1 -0
- package/dist/db/provider.d.ts +59 -0
- package/dist/db/provider.d.ts.map +1 -0
- package/dist/db/provider.js +121 -0
- package/dist/db/provider.js.map +1 -0
- package/dist/db/provider.test.d.ts +2 -0
- package/dist/db/provider.test.d.ts.map +1 -0
- package/dist/db/provider.test.js +226 -0
- package/dist/db/provider.test.js.map +1 -0
- package/dist/db/queries/agents.d.ts +13 -13
- package/dist/db/queries/agents.d.ts.map +1 -1
- package/dist/db/queries/agents.js +27 -28
- package/dist/db/queries/agents.js.map +1 -1
- package/dist/db/queries/agents.test.js +113 -111
- package/dist/db/queries/agents.test.js.map +1 -1
- package/dist/db/queries/escalations.d.ts +16 -16
- package/dist/db/queries/escalations.d.ts.map +1 -1
- package/dist/db/queries/escalations.js +34 -35
- package/dist/db/queries/escalations.js.map +1 -1
- package/dist/db/queries/escalations.test.js +133 -131
- package/dist/db/queries/escalations.test.js.map +1 -1
- package/dist/db/queries/heartbeat.d.ts +5 -5
- package/dist/db/queries/heartbeat.d.ts.map +1 -1
- package/dist/db/queries/heartbeat.js +7 -23
- package/dist/db/queries/heartbeat.js.map +1 -1
- package/dist/db/queries/heartbeat.test.js +76 -76
- package/dist/db/queries/heartbeat.test.js.map +1 -1
- package/dist/db/queries/integration-sync.d.ts +7 -7
- package/dist/db/queries/integration-sync.d.ts.map +1 -1
- package/dist/db/queries/integration-sync.js +13 -14
- package/dist/db/queries/integration-sync.js.map +1 -1
- package/dist/db/queries/logs.d.ts +10 -10
- package/dist/db/queries/logs.d.ts.map +1 -1
- package/dist/db/queries/logs.js +44 -42
- package/dist/db/queries/logs.js.map +1 -1
- package/dist/db/queries/logs.test.js +149 -146
- package/dist/db/queries/logs.test.js.map +1 -1
- package/dist/db/queries/messages.d.ts +6 -6
- package/dist/db/queries/messages.d.ts.map +1 -1
- package/dist/db/queries/messages.js +12 -11
- package/dist/db/queries/messages.js.map +1 -1
- package/dist/db/queries/messages.test.js +47 -46
- package/dist/db/queries/messages.test.js.map +1 -1
- package/dist/db/queries/pull-requests.d.ts +18 -18
- package/dist/db/queries/pull-requests.d.ts.map +1 -1
- package/dist/db/queries/pull-requests.js +50 -48
- package/dist/db/queries/pull-requests.js.map +1 -1
- package/dist/db/queries/pull-requests.test.js +195 -198
- package/dist/db/queries/pull-requests.test.js.map +1 -1
- package/dist/db/queries/requirements.d.ts +8 -8
- package/dist/db/queries/requirements.d.ts.map +1 -1
- package/dist/db/queries/requirements.js +17 -18
- package/dist/db/queries/requirements.js.map +1 -1
- package/dist/db/queries/requirements.test.js +83 -81
- package/dist/db/queries/requirements.test.js.map +1 -1
- package/dist/db/queries/stories.d.ts +29 -29
- package/dist/db/queries/stories.d.ts.map +1 -1
- package/dist/db/queries/stories.js +58 -64
- package/dist/db/queries/stories.js.map +1 -1
- package/dist/db/queries/stories.test.js +172 -170
- package/dist/db/queries/stories.test.js.map +1 -1
- package/dist/db/queries/teams.d.ts +6 -6
- package/dist/db/queries/teams.d.ts.map +1 -1
- package/dist/db/queries/teams.js +11 -12
- package/dist/db/queries/teams.js.map +1 -1
- package/dist/db/queries/teams.test.js +36 -34
- package/dist/db/queries/teams.test.js.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/jira/repair.test.js +26 -24
- package/dist/integrations/jira/repair.test.js.map +1 -1
- package/dist/integrations/jira/stories.d.ts +3 -3
- package/dist/integrations/jira/stories.d.ts.map +1 -1
- package/dist/integrations/jira/stories.js +12 -12
- package/dist/integrations/jira/stories.js.map +1 -1
- package/dist/integrations/jira/stories.test.js +10 -8
- package/dist/integrations/jira/stories.test.js.map +1 -1
- package/dist/integrations/jira/sync.d.ts +7 -7
- package/dist/integrations/jira/sync.d.ts.map +1 -1
- package/dist/integrations/jira/sync.js +17 -20
- package/dist/integrations/jira/sync.js.map +1 -1
- package/dist/integrations/jira/sync.test.js +63 -62
- package/dist/integrations/jira/sync.test.js.map +1 -1
- package/dist/integrations/jira/transitions.d.ts +3 -3
- package/dist/integrations/jira/transitions.d.ts.map +1 -1
- package/dist/integrations/jira/transitions.js +3 -3
- package/dist/integrations/jira/transitions.js.map +1 -1
- package/dist/orchestrator/agent-selector.d.ts +3 -3
- package/dist/orchestrator/agent-selector.d.ts.map +1 -1
- package/dist/orchestrator/agent-selector.js +5 -6
- package/dist/orchestrator/agent-selector.js.map +1 -1
- package/dist/orchestrator/dependency-resolver.d.ts +4 -4
- package/dist/orchestrator/dependency-resolver.d.ts.map +1 -1
- package/dist/orchestrator/dependency-resolver.js +6 -6
- package/dist/orchestrator/dependency-resolver.js.map +1 -1
- package/dist/orchestrator/feature-branch.d.ts +3 -3
- package/dist/orchestrator/feature-branch.d.ts.map +1 -1
- package/dist/orchestrator/feature-branch.js +9 -10
- package/dist/orchestrator/feature-branch.js.map +1 -1
- package/dist/orchestrator/feature-branch.test.js +80 -78
- package/dist/orchestrator/feature-branch.test.js.map +1 -1
- package/dist/orchestrator/orphan-recovery.d.ts +2 -2
- package/dist/orchestrator/orphan-recovery.d.ts.map +1 -1
- package/dist/orchestrator/orphan-recovery.js +10 -10
- package/dist/orchestrator/orphan-recovery.js.map +1 -1
- package/dist/orchestrator/scheduler.d.ts +4 -4
- package/dist/orchestrator/scheduler.d.ts.map +1 -1
- package/dist/orchestrator/scheduler.js +90 -76
- package/dist/orchestrator/scheduler.js.map +1 -1
- package/dist/orchestrator/scheduler.test.js +496 -374
- package/dist/orchestrator/scheduler.test.js.map +1 -1
- package/dist/utils/auto-merge.d.ts.map +1 -1
- package/dist/utils/auto-merge.js +74 -56
- package/dist/utils/auto-merge.js.map +1 -1
- package/dist/utils/auto-merge.test.js +101 -66
- package/dist/utils/auto-merge.test.js.map +1 -1
- package/dist/utils/cli-helpers.d.ts +5 -5
- package/dist/utils/cli-helpers.d.ts.map +1 -1
- package/dist/utils/cli-helpers.js +8 -9
- package/dist/utils/cli-helpers.js.map +1 -1
- package/dist/utils/cli-helpers.test.js +28 -30
- package/dist/utils/cli-helpers.test.js.map +1 -1
- package/dist/utils/paths.d.ts +6 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +12 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/paths.test.js +1 -0
- package/dist/utils/paths.test.js.map +1 -1
- package/dist/utils/pr-sync.d.ts +10 -10
- package/dist/utils/pr-sync.d.ts.map +1 -1
- package/dist/utils/pr-sync.js +20 -21
- package/dist/utils/pr-sync.js.map +1 -1
- package/dist/utils/pr-sync.test.js +52 -50
- package/dist/utils/pr-sync.test.js.map +1 -1
- package/dist/utils/with-hive-context.d.ts.map +1 -1
- package/dist/utils/with-hive-context.js +70 -1
- package/dist/utils/with-hive-context.js.map +1 -1
- package/package.json +3 -1
- package/src/agents/base-agent.test.ts +2 -1
- package/src/agents/base-agent.ts +32 -28
- package/src/agents/intermediate.ts +27 -18
- package/src/agents/junior.ts +27 -18
- package/src/agents/qa.ts +54 -40
- package/src/agents/senior.ts +42 -27
- package/src/agents/tech-lead.ts +42 -32
- package/src/cli/commands/add-repo.test.ts +1 -1
- package/src/cli/commands/add-repo.ts +2 -2
- package/src/cli/commands/agents.test.ts +7 -7
- package/src/cli/commands/agents.ts +12 -10
- package/src/cli/commands/approach.ts +2 -2
- package/src/cli/commands/approvals.test.ts +8 -8
- package/src/cli/commands/approvals.ts +9 -7
- package/src/cli/commands/assign.test.ts +19 -18
- package/src/cli/commands/assign.ts +4 -4
- package/src/cli/commands/cleanup.test.ts +5 -1
- package/src/cli/commands/cleanup.ts +11 -9
- package/src/cli/commands/escalations.test.ts +2 -2
- package/src/cli/commands/escalations.ts +9 -7
- package/src/cli/commands/init.test.ts +5 -0
- package/src/cli/commands/init.ts +53 -5
- package/src/cli/commands/manager/agent-monitoring.ts +3 -3
- package/src/cli/commands/manager/auditor-lifecycle.test.ts +21 -14
- package/src/cli/commands/manager/auditor-lifecycle.ts +3 -3
- package/src/cli/commands/manager/auto-reject-comment-only-reviews.test.ts +28 -23
- package/src/cli/commands/manager/escalation-handler.test.ts +13 -13
- package/src/cli/commands/manager/escalation-handler.ts +19 -12
- package/src/cli/commands/manager/feature-sign-off.test.ts +40 -31
- package/src/cli/commands/manager/feature-sign-off.ts +7 -7
- package/src/cli/commands/manager/feature-test-result.ts +13 -16
- package/src/cli/commands/manager/handoff-recovery.ts +20 -20
- package/src/cli/commands/manager/index.test.ts +4 -4
- package/src/cli/commands/manager/index.ts +58 -59
- package/src/cli/commands/manager/merged-story-cleanup.test.ts +28 -19
- package/src/cli/commands/manager/merged-story-cleanup.ts +11 -14
- package/src/cli/commands/manager/pr-sync-orchestrator.ts +115 -110
- package/src/cli/commands/manager/qa-review-handler.ts +50 -63
- package/src/cli/commands/manager/spin-down.ts +27 -25
- package/src/cli/commands/manager/stale-escalations.ts +2 -3
- package/src/cli/commands/manager/stuck-story-helpers.ts +10 -10
- package/src/cli/commands/manager/stuck-story-processor.ts +56 -62
- package/src/cli/commands/manager/tech-lead-lifecycle.ts +6 -6
- package/src/cli/commands/manager/types.ts +2 -3
- package/src/cli/commands/msg.test.ts +2 -2
- package/src/cli/commands/my-stories.test.ts +4 -2
- package/src/cli/commands/my-stories.ts +22 -27
- package/src/cli/commands/nuke.test.ts +1 -1
- package/src/cli/commands/pr.test.ts +10 -6
- package/src/cli/commands/pr.ts +41 -32
- package/src/cli/commands/progress.test.ts +1 -1
- package/src/cli/commands/progress.ts +11 -6
- package/src/cli/commands/req-headless.test.ts +170 -0
- package/src/cli/commands/req-spawn.test.ts +12 -2
- package/src/cli/commands/req.test.ts +36 -0
- package/src/cli/commands/req.ts +24 -20
- package/src/cli/commands/resume.test.ts +1 -1
- package/src/cli/commands/resume.ts +7 -8
- package/src/cli/commands/status.test.ts +1 -1
- package/src/cli/commands/status.ts +52 -40
- package/src/cli/commands/stories.test.ts +4 -2
- package/src/cli/commands/stories.ts +11 -11
- package/src/cli/commands/teams.test.ts +2 -2
- package/src/cli/commands/teams.ts +11 -11
- package/src/cli/dashboard/index.test.ts +35 -34
- package/src/cli/dashboard/index.ts +34 -23
- package/src/cli/dashboard/panels/activity.ts +10 -4
- package/src/cli/dashboard/panels/agents.ts +8 -5
- package/src/cli/dashboard/panels/escalations.ts +4 -4
- package/src/cli/dashboard/panels/merge-queue.ts +4 -4
- package/src/cli/dashboard/panels/pipeline.ts +10 -4
- package/src/config/schema.ts +1 -0
- package/src/connectors/project-management/operations.ts +9 -10
- package/src/context-files/index.test.ts +1 -0
- package/src/db/client.ts +17 -0
- package/src/db/pg-migrations/001-full-schema.sql +209 -0
- package/src/db/postgres-provider.integration.test.ts +574 -0
- package/src/db/postgres-provider.test.ts +97 -0
- package/src/db/postgres-provider.ts +364 -0
- package/src/db/provider.test.ts +283 -0
- package/src/db/provider.ts +161 -0
- package/src/db/queries/agents.test.ts +114 -113
- package/src/db/queries/agents.ts +50 -36
- package/src/db/queries/escalations.test.ts +134 -133
- package/src/db/queries/escalations.ts +72 -57
- package/src/db/queries/heartbeat.test.ts +77 -78
- package/src/db/queries/heartbeat.ts +24 -46
- package/src/db/queries/integration-sync.ts +26 -26
- package/src/db/queries/logs.test.ts +151 -148
- package/src/db/queries/logs.ts +78 -53
- package/src/db/queries/messages.test.ts +48 -50
- package/src/db/queries/messages.ts +26 -18
- package/src/db/queries/pull-requests.test.ts +194 -199
- package/src/db/queries/pull-requests.ts +117 -88
- package/src/db/queries/requirements.test.ts +84 -83
- package/src/db/queries/requirements.ts +33 -28
- package/src/db/queries/stories.test.ts +173 -172
- package/src/db/queries/stories.ts +141 -110
- package/src/db/queries/teams.test.ts +37 -36
- package/src/db/queries/teams.ts +22 -14
- package/src/index.ts +8 -1
- package/src/integrations/jira/repair.test.ts +27 -26
- package/src/integrations/jira/stories.test.ts +15 -16
- package/src/integrations/jira/stories.ts +15 -15
- package/src/integrations/jira/sync.test.ts +68 -68
- package/src/integrations/jira/sync.ts +29 -39
- package/src/integrations/jira/transitions.ts +6 -6
- package/src/orchestrator/agent-selector.ts +9 -8
- package/src/orchestrator/dependency-resolver.ts +16 -7
- package/src/orchestrator/feature-branch.test.ts +85 -80
- package/src/orchestrator/feature-branch.ts +13 -14
- package/src/orchestrator/orphan-recovery.ts +14 -13
- package/src/orchestrator/scheduler.test.ts +536 -394
- package/src/orchestrator/scheduler.ts +129 -115
- package/src/utils/auto-merge.test.ts +102 -68
- package/src/utils/auto-merge.ts +161 -168
- package/src/utils/cli-helpers.test.ts +30 -32
- package/src/utils/cli-helpers.ts +15 -11
- package/src/utils/paths.test.ts +1 -0
- package/src/utils/paths.ts +14 -1
- package/src/utils/pr-sync.test.ts +55 -52
- package/src/utils/pr-sync.ts +27 -32
- package/src/utils/with-hive-context.ts +89 -1
|
@@ -4,6 +4,7 @@ import { tmpdir } from 'os';
|
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import initSqlJs from 'sql.js';
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { SqliteProvider } from '../db/provider.js';
|
|
7
8
|
import { getLogsByEventType } from '../db/queries/logs.js';
|
|
8
9
|
import { createPullRequest } from '../db/queries/pull-requests.js';
|
|
9
10
|
import { createRequirement } from '../db/queries/requirements.js';
|
|
@@ -162,39 +163,60 @@ CREATE TABLE IF NOT EXISTS requirements (
|
|
|
162
163
|
`;
|
|
163
164
|
beforeEach(async () => {
|
|
164
165
|
const SQL = await initSqlJs();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
const rawDb = new SQL.Database();
|
|
167
|
+
rawDb.run('PRAGMA foreign_keys = ON');
|
|
168
|
+
rawDb.run(INITIAL_MIGRATION);
|
|
169
|
+
rawDb.run("INSERT INTO migrations (name) VALUES ('001-initial.sql')");
|
|
170
|
+
db = new SqliteProvider(rawDb);
|
|
169
171
|
scheduler = new Scheduler(db, mockConfig);
|
|
170
172
|
});
|
|
171
173
|
describe('Scheduler Topological Sort', () => {
|
|
172
|
-
it('should handle stories with no dependencies', () => {
|
|
173
|
-
const team = createTeam(db, {
|
|
174
|
+
it('should handle stories with no dependencies', async () => {
|
|
175
|
+
const team = await createTeam(db, {
|
|
174
176
|
name: 'Test Team',
|
|
175
177
|
repoUrl: 'https://github.com/test/repo',
|
|
176
178
|
repoPath: 'test',
|
|
177
179
|
});
|
|
178
|
-
const story1 = createStory(db, {
|
|
179
|
-
|
|
180
|
+
const story1 = await createStory(db, {
|
|
181
|
+
teamId: team.id,
|
|
182
|
+
title: 'Story 1',
|
|
183
|
+
description: 'Test',
|
|
184
|
+
});
|
|
185
|
+
const story2 = await createStory(db, {
|
|
186
|
+
teamId: team.id,
|
|
187
|
+
title: 'Story 2',
|
|
188
|
+
description: 'Test',
|
|
189
|
+
});
|
|
180
190
|
// Mock the private method by accessing it through reflection
|
|
181
|
-
const sorted = topologicalSort(db, [story1, story2]);
|
|
191
|
+
const sorted = await topologicalSort(db, [story1, story2]);
|
|
182
192
|
expect(sorted).not.toBeNull();
|
|
183
193
|
expect(sorted).toHaveLength(2);
|
|
184
194
|
});
|
|
185
|
-
it('should respect linear dependencies (A -> B -> C)', () => {
|
|
186
|
-
const team = createTeam(db, {
|
|
195
|
+
it('should respect linear dependencies (A -> B -> C)', async () => {
|
|
196
|
+
const team = await createTeam(db, {
|
|
187
197
|
name: 'Test Team',
|
|
188
198
|
repoUrl: 'https://github.com/test/repo',
|
|
189
199
|
repoPath: 'test',
|
|
190
200
|
});
|
|
191
|
-
const storyA = createStory(db, {
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
const storyA = await createStory(db, {
|
|
202
|
+
teamId: team.id,
|
|
203
|
+
title: 'Story A',
|
|
204
|
+
description: 'Test',
|
|
205
|
+
});
|
|
206
|
+
const storyB = await createStory(db, {
|
|
207
|
+
teamId: team.id,
|
|
208
|
+
title: 'Story B',
|
|
209
|
+
description: 'Test',
|
|
210
|
+
});
|
|
211
|
+
const storyC = await createStory(db, {
|
|
212
|
+
teamId: team.id,
|
|
213
|
+
title: 'Story C',
|
|
214
|
+
description: 'Test',
|
|
215
|
+
});
|
|
194
216
|
// B depends on A, C depends on B
|
|
195
|
-
addStoryDependency(db, storyB.id, storyA.id);
|
|
196
|
-
addStoryDependency(db, storyC.id, storyB.id);
|
|
197
|
-
const sorted = topologicalSort(db, [storyC, storyA, storyB]);
|
|
217
|
+
await addStoryDependency(db, storyB.id, storyA.id);
|
|
218
|
+
await addStoryDependency(db, storyC.id, storyB.id);
|
|
219
|
+
const sorted = await topologicalSort(db, [storyC, storyA, storyB]);
|
|
198
220
|
expect(sorted).not.toBeNull();
|
|
199
221
|
expect(sorted).toHaveLength(3);
|
|
200
222
|
// A should come first, then B, then C
|
|
@@ -202,22 +224,38 @@ describe('Scheduler Topological Sort', () => {
|
|
|
202
224
|
expect(ids.indexOf(storyA.id)).toBeLessThan(ids.indexOf(storyB.id));
|
|
203
225
|
expect(ids.indexOf(storyB.id)).toBeLessThan(ids.indexOf(storyC.id));
|
|
204
226
|
});
|
|
205
|
-
it('should respect diamond dependencies (A -> B, A -> C, B -> D, C -> D)', () => {
|
|
206
|
-
const team = createTeam(db, {
|
|
227
|
+
it('should respect diamond dependencies (A -> B, A -> C, B -> D, C -> D)', async () => {
|
|
228
|
+
const team = await createTeam(db, {
|
|
207
229
|
name: 'Test Team',
|
|
208
230
|
repoUrl: 'https://github.com/test/repo',
|
|
209
231
|
repoPath: 'test',
|
|
210
232
|
});
|
|
211
|
-
const storyA = createStory(db, {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
const storyA = await createStory(db, {
|
|
234
|
+
teamId: team.id,
|
|
235
|
+
title: 'Story A',
|
|
236
|
+
description: 'Test',
|
|
237
|
+
});
|
|
238
|
+
const storyB = await createStory(db, {
|
|
239
|
+
teamId: team.id,
|
|
240
|
+
title: 'Story B',
|
|
241
|
+
description: 'Test',
|
|
242
|
+
});
|
|
243
|
+
const storyC = await createStory(db, {
|
|
244
|
+
teamId: team.id,
|
|
245
|
+
title: 'Story C',
|
|
246
|
+
description: 'Test',
|
|
247
|
+
});
|
|
248
|
+
const storyD = await createStory(db, {
|
|
249
|
+
teamId: team.id,
|
|
250
|
+
title: 'Story D',
|
|
251
|
+
description: 'Test',
|
|
252
|
+
});
|
|
215
253
|
// B and C depend on A, D depends on both B and C
|
|
216
|
-
addStoryDependency(db, storyB.id, storyA.id);
|
|
217
|
-
addStoryDependency(db, storyC.id, storyA.id);
|
|
218
|
-
addStoryDependency(db, storyD.id, storyB.id);
|
|
219
|
-
addStoryDependency(db, storyD.id, storyC.id);
|
|
220
|
-
const sorted = topologicalSort(db, [storyD, storyB, storyA, storyC]);
|
|
254
|
+
await addStoryDependency(db, storyB.id, storyA.id);
|
|
255
|
+
await addStoryDependency(db, storyC.id, storyA.id);
|
|
256
|
+
await addStoryDependency(db, storyD.id, storyB.id);
|
|
257
|
+
await addStoryDependency(db, storyD.id, storyC.id);
|
|
258
|
+
const sorted = await topologicalSort(db, [storyD, storyB, storyA, storyC]);
|
|
221
259
|
expect(sorted).not.toBeNull();
|
|
222
260
|
expect(sorted).toHaveLength(4);
|
|
223
261
|
const ids = sorted.map((s) => s.id);
|
|
@@ -231,117 +269,149 @@ describe('Scheduler Topological Sort', () => {
|
|
|
231
269
|
expect(ids.indexOf(storyB.id)).toBeLessThan(ids.indexOf(storyD.id));
|
|
232
270
|
expect(ids.indexOf(storyC.id)).toBeLessThan(ids.indexOf(storyD.id));
|
|
233
271
|
});
|
|
234
|
-
it('should detect circular dependencies', () => {
|
|
235
|
-
const team = createTeam(db, {
|
|
272
|
+
it('should detect circular dependencies', async () => {
|
|
273
|
+
const team = await createTeam(db, {
|
|
236
274
|
name: 'Test Team',
|
|
237
275
|
repoUrl: 'https://github.com/test/repo',
|
|
238
276
|
repoPath: 'test',
|
|
239
277
|
});
|
|
240
|
-
const storyA = createStory(db, {
|
|
241
|
-
|
|
278
|
+
const storyA = await createStory(db, {
|
|
279
|
+
teamId: team.id,
|
|
280
|
+
title: 'Story A',
|
|
281
|
+
description: 'Test',
|
|
282
|
+
});
|
|
283
|
+
const storyB = await createStory(db, {
|
|
284
|
+
teamId: team.id,
|
|
285
|
+
title: 'Story B',
|
|
286
|
+
description: 'Test',
|
|
287
|
+
});
|
|
242
288
|
// Create circular dependency: A -> B -> A
|
|
243
|
-
addStoryDependency(db, storyB.id, storyA.id);
|
|
244
|
-
addStoryDependency(db, storyA.id, storyB.id);
|
|
245
|
-
const sorted = topologicalSort(db, [storyA, storyB]);
|
|
289
|
+
await addStoryDependency(db, storyB.id, storyA.id);
|
|
290
|
+
await addStoryDependency(db, storyA.id, storyB.id);
|
|
291
|
+
const sorted = await topologicalSort(db, [storyA, storyB]);
|
|
246
292
|
expect(sorted).toBeNull();
|
|
247
293
|
});
|
|
248
294
|
});
|
|
249
295
|
describe('Scheduler Dependency Satisfaction', () => {
|
|
250
|
-
it('should consider merged stories as satisfying dependencies', () => {
|
|
251
|
-
const team = createTeam(db, {
|
|
296
|
+
it('should consider merged stories as satisfying dependencies', async () => {
|
|
297
|
+
const team = await createTeam(db, {
|
|
252
298
|
name: 'Test Team',
|
|
253
299
|
repoUrl: 'https://github.com/test/repo',
|
|
254
300
|
repoPath: 'test',
|
|
255
301
|
});
|
|
256
|
-
const depStory = createStory(db, {
|
|
257
|
-
|
|
302
|
+
const depStory = await createStory(db, {
|
|
303
|
+
teamId: team.id,
|
|
304
|
+
title: 'Dependency',
|
|
305
|
+
description: 'Test',
|
|
306
|
+
});
|
|
307
|
+
const mainStory = await createStory(db, {
|
|
258
308
|
teamId: team.id,
|
|
259
309
|
title: 'Main Story',
|
|
260
310
|
description: 'Test',
|
|
261
311
|
});
|
|
262
|
-
addStoryDependency(db, mainStory.id, depStory.id);
|
|
312
|
+
await addStoryDependency(db, mainStory.id, depStory.id);
|
|
263
313
|
// Initially, dependencies are not satisfied
|
|
264
|
-
let isSatisfied = areDependenciesSatisfied(db, mainStory.id);
|
|
314
|
+
let isSatisfied = await areDependenciesSatisfied(db, mainStory.id);
|
|
265
315
|
expect(isSatisfied).toBe(false);
|
|
266
316
|
// Mark dependency as merged
|
|
267
|
-
updateStory(db, depStory.id, { status: 'merged' });
|
|
268
|
-
isSatisfied = areDependenciesSatisfied(db, mainStory.id);
|
|
317
|
+
await updateStory(db, depStory.id, { status: 'merged' });
|
|
318
|
+
isSatisfied = await areDependenciesSatisfied(db, mainStory.id);
|
|
269
319
|
expect(isSatisfied).toBe(true);
|
|
270
320
|
});
|
|
271
|
-
it('should not consider in-progress stories as satisfying dependencies', () => {
|
|
272
|
-
const team = createTeam(db, {
|
|
321
|
+
it('should not consider in-progress stories as satisfying dependencies', async () => {
|
|
322
|
+
const team = await createTeam(db, {
|
|
273
323
|
name: 'Test Team',
|
|
274
324
|
repoUrl: 'https://github.com/test/repo',
|
|
275
325
|
repoPath: 'test',
|
|
276
326
|
});
|
|
277
|
-
const depStory = createStory(db, {
|
|
278
|
-
|
|
327
|
+
const depStory = await createStory(db, {
|
|
328
|
+
teamId: team.id,
|
|
329
|
+
title: 'Dependency',
|
|
330
|
+
description: 'Test',
|
|
331
|
+
});
|
|
332
|
+
const mainStory = await createStory(db, {
|
|
279
333
|
teamId: team.id,
|
|
280
334
|
title: 'Main Story',
|
|
281
335
|
description: 'Test',
|
|
282
336
|
});
|
|
283
|
-
addStoryDependency(db, mainStory.id, depStory.id);
|
|
337
|
+
await addStoryDependency(db, mainStory.id, depStory.id);
|
|
284
338
|
// Mark dependency as in_progress - this should NOT satisfy the dependency
|
|
285
|
-
updateStory(db, depStory.id, { status: 'in_progress' });
|
|
286
|
-
const isSatisfied = areDependenciesSatisfied(db, mainStory.id);
|
|
339
|
+
await updateStory(db, depStory.id, { status: 'in_progress' });
|
|
340
|
+
const isSatisfied = await areDependenciesSatisfied(db, mainStory.id);
|
|
287
341
|
expect(isSatisfied).toBe(false);
|
|
288
342
|
});
|
|
289
|
-
it('should not consider planned stories as satisfying dependencies', () => {
|
|
290
|
-
const team = createTeam(db, {
|
|
343
|
+
it('should not consider planned stories as satisfying dependencies', async () => {
|
|
344
|
+
const team = await createTeam(db, {
|
|
291
345
|
name: 'Test Team',
|
|
292
346
|
repoUrl: 'https://github.com/test/repo',
|
|
293
347
|
repoPath: 'test',
|
|
294
348
|
});
|
|
295
|
-
const depStory = createStory(db, {
|
|
296
|
-
|
|
349
|
+
const depStory = await createStory(db, {
|
|
350
|
+
teamId: team.id,
|
|
351
|
+
title: 'Dependency',
|
|
352
|
+
description: 'Test',
|
|
353
|
+
});
|
|
354
|
+
const mainStory = await createStory(db, {
|
|
297
355
|
teamId: team.id,
|
|
298
356
|
title: 'Main Story',
|
|
299
357
|
description: 'Test',
|
|
300
358
|
});
|
|
301
|
-
addStoryDependency(db, mainStory.id, depStory.id);
|
|
359
|
+
await addStoryDependency(db, mainStory.id, depStory.id);
|
|
302
360
|
// Update main story status to planned (default)
|
|
303
|
-
updateStory(db, mainStory.id, { status: 'planned' });
|
|
304
|
-
const isSatisfied = areDependenciesSatisfied(db, mainStory.id);
|
|
361
|
+
await updateStory(db, mainStory.id, { status: 'planned' });
|
|
362
|
+
const isSatisfied = await areDependenciesSatisfied(db, mainStory.id);
|
|
305
363
|
expect(isSatisfied).toBe(false);
|
|
306
364
|
});
|
|
307
|
-
it('should handle multiple dependencies', () => {
|
|
308
|
-
const team = createTeam(db, {
|
|
365
|
+
it('should handle multiple dependencies', async () => {
|
|
366
|
+
const team = await createTeam(db, {
|
|
309
367
|
name: 'Test Team',
|
|
310
368
|
repoUrl: 'https://github.com/test/repo',
|
|
311
369
|
repoPath: 'test',
|
|
312
370
|
});
|
|
313
|
-
const dep1 = createStory(db, { teamId: team.id, title: 'Dep 1', description: 'Test' });
|
|
314
|
-
const dep2 = createStory(db, { teamId: team.id, title: 'Dep 2', description: 'Test' });
|
|
315
|
-
const mainStory = createStory(db, {
|
|
371
|
+
const dep1 = await createStory(db, { teamId: team.id, title: 'Dep 1', description: 'Test' });
|
|
372
|
+
const dep2 = await createStory(db, { teamId: team.id, title: 'Dep 2', description: 'Test' });
|
|
373
|
+
const mainStory = await createStory(db, {
|
|
316
374
|
teamId: team.id,
|
|
317
375
|
title: 'Main Story',
|
|
318
376
|
description: 'Test',
|
|
319
377
|
});
|
|
320
|
-
addStoryDependency(db, mainStory.id, dep1.id);
|
|
321
|
-
addStoryDependency(db, mainStory.id, dep2.id);
|
|
378
|
+
await addStoryDependency(db, mainStory.id, dep1.id);
|
|
379
|
+
await addStoryDependency(db, mainStory.id, dep2.id);
|
|
322
380
|
// Mark only first dependency as merged
|
|
323
|
-
updateStory(db, dep1.id, { status: 'merged' });
|
|
324
|
-
let isSatisfied = areDependenciesSatisfied(db, mainStory.id);
|
|
381
|
+
await updateStory(db, dep1.id, { status: 'merged' });
|
|
382
|
+
let isSatisfied = await areDependenciesSatisfied(db, mainStory.id);
|
|
325
383
|
expect(isSatisfied).toBe(false);
|
|
326
384
|
// Mark second dependency as merged too
|
|
327
|
-
updateStory(db, dep2.id, { status: 'merged' });
|
|
328
|
-
isSatisfied = areDependenciesSatisfied(db, mainStory.id);
|
|
385
|
+
await updateStory(db, dep2.id, { status: 'merged' });
|
|
386
|
+
isSatisfied = await areDependenciesSatisfied(db, mainStory.id);
|
|
329
387
|
expect(isSatisfied).toBe(true);
|
|
330
388
|
});
|
|
331
389
|
});
|
|
332
390
|
describe('Scheduler Build Dependency Graph', () => {
|
|
333
|
-
it('should correctly build a dependency graph', () => {
|
|
334
|
-
const team = createTeam(db, {
|
|
391
|
+
it('should correctly build a dependency graph', async () => {
|
|
392
|
+
const team = await createTeam(db, {
|
|
335
393
|
name: 'Test Team',
|
|
336
394
|
repoUrl: 'https://github.com/test/repo',
|
|
337
395
|
repoPath: 'test',
|
|
338
396
|
});
|
|
339
|
-
const storyA = createStory(db, {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
397
|
+
const storyA = await createStory(db, {
|
|
398
|
+
teamId: team.id,
|
|
399
|
+
title: 'Story A',
|
|
400
|
+
description: 'Test',
|
|
401
|
+
});
|
|
402
|
+
const storyB = await createStory(db, {
|
|
403
|
+
teamId: team.id,
|
|
404
|
+
title: 'Story B',
|
|
405
|
+
description: 'Test',
|
|
406
|
+
});
|
|
407
|
+
const storyC = await createStory(db, {
|
|
408
|
+
teamId: team.id,
|
|
409
|
+
title: 'Story C',
|
|
410
|
+
description: 'Test',
|
|
411
|
+
});
|
|
412
|
+
await addStoryDependency(db, storyB.id, storyA.id);
|
|
413
|
+
await addStoryDependency(db, storyC.id, storyA.id);
|
|
414
|
+
const graph = await buildDependencyGraph(db, [storyA, storyB, storyC]);
|
|
345
415
|
expect(graph.has(storyA.id)).toBe(true);
|
|
346
416
|
expect(graph.has(storyB.id)).toBe(true);
|
|
347
417
|
expect(graph.has(storyC.id)).toBe(true);
|
|
@@ -349,26 +419,38 @@ describe('Scheduler Build Dependency Graph', () => {
|
|
|
349
419
|
expect(graph.get(storyB.id)).toEqual(new Set([storyA.id]));
|
|
350
420
|
expect(graph.get(storyC.id)).toEqual(new Set([storyA.id]));
|
|
351
421
|
});
|
|
352
|
-
it('should include only stories in the input list', () => {
|
|
353
|
-
const team = createTeam(db, {
|
|
422
|
+
it('should include only stories in the input list', async () => {
|
|
423
|
+
const team = await createTeam(db, {
|
|
354
424
|
name: 'Test Team',
|
|
355
425
|
repoUrl: 'https://github.com/test/repo',
|
|
356
426
|
repoPath: 'test',
|
|
357
427
|
});
|
|
358
|
-
const storyA = createStory(db, {
|
|
359
|
-
|
|
360
|
-
|
|
428
|
+
const storyA = await createStory(db, {
|
|
429
|
+
teamId: team.id,
|
|
430
|
+
title: 'Story A',
|
|
431
|
+
description: 'Test',
|
|
432
|
+
});
|
|
433
|
+
const storyB = await createStory(db, {
|
|
434
|
+
teamId: team.id,
|
|
435
|
+
title: 'Story B',
|
|
436
|
+
description: 'Test',
|
|
437
|
+
});
|
|
438
|
+
const storyC = await createStory(db, {
|
|
439
|
+
teamId: team.id,
|
|
440
|
+
title: 'Story C',
|
|
441
|
+
description: 'Test',
|
|
442
|
+
});
|
|
361
443
|
// B depends on A (A is not in the filter list)
|
|
362
|
-
addStoryDependency(db, storyB.id, storyA.id);
|
|
444
|
+
await addStoryDependency(db, storyB.id, storyA.id);
|
|
363
445
|
// Only include B and C in the graph
|
|
364
|
-
const graph = buildDependencyGraph(db, [storyB, storyC]);
|
|
446
|
+
const graph = await buildDependencyGraph(db, [storyB, storyC]);
|
|
365
447
|
expect(graph.has(storyB.id)).toBe(true);
|
|
366
448
|
expect(graph.has(storyC.id)).toBe(true);
|
|
367
449
|
expect(graph.has(storyA.id)).toBe(false);
|
|
368
450
|
});
|
|
369
451
|
});
|
|
370
452
|
describe('Scheduler Worktree Removal', () => {
|
|
371
|
-
it('should remove worktrees with a short cleanup timeout', () => {
|
|
453
|
+
it('should remove worktrees with a short cleanup timeout', async () => {
|
|
372
454
|
const removeSpy = vi.spyOn(worktreeModule, 'removeWorktree').mockReturnValue({
|
|
373
455
|
success: true,
|
|
374
456
|
fullWorktreePath: '/tmp/repos/test-agent-1',
|
|
@@ -378,7 +460,7 @@ describe('Scheduler Worktree Removal', () => {
|
|
|
378
460
|
expect(removeSpy).toHaveBeenCalledWith('/tmp', 'repos/test-agent-1', { timeout: 5000 });
|
|
379
461
|
vi.restoreAllMocks();
|
|
380
462
|
});
|
|
381
|
-
it('should log worktree removal failures to the database', () => {
|
|
463
|
+
it('should log worktree removal failures to the database', async () => {
|
|
382
464
|
// Mock the shared removeWorktree to simulate failure
|
|
383
465
|
vi.spyOn(worktreeModule, 'removeWorktree').mockReturnValue({
|
|
384
466
|
success: false,
|
|
@@ -386,9 +468,9 @@ describe('Scheduler Worktree Removal', () => {
|
|
|
386
468
|
fullWorktreePath: '/tmp/repos/test-agent-1',
|
|
387
469
|
});
|
|
388
470
|
const removeMethod = scheduler.removeAgentWorktree;
|
|
389
|
-
removeMethod.call(scheduler, 'repos/test-agent-1', 'agent-test-1');
|
|
471
|
+
await removeMethod.call(scheduler, 'repos/test-agent-1', 'agent-test-1');
|
|
390
472
|
// Check that the failure was logged
|
|
391
|
-
const logs = getLogsByEventType(db, 'WORKTREE_REMOVAL_FAILED');
|
|
473
|
+
const logs = await getLogsByEventType(db, 'WORKTREE_REMOVAL_FAILED');
|
|
392
474
|
expect(logs).toHaveLength(1);
|
|
393
475
|
expect(logs[0].agent_id).toBe('agent-test-1');
|
|
394
476
|
expect(logs[0].event_type).toBe('WORKTREE_REMOVAL_FAILED');
|
|
@@ -396,249 +478,253 @@ describe('Scheduler Worktree Removal', () => {
|
|
|
396
478
|
expect(logs[0].message).toContain('Permission denied');
|
|
397
479
|
vi.restoreAllMocks();
|
|
398
480
|
});
|
|
399
|
-
it('should handle empty worktree paths gracefully', () => {
|
|
481
|
+
it('should handle empty worktree paths gracefully', async () => {
|
|
400
482
|
const removeMethod = scheduler.removeAgentWorktree;
|
|
401
483
|
// Should return without error for empty path
|
|
402
484
|
removeMethod.call(scheduler, '', 'agent-test-1');
|
|
403
485
|
// Should not log anything
|
|
404
|
-
const logs = getLogsByEventType(db, 'WORKTREE_REMOVAL_FAILED');
|
|
486
|
+
const logs = await getLogsByEventType(db, 'WORKTREE_REMOVAL_FAILED');
|
|
405
487
|
expect(logs).toHaveLength(0);
|
|
406
488
|
});
|
|
407
489
|
});
|
|
408
490
|
describe('Scheduler Orphaned Story Recovery', () => {
|
|
409
491
|
it('should recover orphaned stories assigned to terminated agents', async () => {
|
|
410
492
|
// Setup: Create team, agents, and a story
|
|
411
|
-
const team = createTeam(db, {
|
|
493
|
+
const team = await createTeam(db, {
|
|
412
494
|
name: 'Test Team',
|
|
413
495
|
repoUrl: 'https://github.com/test/repo',
|
|
414
496
|
repoPath: 'test',
|
|
415
497
|
});
|
|
416
498
|
// Create a terminated agent in the database
|
|
417
499
|
const terminatedAgentId = 'agent-terminated-1';
|
|
418
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
500
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
419
501
|
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`, [terminatedAgentId, 'intermediate', team.id, 'terminated']);
|
|
420
502
|
// Create a story assigned to the terminated agent
|
|
421
|
-
const story = createStory(db, {
|
|
503
|
+
const story = await createStory(db, {
|
|
422
504
|
teamId: team.id,
|
|
423
505
|
title: 'Orphaned Story',
|
|
424
506
|
description: 'Test',
|
|
425
507
|
});
|
|
426
|
-
updateStory(db, story.id, {
|
|
508
|
+
await updateStory(db, story.id, {
|
|
427
509
|
assignedAgentId: terminatedAgentId,
|
|
428
510
|
status: 'in_progress',
|
|
429
511
|
});
|
|
430
512
|
// Get the recovery method
|
|
431
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
513
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
432
514
|
// Verify the story was recovered
|
|
433
515
|
expect(recovered).toContain(story.id);
|
|
434
516
|
expect(recovered.length).toBe(1);
|
|
435
517
|
// Verify the story's assignment was cleared and status changed
|
|
436
|
-
const recoveredStory = db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
518
|
+
const recoveredStory = db.db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
437
519
|
expect(recoveredStory?.[0]).toBeNull(); // assigned_agent_id should be null
|
|
438
520
|
expect(recoveredStory?.[1]).toBe('planned'); // status should be 'planned'
|
|
439
521
|
});
|
|
440
522
|
it('should not affect stories assigned to active agents', async () => {
|
|
441
|
-
const team = createTeam(db, {
|
|
523
|
+
const team = await createTeam(db, {
|
|
442
524
|
name: 'Test Team',
|
|
443
525
|
repoUrl: 'https://github.com/test/repo',
|
|
444
526
|
repoPath: 'test',
|
|
445
527
|
});
|
|
446
528
|
// Create an active (non-terminated) agent
|
|
447
529
|
const activeAgentId = 'agent-active-1';
|
|
448
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
530
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
449
531
|
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`, [activeAgentId, 'intermediate', team.id, 'working']);
|
|
450
532
|
// Create a story assigned to the active agent
|
|
451
|
-
const story = createStory(db, {
|
|
452
|
-
|
|
533
|
+
const story = await createStory(db, {
|
|
534
|
+
teamId: team.id,
|
|
535
|
+
title: 'Active Story',
|
|
536
|
+
description: 'Test',
|
|
537
|
+
});
|
|
538
|
+
await updateStory(db, story.id, {
|
|
453
539
|
assignedAgentId: activeAgentId,
|
|
454
540
|
status: 'in_progress',
|
|
455
541
|
});
|
|
456
|
-
db.run(`UPDATE agents SET current_story_id = ? WHERE id = ?`, [story.id, activeAgentId]);
|
|
542
|
+
db.db.run(`UPDATE agents SET current_story_id = ? WHERE id = ?`, [story.id, activeAgentId]);
|
|
457
543
|
// Get the recovery method
|
|
458
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
544
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
459
545
|
// Verify no stories were recovered
|
|
460
546
|
expect(recovered.length).toBe(0);
|
|
461
547
|
// Verify the story's assignment was NOT changed
|
|
462
|
-
const unchangedStory = db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
548
|
+
const unchangedStory = db.db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
463
549
|
expect(unchangedStory?.[0]).toBe(activeAgentId);
|
|
464
550
|
expect(unchangedStory?.[1]).toBe('in_progress');
|
|
465
551
|
});
|
|
466
552
|
it('should recover in_progress stories assigned to idle agents with no current story', async () => {
|
|
467
|
-
const team = createTeam(db, {
|
|
553
|
+
const team = await createTeam(db, {
|
|
468
554
|
name: 'Inconsistent Team',
|
|
469
555
|
repoUrl: 'https://github.com/test/repo',
|
|
470
556
|
repoPath: 'test',
|
|
471
557
|
});
|
|
472
558
|
const idleAgentId = 'agent-idle-1';
|
|
473
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
559
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
474
560
|
VALUES (?, ?, ?, ?, NULL, datetime('now'), datetime('now'))`, [idleAgentId, 'intermediate', team.id, 'idle']);
|
|
475
|
-
const story = createStory(db, {
|
|
561
|
+
const story = await createStory(db, {
|
|
476
562
|
teamId: team.id,
|
|
477
563
|
title: 'Inconsistent Assignment Story',
|
|
478
564
|
description: 'Assigned to idle agent',
|
|
479
565
|
});
|
|
480
|
-
updateStory(db, story.id, {
|
|
566
|
+
await updateStory(db, story.id, {
|
|
481
567
|
assignedAgentId: idleAgentId,
|
|
482
568
|
status: 'in_progress',
|
|
483
569
|
});
|
|
484
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
570
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
485
571
|
expect(recovered).toContain(story.id);
|
|
486
|
-
const recoveredStory = db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
572
|
+
const recoveredStory = db.db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
487
573
|
expect(recoveredStory?.[0]).toBeNull();
|
|
488
574
|
expect(recoveredStory?.[1]).toBe('planned');
|
|
489
575
|
});
|
|
490
576
|
it('should recover in_progress stories when agent current_story_id points to a different story', async () => {
|
|
491
|
-
const team = createTeam(db, {
|
|
577
|
+
const team = await createTeam(db, {
|
|
492
578
|
name: 'Mismatched Team',
|
|
493
579
|
repoUrl: 'https://github.com/test/repo',
|
|
494
580
|
repoPath: 'test',
|
|
495
581
|
});
|
|
496
582
|
const workingAgentId = 'agent-working-mismatch-1';
|
|
497
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
583
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
498
584
|
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))`, [workingAgentId, 'intermediate', team.id, 'working', 'STORY-OTHER']);
|
|
499
|
-
const story = createStory(db, {
|
|
585
|
+
const story = await createStory(db, {
|
|
500
586
|
teamId: team.id,
|
|
501
587
|
title: 'Mismatched Assignment Story',
|
|
502
588
|
description: 'Assigned story does not match agent current story',
|
|
503
589
|
});
|
|
504
|
-
updateStory(db, story.id, {
|
|
590
|
+
await updateStory(db, story.id, {
|
|
505
591
|
assignedAgentId: workingAgentId,
|
|
506
592
|
status: 'in_progress',
|
|
507
593
|
});
|
|
508
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
594
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
509
595
|
expect(recovered).toContain(story.id);
|
|
510
|
-
const recoveredStory = db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
596
|
+
const recoveredStory = db.db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
511
597
|
expect(recoveredStory?.[0]).toBeNull();
|
|
512
598
|
expect(recoveredStory?.[1]).toBe('planned');
|
|
513
599
|
});
|
|
514
600
|
it('should recover in_progress stories assigned to blocked agents even with matching current story', async () => {
|
|
515
|
-
const team = createTeam(db, {
|
|
601
|
+
const team = await createTeam(db, {
|
|
516
602
|
name: 'Blocked Team',
|
|
517
603
|
repoUrl: 'https://github.com/test/repo',
|
|
518
604
|
repoPath: 'test',
|
|
519
605
|
});
|
|
520
606
|
const blockedAgentId = 'agent-blocked-1';
|
|
521
|
-
const story = createStory(db, {
|
|
607
|
+
const story = await createStory(db, {
|
|
522
608
|
teamId: team.id,
|
|
523
609
|
title: 'Blocked Assignment Story',
|
|
524
610
|
description: 'Assigned to blocked agent',
|
|
525
611
|
});
|
|
526
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
612
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
527
613
|
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))`, [blockedAgentId, 'intermediate', team.id, 'blocked', story.id]);
|
|
528
|
-
updateStory(db, story.id, {
|
|
614
|
+
await updateStory(db, story.id, {
|
|
529
615
|
assignedAgentId: blockedAgentId,
|
|
530
616
|
status: 'in_progress',
|
|
531
617
|
});
|
|
532
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
618
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
533
619
|
expect(recovered).toContain(story.id);
|
|
534
|
-
const recoveredStory = db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
620
|
+
const recoveredStory = db.db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${story.id}'`)[0]?.values[0];
|
|
535
621
|
expect(recoveredStory?.[0]).toBeNull();
|
|
536
622
|
expect(recoveredStory?.[1]).toBe('planned');
|
|
537
623
|
});
|
|
538
624
|
it('should not recover non-in_progress stories with inconsistent assignment', async () => {
|
|
539
|
-
const team = createTeam(db, {
|
|
625
|
+
const team = await createTeam(db, {
|
|
540
626
|
name: 'Review Team',
|
|
541
627
|
repoUrl: 'https://github.com/test/repo',
|
|
542
628
|
repoPath: 'test',
|
|
543
629
|
});
|
|
544
630
|
const idleAgentId = 'agent-idle-review-1';
|
|
545
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
631
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
546
632
|
VALUES (?, ?, ?, ?, NULL, datetime('now'), datetime('now'))`, [idleAgentId, 'intermediate', team.id, 'idle']);
|
|
547
|
-
const reviewStory = createStory(db, {
|
|
633
|
+
const reviewStory = await createStory(db, {
|
|
548
634
|
teamId: team.id,
|
|
549
635
|
title: 'Review Story',
|
|
550
636
|
description: 'Should not be recovered by in_progress consistency check',
|
|
551
637
|
});
|
|
552
|
-
updateStory(db, reviewStory.id, {
|
|
638
|
+
await updateStory(db, reviewStory.id, {
|
|
553
639
|
assignedAgentId: idleAgentId,
|
|
554
640
|
status: 'review',
|
|
555
641
|
});
|
|
556
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
642
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
557
643
|
expect(recovered).not.toContain(reviewStory.id);
|
|
558
|
-
const unchangedStory = db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${reviewStory.id}'`)[0]?.values[0];
|
|
644
|
+
const unchangedStory = db.db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${reviewStory.id}'`)[0]?.values[0];
|
|
559
645
|
expect(unchangedStory?.[0]).toBe(idleAgentId);
|
|
560
646
|
expect(unchangedStory?.[1]).toBe('review');
|
|
561
647
|
});
|
|
562
648
|
it('should recover stale in_progress stories without assigned agents', async () => {
|
|
563
|
-
const team = createTeam(db, {
|
|
649
|
+
const team = await createTeam(db, {
|
|
564
650
|
name: 'Stale Team',
|
|
565
651
|
repoUrl: 'https://github.com/test/repo',
|
|
566
652
|
repoPath: 'test',
|
|
567
653
|
});
|
|
568
|
-
const staleStory = createStory(db, {
|
|
654
|
+
const staleStory = await createStory(db, {
|
|
569
655
|
teamId: team.id,
|
|
570
656
|
title: 'Stale In Progress Story',
|
|
571
657
|
description: 'Lost assignment',
|
|
572
658
|
});
|
|
573
|
-
updateStory(db, staleStory.id, {
|
|
659
|
+
await updateStory(db, staleStory.id, {
|
|
574
660
|
status: 'in_progress',
|
|
575
661
|
assignedAgentId: null,
|
|
576
662
|
});
|
|
577
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
663
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
578
664
|
expect(recovered).toContain(staleStory.id);
|
|
579
|
-
const recoveredStory = db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${staleStory.id}'`)[0]?.values[0];
|
|
665
|
+
const recoveredStory = db.db.exec(`SELECT assigned_agent_id, status FROM stories WHERE id = '${staleStory.id}'`)[0]?.values[0];
|
|
580
666
|
expect(recoveredStory?.[0]).toBeNull();
|
|
581
667
|
expect(recoveredStory?.[1]).toBe('planned');
|
|
582
668
|
});
|
|
583
669
|
it('should not recover planned stories that are unassigned', async () => {
|
|
584
|
-
const team = createTeam(db, {
|
|
670
|
+
const team = await createTeam(db, {
|
|
585
671
|
name: 'Planned Team',
|
|
586
672
|
repoUrl: 'https://github.com/test/repo',
|
|
587
673
|
repoPath: 'test',
|
|
588
674
|
});
|
|
589
|
-
const plannedStory = createStory(db, {
|
|
675
|
+
const plannedStory = await createStory(db, {
|
|
590
676
|
teamId: team.id,
|
|
591
677
|
title: 'Already Planned',
|
|
592
678
|
description: 'Should stay planned',
|
|
593
679
|
});
|
|
594
|
-
updateStory(db, plannedStory.id, {
|
|
680
|
+
await updateStory(db, plannedStory.id, {
|
|
595
681
|
status: 'planned',
|
|
596
682
|
assignedAgentId: null,
|
|
597
683
|
});
|
|
598
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
684
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
599
685
|
expect(recovered).not.toContain(plannedStory.id);
|
|
600
686
|
});
|
|
601
687
|
it('should recover multiple orphaned stories', async () => {
|
|
602
|
-
const team = createTeam(db, {
|
|
688
|
+
const team = await createTeam(db, {
|
|
603
689
|
name: 'Test Team',
|
|
604
690
|
repoUrl: 'https://github.com/test/repo',
|
|
605
691
|
repoPath: 'test',
|
|
606
692
|
});
|
|
607
693
|
// Create a terminated agent
|
|
608
694
|
const terminatedAgentId = 'agent-terminated-2';
|
|
609
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
695
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
610
696
|
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`, [terminatedAgentId, 'intermediate', team.id, 'terminated']);
|
|
611
697
|
// Create multiple stories assigned to the terminated agent
|
|
612
|
-
const story1 = createStory(db, {
|
|
698
|
+
const story1 = await createStory(db, {
|
|
613
699
|
teamId: team.id,
|
|
614
700
|
title: 'Orphaned Story 1',
|
|
615
701
|
description: 'Test',
|
|
616
702
|
});
|
|
617
|
-
const story2 = createStory(db, {
|
|
703
|
+
const story2 = await createStory(db, {
|
|
618
704
|
teamId: team.id,
|
|
619
705
|
title: 'Orphaned Story 2',
|
|
620
706
|
description: 'Test',
|
|
621
707
|
});
|
|
622
|
-
updateStory(db, story1.id, {
|
|
708
|
+
await updateStory(db, story1.id, {
|
|
623
709
|
assignedAgentId: terminatedAgentId,
|
|
624
710
|
status: 'in_progress',
|
|
625
711
|
});
|
|
626
|
-
updateStory(db, story2.id, {
|
|
712
|
+
await updateStory(db, story2.id, {
|
|
627
713
|
assignedAgentId: terminatedAgentId,
|
|
628
714
|
status: 'review',
|
|
629
715
|
});
|
|
630
716
|
// Get the recovery method
|
|
631
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp');
|
|
717
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp');
|
|
632
718
|
// Verify both stories were recovered
|
|
633
719
|
expect(recovered.length).toBe(2);
|
|
634
720
|
expect(recovered).toContain(story1.id);
|
|
635
721
|
expect(recovered).toContain(story2.id);
|
|
636
722
|
});
|
|
637
|
-
it('should write markdown files when storiesDir is provided during orphan recovery', () => {
|
|
723
|
+
it('should write markdown files when storiesDir is provided during orphan recovery', async () => {
|
|
638
724
|
const storiesDir = join(tmpdir(), `hive-test-stories-${Date.now()}`);
|
|
639
725
|
mkdirSync(storiesDir, { recursive: true });
|
|
640
726
|
try {
|
|
641
|
-
const team = createTeam(db, {
|
|
727
|
+
const team = await createTeam(db, {
|
|
642
728
|
name: 'MD Test Team',
|
|
643
729
|
repoUrl: 'https://github.com/test/repo',
|
|
644
730
|
repoPath: 'test',
|
|
@@ -646,22 +732,22 @@ describe('Scheduler Orphaned Story Recovery', () => {
|
|
|
646
732
|
const terminatedAgentId = 'agent-md-terminated';
|
|
647
733
|
db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
648
734
|
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`, [terminatedAgentId, 'intermediate', team.id, 'terminated']);
|
|
649
|
-
const story = createStory(db, {
|
|
735
|
+
const story = await createStory(db, {
|
|
650
736
|
teamId: team.id,
|
|
651
737
|
title: 'Story with Markdown',
|
|
652
738
|
description: 'Should get a markdown file on recovery',
|
|
653
739
|
});
|
|
654
|
-
updateStory(db, story.id, {
|
|
740
|
+
await updateStory(db, story.id, {
|
|
655
741
|
assignedAgentId: terminatedAgentId,
|
|
656
742
|
status: 'in_progress',
|
|
657
743
|
});
|
|
658
|
-
const recovered = detectAndRecoverOrphanedStories(db, '/tmp', storiesDir);
|
|
744
|
+
const recovered = await detectAndRecoverOrphanedStories(db, '/tmp', storiesDir);
|
|
659
745
|
expect(recovered).toContain(story.id);
|
|
660
746
|
// Verify markdown file was written
|
|
661
747
|
const mdPath = join(storiesDir, `${story.id}.md`);
|
|
662
748
|
expect(existsSync(mdPath)).toBe(true);
|
|
663
749
|
// Verify markdown_path was set in DB
|
|
664
|
-
const updatedStory = getStoryById(db, story.id);
|
|
750
|
+
const updatedStory = await getStoryById(db, story.id);
|
|
665
751
|
expect(updatedStory?.markdown_path).toBe(mdPath);
|
|
666
752
|
}
|
|
667
753
|
finally {
|
|
@@ -676,83 +762,83 @@ describe('Scheduler Refactor Capacity Policy', () => {
|
|
|
676
762
|
refactor: config,
|
|
677
763
|
};
|
|
678
764
|
}
|
|
679
|
-
it('should enforce refactor budget based on feature workload', () => {
|
|
680
|
-
const team = createTeam(db, {
|
|
765
|
+
it('should enforce refactor budget based on feature workload', async () => {
|
|
766
|
+
const team = await createTeam(db, {
|
|
681
767
|
name: 'Refactor Team',
|
|
682
768
|
repoUrl: 'https://github.com/test/repo',
|
|
683
769
|
repoPath: 'test',
|
|
684
770
|
});
|
|
685
|
-
const feature = createStory(db, {
|
|
771
|
+
const feature = await createStory(db, {
|
|
686
772
|
teamId: team.id,
|
|
687
773
|
title: 'Add endpoint',
|
|
688
774
|
description: 'Feature story',
|
|
689
775
|
});
|
|
690
|
-
updateStory(db, feature.id, { status: 'planned', storyPoints: 10, complexityScore: 10 });
|
|
691
|
-
const refactorA = createStory(db, {
|
|
776
|
+
await updateStory(db, feature.id, { status: 'planned', storyPoints: 10, complexityScore: 10 });
|
|
777
|
+
const refactorA = await createStory(db, {
|
|
692
778
|
teamId: team.id,
|
|
693
779
|
title: 'Refactor: clean parser',
|
|
694
780
|
description: 'Refactor A',
|
|
695
781
|
});
|
|
696
|
-
updateStory(db, refactorA.id, { status: 'planned', storyPoints: 1, complexityScore: 1 });
|
|
697
|
-
const refactorB = createStory(db, {
|
|
782
|
+
await updateStory(db, refactorA.id, { status: 'planned', storyPoints: 1, complexityScore: 1 });
|
|
783
|
+
const refactorB = await createStory(db, {
|
|
698
784
|
teamId: team.id,
|
|
699
785
|
title: 'Refactor: simplify auth flow',
|
|
700
786
|
description: 'Refactor B',
|
|
701
787
|
});
|
|
702
|
-
updateStory(db, refactorB.id, { status: 'planned', storyPoints: 2, complexityScore: 2 });
|
|
788
|
+
await updateStory(db, refactorB.id, { status: 'planned', storyPoints: 2, complexityScore: 2 });
|
|
703
789
|
const scalingConfig = createRefactorScalingConfig({
|
|
704
790
|
enabled: true,
|
|
705
791
|
capacity_percent: 10,
|
|
706
792
|
allow_without_feature_work: true,
|
|
707
793
|
});
|
|
708
|
-
const selected = selectStoriesForCapacity([
|
|
709
|
-
getStoryById(db, feature.id),
|
|
710
|
-
getStoryById(db, refactorA.id),
|
|
711
|
-
getStoryById(db, refactorB.id),
|
|
712
|
-
], scalingConfig);
|
|
794
|
+
const selected = (await selectStoriesForCapacity([
|
|
795
|
+
(await getStoryById(db, feature.id)),
|
|
796
|
+
(await getStoryById(db, refactorA.id)),
|
|
797
|
+
(await getStoryById(db, refactorB.id)),
|
|
798
|
+
], scalingConfig));
|
|
713
799
|
expect(selected.map(s => s.id)).toContain(feature.id);
|
|
714
800
|
expect(selected.map(s => s.id)).toContain(refactorA.id);
|
|
715
801
|
expect(selected.map(s => s.id)).not.toContain(refactorB.id);
|
|
716
802
|
});
|
|
717
|
-
it('should allow refactor-only queues when policy permits', () => {
|
|
718
|
-
const team = createTeam(db, {
|
|
803
|
+
it('should allow refactor-only queues when policy permits', async () => {
|
|
804
|
+
const team = await createTeam(db, {
|
|
719
805
|
name: 'Maintenance Team',
|
|
720
806
|
repoUrl: 'https://github.com/test/repo',
|
|
721
807
|
repoPath: 'test',
|
|
722
808
|
});
|
|
723
|
-
const refactor = createStory(db, {
|
|
809
|
+
const refactor = await createStory(db, {
|
|
724
810
|
teamId: team.id,
|
|
725
811
|
title: 'Refactor: remove dead code',
|
|
726
812
|
description: 'Maintenance',
|
|
727
813
|
});
|
|
728
|
-
updateStory(db, refactor.id, { status: 'planned', storyPoints: 3, complexityScore: 3 });
|
|
814
|
+
await updateStory(db, refactor.id, { status: 'planned', storyPoints: 3, complexityScore: 3 });
|
|
729
815
|
const scalingConfig = createRefactorScalingConfig({
|
|
730
816
|
enabled: true,
|
|
731
817
|
capacity_percent: 10,
|
|
732
818
|
allow_without_feature_work: true,
|
|
733
819
|
});
|
|
734
|
-
const selected = selectStoriesForCapacity([getStoryById(db, refactor.id)], scalingConfig);
|
|
820
|
+
const selected = (await selectStoriesForCapacity([(await getStoryById(db, refactor.id))], scalingConfig));
|
|
735
821
|
expect(selected).toHaveLength(1);
|
|
736
822
|
expect(selected[0].id).toBe(refactor.id);
|
|
737
823
|
});
|
|
738
|
-
it('should block refactor-only queues when policy disallows it', () => {
|
|
739
|
-
const team = createTeam(db, {
|
|
824
|
+
it('should block refactor-only queues when policy disallows it', async () => {
|
|
825
|
+
const team = await createTeam(db, {
|
|
740
826
|
name: 'Strict Team',
|
|
741
827
|
repoUrl: 'https://github.com/test/repo',
|
|
742
828
|
repoPath: 'test',
|
|
743
829
|
});
|
|
744
|
-
const refactor = createStory(db, {
|
|
830
|
+
const refactor = await createStory(db, {
|
|
745
831
|
teamId: team.id,
|
|
746
832
|
title: 'Refactor: rename internals',
|
|
747
833
|
description: 'Maintenance',
|
|
748
834
|
});
|
|
749
|
-
updateStory(db, refactor.id, { status: 'planned', storyPoints: 2, complexityScore: 2 });
|
|
835
|
+
await updateStory(db, refactor.id, { status: 'planned', storyPoints: 2, complexityScore: 2 });
|
|
750
836
|
const scalingConfig = createRefactorScalingConfig({
|
|
751
837
|
enabled: true,
|
|
752
838
|
capacity_percent: 10,
|
|
753
839
|
allow_without_feature_work: false,
|
|
754
840
|
});
|
|
755
|
-
const selected = selectStoriesForCapacity([getStoryById(db, refactor.id)], scalingConfig);
|
|
841
|
+
const selected = (await selectStoriesForCapacity([(await getStoryById(db, refactor.id))], scalingConfig));
|
|
756
842
|
expect(selected).toHaveLength(0);
|
|
757
843
|
});
|
|
758
844
|
});
|
|
@@ -823,29 +909,29 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
823
909
|
expect(isRefactorStory(mkStory(title))).toBe(false);
|
|
824
910
|
});
|
|
825
911
|
// 5 tests: capacity point calculation
|
|
826
|
-
it('should use story_points when both story_points and complexity_score exist', () => {
|
|
912
|
+
it('should use story_points when both story_points and complexity_score exist', async () => {
|
|
827
913
|
expect(getCapacityPoints(mkStory('Feature', 8, 3))).toBe(8);
|
|
828
914
|
});
|
|
829
|
-
it('should use complexity_score when story_points is null', () => {
|
|
915
|
+
it('should use complexity_score when story_points is null', async () => {
|
|
830
916
|
expect(getCapacityPoints(mkStory('Feature', null, 5))).toBe(5);
|
|
831
917
|
});
|
|
832
|
-
it('should default to 1 when both story_points and complexity_score are null', () => {
|
|
918
|
+
it('should default to 1 when both story_points and complexity_score are null', async () => {
|
|
833
919
|
expect(getCapacityPoints(mkStory('Feature', null, null))).toBe(1);
|
|
834
920
|
});
|
|
835
|
-
it('should treat story_points 0 as missing and fall back to complexity_score', () => {
|
|
921
|
+
it('should treat story_points 0 as missing and fall back to complexity_score', async () => {
|
|
836
922
|
expect(getCapacityPoints(mkStory('Feature', 0, 4))).toBe(4);
|
|
837
923
|
});
|
|
838
|
-
it('should treat 0/0 points as minimum 1 capacity unit', () => {
|
|
924
|
+
it('should treat 0/0 points as minimum 1 capacity unit', async () => {
|
|
839
925
|
expect(getCapacityPoints(mkStory('Feature', 0, 0))).toBe(1);
|
|
840
926
|
});
|
|
841
|
-
it('should use story_points when complexity_score is null', () => {
|
|
927
|
+
it('should use story_points when complexity_score is null', async () => {
|
|
842
928
|
expect(getCapacityPoints(mkStory('Feature', 6, null))).toBe(6);
|
|
843
929
|
});
|
|
844
|
-
it('should pass through non-integer capacity points as provided', () => {
|
|
930
|
+
it('should pass through non-integer capacity points as provided', async () => {
|
|
845
931
|
expect(getCapacityPoints(mkStory('Feature', 2.5, null))).toBe(2.5);
|
|
846
932
|
});
|
|
847
933
|
// 12 tests: capacity selection behavior
|
|
848
|
-
it('should filter out refactor stories when refactor policy is disabled', () => {
|
|
934
|
+
it('should filter out refactor stories when refactor policy is disabled', async () => {
|
|
849
935
|
const scalingConfig = mkScalingConfig({
|
|
850
936
|
enabled: false,
|
|
851
937
|
capacity_percent: 100,
|
|
@@ -853,10 +939,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
853
939
|
});
|
|
854
940
|
const feature = mkStory('Feature: add endpoint', 8, 8);
|
|
855
941
|
const refactor = mkStory('Refactor: split parser', 2, 2);
|
|
856
|
-
const selected = selectStoriesForCapacity([feature, refactor], scalingConfig);
|
|
942
|
+
const selected = (await selectStoriesForCapacity([feature, refactor], scalingConfig));
|
|
857
943
|
expect(selected.map(s => s.id)).toEqual([feature.id]);
|
|
858
944
|
});
|
|
859
|
-
it('should include all refactor stories when capacity percent is 100', () => {
|
|
945
|
+
it('should include all refactor stories when capacity percent is 100', async () => {
|
|
860
946
|
const scalingConfig = mkScalingConfig({
|
|
861
947
|
enabled: true,
|
|
862
948
|
capacity_percent: 100,
|
|
@@ -865,10 +951,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
865
951
|
const feature = mkStory('Feature: add endpoint', 10, 10);
|
|
866
952
|
const refactorA = mkStory('Refactor: split parser', 3, 3);
|
|
867
953
|
const refactorB = mkStory('Refactor: normalize naming', 4, 4);
|
|
868
|
-
const selected = selectStoriesForCapacity([feature, refactorA, refactorB], scalingConfig);
|
|
954
|
+
const selected = (await selectStoriesForCapacity([feature, refactorA, refactorB], scalingConfig));
|
|
869
955
|
expect(selected.map(s => s.id)).toEqual([feature.id, refactorA.id, refactorB.id]);
|
|
870
956
|
});
|
|
871
|
-
it('should include no refactor stories when capacity percent is 0 and feature work exists', () => {
|
|
957
|
+
it('should include no refactor stories when capacity percent is 0 and feature work exists', async () => {
|
|
872
958
|
const scalingConfig = mkScalingConfig({
|
|
873
959
|
enabled: true,
|
|
874
960
|
capacity_percent: 0,
|
|
@@ -876,10 +962,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
876
962
|
});
|
|
877
963
|
const feature = mkStory('Feature: add endpoint', 10, 10);
|
|
878
964
|
const refactor = mkStory('Refactor: split parser', 1, 1);
|
|
879
|
-
const selected = selectStoriesForCapacity([feature, refactor], scalingConfig);
|
|
965
|
+
const selected = (await selectStoriesForCapacity([feature, refactor], scalingConfig));
|
|
880
966
|
expect(selected.map(s => s.id)).toEqual([feature.id]);
|
|
881
967
|
});
|
|
882
|
-
it('should allow at least one refactor point when percent is positive but rounded budget is zero', () => {
|
|
968
|
+
it('should allow at least one refactor point when percent is positive but rounded budget is zero', async () => {
|
|
883
969
|
const scalingConfig = mkScalingConfig({
|
|
884
970
|
enabled: true,
|
|
885
971
|
capacity_percent: 10,
|
|
@@ -887,10 +973,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
887
973
|
});
|
|
888
974
|
const feature = mkStory('Feature: tiny patch', 5, 5); // floor(5 * 0.1) = 0 -> min 1
|
|
889
975
|
const refactor = mkStory('Refactor: tighten types', 1, 1);
|
|
890
|
-
const selected = selectStoriesForCapacity([feature, refactor], scalingConfig);
|
|
976
|
+
const selected = (await selectStoriesForCapacity([feature, refactor], scalingConfig));
|
|
891
977
|
expect(selected.map(s => s.id)).toEqual([feature.id, refactor.id]);
|
|
892
978
|
});
|
|
893
|
-
it('should compute budget from total feature story points across multiple stories', () => {
|
|
979
|
+
it('should compute budget from total feature story points across multiple stories', async () => {
|
|
894
980
|
const scalingConfig = mkScalingConfig({
|
|
895
981
|
enabled: true,
|
|
896
982
|
capacity_percent: 20,
|
|
@@ -901,10 +987,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
901
987
|
const refactorA = mkStory('Refactor: A', 1, 1);
|
|
902
988
|
const refactorB = mkStory('Refactor: B', 1, 1);
|
|
903
989
|
const refactorC = mkStory('Refactor: C', 1, 1);
|
|
904
|
-
const selected = selectStoriesForCapacity([featureA, featureB, refactorA, refactorB, refactorC], scalingConfig);
|
|
990
|
+
const selected = (await selectStoriesForCapacity([featureA, featureB, refactorA, refactorB, refactorC], scalingConfig));
|
|
905
991
|
expect(selected.map(s => s.id)).toEqual([featureA.id, featureB.id, refactorA.id, refactorB.id]);
|
|
906
992
|
});
|
|
907
|
-
it('should skip a refactor story that exceeds remaining budget', () => {
|
|
993
|
+
it('should skip a refactor story that exceeds remaining budget', async () => {
|
|
908
994
|
const scalingConfig = mkScalingConfig({
|
|
909
995
|
enabled: true,
|
|
910
996
|
capacity_percent: 20,
|
|
@@ -912,10 +998,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
912
998
|
});
|
|
913
999
|
const feature = mkStory('Feature: A', 10, 10); // budget = 2
|
|
914
1000
|
const refactorLarge = mkStory('Refactor: big cleanup', 3, 3);
|
|
915
|
-
const selected = selectStoriesForCapacity([feature, refactorLarge], scalingConfig);
|
|
1001
|
+
const selected = (await selectStoriesForCapacity([feature, refactorLarge], scalingConfig));
|
|
916
1002
|
expect(selected.map(s => s.id)).toEqual([feature.id]);
|
|
917
1003
|
});
|
|
918
|
-
it('should select a later smaller refactor story if an earlier one exceeds budget', () => {
|
|
1004
|
+
it('should select a later smaller refactor story if an earlier one exceeds budget', async () => {
|
|
919
1005
|
const scalingConfig = mkScalingConfig({
|
|
920
1006
|
enabled: true,
|
|
921
1007
|
capacity_percent: 20,
|
|
@@ -924,10 +1010,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
924
1010
|
const feature = mkStory('Feature: A', 10, 10); // budget = 2
|
|
925
1011
|
const refactorLarge = mkStory('Refactor: big cleanup', 3, 3); // skipped
|
|
926
1012
|
const refactorSmall = mkStory('Refactor: tiny cleanup', 2, 2); // fits
|
|
927
|
-
const selected = selectStoriesForCapacity([feature, refactorLarge, refactorSmall], scalingConfig);
|
|
1013
|
+
const selected = (await selectStoriesForCapacity([feature, refactorLarge, refactorSmall], scalingConfig));
|
|
928
1014
|
expect(selected.map(s => s.id)).toEqual([feature.id, refactorSmall.id]);
|
|
929
1015
|
});
|
|
930
|
-
it('should allow refactor-only queues when configured to allow without feature work', () => {
|
|
1016
|
+
it('should allow refactor-only queues when configured to allow without feature work', async () => {
|
|
931
1017
|
const scalingConfig = mkScalingConfig({
|
|
932
1018
|
enabled: true,
|
|
933
1019
|
capacity_percent: 10,
|
|
@@ -935,10 +1021,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
935
1021
|
});
|
|
936
1022
|
const refactorA = mkStory('Refactor: A', 3, 3);
|
|
937
1023
|
const refactorB = mkStory('Refactor: B', 5, 5);
|
|
938
|
-
const selected = selectStoriesForCapacity([refactorA, refactorB], scalingConfig);
|
|
1024
|
+
const selected = (await selectStoriesForCapacity([refactorA, refactorB], scalingConfig));
|
|
939
1025
|
expect(selected.map(s => s.id)).toEqual([refactorA.id, refactorB.id]);
|
|
940
1026
|
});
|
|
941
|
-
it('should block refactor-only queues when allow_without_feature_work is false', () => {
|
|
1027
|
+
it('should block refactor-only queues when allow_without_feature_work is false', async () => {
|
|
942
1028
|
const scalingConfig = mkScalingConfig({
|
|
943
1029
|
enabled: true,
|
|
944
1030
|
capacity_percent: 10,
|
|
@@ -946,10 +1032,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
946
1032
|
});
|
|
947
1033
|
const refactorA = mkStory('Refactor: A', 3, 3);
|
|
948
1034
|
const refactorB = mkStory('Refactor: B', 5, 5);
|
|
949
|
-
const selected = selectStoriesForCapacity([refactorA, refactorB], scalingConfig);
|
|
1035
|
+
const selected = (await selectStoriesForCapacity([refactorA, refactorB], scalingConfig));
|
|
950
1036
|
expect(selected).toHaveLength(0);
|
|
951
1037
|
});
|
|
952
|
-
it('should preserve order of selected stories', () => {
|
|
1038
|
+
it('should preserve order of selected stories', async () => {
|
|
953
1039
|
const scalingConfig = mkScalingConfig({
|
|
954
1040
|
enabled: true,
|
|
955
1041
|
capacity_percent: 20,
|
|
@@ -959,26 +1045,26 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
959
1045
|
const refactorA = mkStory('Refactor: A', 1, 1);
|
|
960
1046
|
const featureB = mkStory('Feature: B', 5, 5);
|
|
961
1047
|
const refactorB = mkStory('Refactor: B', 1, 1);
|
|
962
|
-
const selected = selectStoriesForCapacity([featureA, refactorA, featureB, refactorB], scalingConfig);
|
|
1048
|
+
const selected = (await selectStoriesForCapacity([featureA, refactorA, featureB, refactorB], scalingConfig));
|
|
963
1049
|
expect(selected.map(s => s.id)).toEqual([featureA.id, refactorA.id, featureB.id, refactorB.id]);
|
|
964
1050
|
});
|
|
965
|
-
it('should default to disabled behavior when refactor config is missing', () => {
|
|
1051
|
+
it('should default to disabled behavior when refactor config is missing', async () => {
|
|
966
1052
|
const scalingConfig = mkScalingConfig();
|
|
967
1053
|
const feature = mkStory('Feature: A', 5, 5);
|
|
968
1054
|
const refactor = mkStory('Refactor: A', 1, 1);
|
|
969
|
-
const selected = selectStoriesForCapacity([feature, refactor], scalingConfig);
|
|
1055
|
+
const selected = (await selectStoriesForCapacity([feature, refactor], scalingConfig));
|
|
970
1056
|
expect(selected.map(s => s.id)).toEqual([feature.id]);
|
|
971
1057
|
});
|
|
972
|
-
it('should return an empty array when no stories are provided', () => {
|
|
1058
|
+
it('should return an empty array when no stories are provided', async () => {
|
|
973
1059
|
const scalingConfig = mkScalingConfig({
|
|
974
1060
|
enabled: true,
|
|
975
1061
|
capacity_percent: 50,
|
|
976
1062
|
allow_without_feature_work: true,
|
|
977
1063
|
});
|
|
978
|
-
const selected = selectStoriesForCapacity([], scalingConfig);
|
|
1064
|
+
const selected = (await selectStoriesForCapacity([], scalingConfig));
|
|
979
1065
|
expect(selected).toEqual([]);
|
|
980
1066
|
});
|
|
981
|
-
it('should include refactor stories when cumulative points exactly match budget', () => {
|
|
1067
|
+
it('should include refactor stories when cumulative points exactly match budget', async () => {
|
|
982
1068
|
const scalingConfig = mkScalingConfig({
|
|
983
1069
|
enabled: true,
|
|
984
1070
|
capacity_percent: 30,
|
|
@@ -987,10 +1073,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
987
1073
|
const feature = mkStory('Feature: A', 10, 10); // budget = 3
|
|
988
1074
|
const refactorA = mkStory('Refactor: A', 1, 1);
|
|
989
1075
|
const refactorB = mkStory('Refactor: B', 2, 2);
|
|
990
|
-
const selected = selectStoriesForCapacity([feature, refactorA, refactorB], scalingConfig);
|
|
1076
|
+
const selected = (await selectStoriesForCapacity([feature, refactorA, refactorB], scalingConfig));
|
|
991
1077
|
expect(selected.map(s => s.id)).toEqual([feature.id, refactorA.id, refactorB.id]);
|
|
992
1078
|
});
|
|
993
|
-
it('should continue selecting later refactors after partially consuming budget and skipping a too-large one', () => {
|
|
1079
|
+
it('should continue selecting later refactors after partially consuming budget and skipping a too-large one', async () => {
|
|
994
1080
|
const scalingConfig = mkScalingConfig({
|
|
995
1081
|
enabled: true,
|
|
996
1082
|
capacity_percent: 50,
|
|
@@ -1000,10 +1086,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
1000
1086
|
const refactorA = mkStory('Refactor: A', 2, 2); // used = 2
|
|
1001
1087
|
const refactorLarge = mkStory('Refactor: Large', 4, 4); // skipped (2 + 4 > 5)
|
|
1002
1088
|
const refactorB = mkStory('Refactor: B', 3, 3); // used = 5
|
|
1003
|
-
const selected = selectStoriesForCapacity([feature, refactorA, refactorLarge, refactorB], scalingConfig);
|
|
1089
|
+
const selected = (await selectStoriesForCapacity([feature, refactorA, refactorLarge, refactorB], scalingConfig));
|
|
1004
1090
|
expect(selected.map(s => s.id)).toEqual([feature.id, refactorA.id, refactorB.id]);
|
|
1005
1091
|
});
|
|
1006
|
-
it('should derive feature budget from complexity when story_points are not set', () => {
|
|
1092
|
+
it('should derive feature budget from complexity when story_points are not set', async () => {
|
|
1007
1093
|
const scalingConfig = mkScalingConfig({
|
|
1008
1094
|
enabled: true,
|
|
1009
1095
|
capacity_percent: 20,
|
|
@@ -1013,10 +1099,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
1013
1099
|
const featureB = mkStory('Feature: B', null, 4); // feature total = 10, budget = 2
|
|
1014
1100
|
const refactorA = mkStory('Refactor: A', 1, 1);
|
|
1015
1101
|
const refactorB = mkStory('Refactor: B', 2, 2);
|
|
1016
|
-
const selected = selectStoriesForCapacity([featureA, featureB, refactorA, refactorB], scalingConfig);
|
|
1102
|
+
const selected = (await selectStoriesForCapacity([featureA, featureB, refactorA, refactorB], scalingConfig));
|
|
1017
1103
|
expect(selected.map(s => s.id)).toEqual([featureA.id, featureB.id, refactorA.id]);
|
|
1018
1104
|
});
|
|
1019
|
-
it('should allow one point of refactor work when feature stories have no explicit points', () => {
|
|
1105
|
+
it('should allow one point of refactor work when feature stories have no explicit points', async () => {
|
|
1020
1106
|
const scalingConfig = mkScalingConfig({
|
|
1021
1107
|
enabled: true,
|
|
1022
1108
|
capacity_percent: 10,
|
|
@@ -1025,10 +1111,10 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
1025
1111
|
const feature = mkStory('Feature: A', null, null); // defaults to 1, floor(1 * 0.1)=0 -> min 1
|
|
1026
1112
|
const refactorA = mkStory('Refactor: A', 1, 1);
|
|
1027
1113
|
const refactorB = mkStory('Refactor: B', 1, 1);
|
|
1028
|
-
const selected = selectStoriesForCapacity([feature, refactorA, refactorB], scalingConfig);
|
|
1114
|
+
const selected = (await selectStoriesForCapacity([feature, refactorA, refactorB], scalingConfig));
|
|
1029
1115
|
expect(selected.map(s => s.id)).toEqual([feature.id, refactorA.id]);
|
|
1030
1116
|
});
|
|
1031
|
-
it('should ignore capacity_percent for refactor-only queues when allow_without_feature_work is true', () => {
|
|
1117
|
+
it('should ignore capacity_percent for refactor-only queues when allow_without_feature_work is true', async () => {
|
|
1032
1118
|
const scalingConfig = mkScalingConfig({
|
|
1033
1119
|
enabled: true,
|
|
1034
1120
|
capacity_percent: 0,
|
|
@@ -1036,28 +1122,40 @@ describe('Scheduler Refactor Policy Test Matrix', () => {
|
|
|
1036
1122
|
});
|
|
1037
1123
|
const refactorA = mkStory('Refactor: A', 2, 2);
|
|
1038
1124
|
const refactorB = mkStory('Refactor: B', 4, 4);
|
|
1039
|
-
const selected = selectStoriesForCapacity([refactorA, refactorB], scalingConfig);
|
|
1125
|
+
const selected = (await selectStoriesForCapacity([refactorA, refactorB], scalingConfig));
|
|
1040
1126
|
expect(selected.map(s => s.id)).toEqual([refactorA.id, refactorB.id]);
|
|
1041
1127
|
});
|
|
1042
1128
|
});
|
|
1043
1129
|
describe('Scheduler Agent Selection', () => {
|
|
1044
|
-
it('should select agent with least workload from multiple agents', () => {
|
|
1045
|
-
const team = createTeam(db, {
|
|
1130
|
+
it('should select agent with least workload from multiple agents', async () => {
|
|
1131
|
+
const team = await createTeam(db, {
|
|
1046
1132
|
name: 'Test Team',
|
|
1047
1133
|
repoUrl: 'https://github.com/test/repo',
|
|
1048
1134
|
repoPath: 'test',
|
|
1049
1135
|
});
|
|
1050
1136
|
// Create three junior agents with different workloads
|
|
1051
|
-
db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('junior-1', 'junior', '${team.id}', 'idle')`);
|
|
1052
|
-
db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('junior-2', 'junior', '${team.id}', 'idle')`);
|
|
1053
|
-
db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('junior-3', 'junior', '${team.id}', 'idle')`);
|
|
1137
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('junior-1', 'junior', '${team.id}', 'idle')`);
|
|
1138
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('junior-2', 'junior', '${team.id}', 'idle')`);
|
|
1139
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('junior-3', 'junior', '${team.id}', 'idle')`);
|
|
1054
1140
|
// Give junior-1 two stories, junior-2 one story, junior-3 zero stories
|
|
1055
|
-
const story1 = createStory(db, {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1141
|
+
const story1 = await createStory(db, {
|
|
1142
|
+
teamId: team.id,
|
|
1143
|
+
title: 'Story 1',
|
|
1144
|
+
description: 'Test',
|
|
1145
|
+
});
|
|
1146
|
+
const story2 = await createStory(db, {
|
|
1147
|
+
teamId: team.id,
|
|
1148
|
+
title: 'Story 2',
|
|
1149
|
+
description: 'Test',
|
|
1150
|
+
});
|
|
1151
|
+
const story3 = await createStory(db, {
|
|
1152
|
+
teamId: team.id,
|
|
1153
|
+
title: 'Story 3',
|
|
1154
|
+
description: 'Test',
|
|
1155
|
+
});
|
|
1156
|
+
await updateStory(db, story1.id, { assignedAgentId: 'junior-1', status: 'in_progress' });
|
|
1157
|
+
await updateStory(db, story2.id, { assignedAgentId: 'junior-1', status: 'in_progress' });
|
|
1158
|
+
await updateStory(db, story3.id, { assignedAgentId: 'junior-2', status: 'in_progress' });
|
|
1061
1159
|
const agents = [
|
|
1062
1160
|
{
|
|
1063
1161
|
id: 'junior-1',
|
|
@@ -1105,12 +1203,12 @@ describe('Scheduler Agent Selection', () => {
|
|
|
1105
1203
|
updated_at: '',
|
|
1106
1204
|
},
|
|
1107
1205
|
];
|
|
1108
|
-
const selected = selectAgentWithLeastWorkload(db, agents);
|
|
1206
|
+
const selected = await selectAgentWithLeastWorkload(db, agents);
|
|
1109
1207
|
// Should select junior-3 who has zero stories
|
|
1110
1208
|
expect(selected.id).toBe('junior-3');
|
|
1111
1209
|
});
|
|
1112
|
-
it('should select first agent when all have equal workload', () => {
|
|
1113
|
-
const team = createTeam(db, {
|
|
1210
|
+
it('should select first agent when all have equal workload', async () => {
|
|
1211
|
+
const team = await createTeam(db, {
|
|
1114
1212
|
name: 'Test Team',
|
|
1115
1213
|
repoUrl: 'https://github.com/test/repo',
|
|
1116
1214
|
repoPath: 'test',
|
|
@@ -1147,67 +1245,75 @@ describe('Scheduler Agent Selection', () => {
|
|
|
1147
1245
|
updated_at: '',
|
|
1148
1246
|
},
|
|
1149
1247
|
];
|
|
1150
|
-
const selected = selectAgentWithLeastWorkload(db, agents);
|
|
1248
|
+
const selected = await selectAgentWithLeastWorkload(db, agents);
|
|
1151
1249
|
expect(selected.id).toBe('agent-1');
|
|
1152
1250
|
});
|
|
1153
|
-
it('should calculate agent workload correctly', () => {
|
|
1154
|
-
const team = createTeam(db, {
|
|
1251
|
+
it('should calculate agent workload correctly', async () => {
|
|
1252
|
+
const team = await createTeam(db, {
|
|
1155
1253
|
name: 'Test Team',
|
|
1156
1254
|
repoUrl: 'https://github.com/test/repo',
|
|
1157
1255
|
repoPath: 'test',
|
|
1158
1256
|
});
|
|
1159
|
-
db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('agent-1', 'junior', '${team.id}', 'idle')`);
|
|
1160
|
-
const story1 = createStory(db, {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1257
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('agent-1', 'junior', '${team.id}', 'idle')`);
|
|
1258
|
+
const story1 = await createStory(db, {
|
|
1259
|
+
teamId: team.id,
|
|
1260
|
+
title: 'Story 1',
|
|
1261
|
+
description: 'Test',
|
|
1262
|
+
});
|
|
1263
|
+
const story2 = await createStory(db, {
|
|
1264
|
+
teamId: team.id,
|
|
1265
|
+
title: 'Story 2',
|
|
1266
|
+
description: 'Test',
|
|
1267
|
+
});
|
|
1268
|
+
await updateStory(db, story1.id, { assignedAgentId: 'agent-1', status: 'in_progress' });
|
|
1269
|
+
await updateStory(db, story2.id, { assignedAgentId: 'agent-1', status: 'in_progress' });
|
|
1270
|
+
const workload = await getAgentWorkload(db, 'agent-1');
|
|
1165
1271
|
expect(workload).toBe(2);
|
|
1166
1272
|
});
|
|
1167
|
-
it('should return zero workload for agent with no stories', () => {
|
|
1168
|
-
const team = createTeam(db, {
|
|
1273
|
+
it('should return zero workload for agent with no stories', async () => {
|
|
1274
|
+
const team = await createTeam(db, {
|
|
1169
1275
|
name: 'Test Team',
|
|
1170
1276
|
repoUrl: 'https://github.com/test/repo',
|
|
1171
1277
|
repoPath: 'test',
|
|
1172
1278
|
});
|
|
1173
|
-
db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('agent-1', 'junior', '${team.id}', 'idle')`);
|
|
1174
|
-
const workload = getAgentWorkload(db, 'agent-1');
|
|
1279
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('agent-1', 'junior', '${team.id}', 'idle')`);
|
|
1280
|
+
const workload = await getAgentWorkload(db, 'agent-1');
|
|
1175
1281
|
expect(workload).toBe(0);
|
|
1176
1282
|
});
|
|
1177
1283
|
});
|
|
1178
1284
|
describe('Scheduler Complexity Routing', () => {
|
|
1179
|
-
it('should route low complexity stories to junior agents', () => {
|
|
1285
|
+
it('should route low complexity stories to junior agents', async () => {
|
|
1180
1286
|
// Test the routing logic: complexity <= junior_max_complexity goes to junior
|
|
1181
1287
|
const complexity = 2;
|
|
1182
1288
|
expect(complexity).toBeLessThanOrEqual(mockConfig.scaling.junior_max_complexity);
|
|
1183
1289
|
});
|
|
1184
|
-
it('should route medium complexity stories to intermediate agents', () => {
|
|
1290
|
+
it('should route medium complexity stories to intermediate agents', async () => {
|
|
1185
1291
|
// Test the routing logic: complexity between junior and intermediate thresholds
|
|
1186
1292
|
const complexity = 4;
|
|
1187
1293
|
expect(complexity).toBeGreaterThan(mockConfig.scaling.junior_max_complexity);
|
|
1188
1294
|
expect(complexity).toBeLessThanOrEqual(mockConfig.scaling.intermediate_max_complexity);
|
|
1189
1295
|
});
|
|
1190
|
-
it('should route high complexity stories to senior agents', () => {
|
|
1296
|
+
it('should route high complexity stories to senior agents', async () => {
|
|
1191
1297
|
// Test the routing logic: complexity > intermediate_max_complexity goes to senior
|
|
1192
1298
|
const complexity = 8;
|
|
1193
1299
|
expect(complexity).toBeGreaterThan(mockConfig.scaling.intermediate_max_complexity);
|
|
1194
1300
|
});
|
|
1195
|
-
it('should handle edge case at junior boundary', () => {
|
|
1301
|
+
it('should handle edge case at junior boundary', async () => {
|
|
1196
1302
|
// Complexity exactly at junior_max_complexity should still go to junior
|
|
1197
1303
|
const complexity = 3;
|
|
1198
1304
|
expect(complexity).toBeLessThanOrEqual(mockConfig.scaling.junior_max_complexity);
|
|
1199
1305
|
});
|
|
1200
|
-
it('should handle edge case at intermediate boundary', () => {
|
|
1306
|
+
it('should handle edge case at intermediate boundary', async () => {
|
|
1201
1307
|
// Complexity exactly at intermediate_max_complexity should still go to intermediate
|
|
1202
1308
|
const complexity = 5;
|
|
1203
1309
|
expect(complexity).toBeLessThanOrEqual(mockConfig.scaling.intermediate_max_complexity);
|
|
1204
1310
|
});
|
|
1205
|
-
it('should use config values for routing thresholds', () => {
|
|
1311
|
+
it('should use config values for routing thresholds', async () => {
|
|
1206
1312
|
// Verify config values are set correctly for routing logic
|
|
1207
1313
|
expect(mockConfig.scaling.junior_max_complexity).toBe(3);
|
|
1208
1314
|
expect(mockConfig.scaling.intermediate_max_complexity).toBe(5);
|
|
1209
1315
|
});
|
|
1210
|
-
it('should default to complexity 5 when not specified', () => {
|
|
1316
|
+
it('should default to complexity 5 when not specified', async () => {
|
|
1211
1317
|
// Test default complexity value used in assignStories
|
|
1212
1318
|
const complexity = null;
|
|
1213
1319
|
const defaultComplexity = complexity || 5;
|
|
@@ -1215,187 +1321,203 @@ describe('Scheduler Complexity Routing', () => {
|
|
|
1215
1321
|
});
|
|
1216
1322
|
});
|
|
1217
1323
|
describe('Scheduler Story Assignment Prevention', () => {
|
|
1218
|
-
it('should prevent duplicate story assignments', () => {
|
|
1219
|
-
const team = createTeam(db, {
|
|
1324
|
+
it('should prevent duplicate story assignments', async () => {
|
|
1325
|
+
const team = await createTeam(db, {
|
|
1220
1326
|
name: 'Test Team',
|
|
1221
1327
|
repoUrl: 'https://github.com/test/repo',
|
|
1222
1328
|
repoPath: 'test',
|
|
1223
1329
|
});
|
|
1224
|
-
db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('agent-1', 'junior', '${team.id}', 'idle')`);
|
|
1330
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status) VALUES ('agent-1', 'junior', '${team.id}', 'idle')`);
|
|
1225
1331
|
// Create a story and assign it
|
|
1226
|
-
const story = createStory(db, {
|
|
1332
|
+
const story = await createStory(db, {
|
|
1227
1333
|
teamId: team.id,
|
|
1228
1334
|
title: 'Story',
|
|
1229
1335
|
description: 'Test',
|
|
1230
1336
|
});
|
|
1231
|
-
updateStory(db, story.id, { complexityScore: 2, status: 'planned' });
|
|
1337
|
+
await updateStory(db, story.id, { complexityScore: 2, status: 'planned' });
|
|
1232
1338
|
// First assignment
|
|
1233
|
-
updateStory(db, story.id, { assignedAgentId: 'agent-1', status: 'in_progress' });
|
|
1339
|
+
await updateStory(db, story.id, { assignedAgentId: 'agent-1', status: 'in_progress' });
|
|
1234
1340
|
// Verify the story is now assigned
|
|
1235
|
-
const result = db.exec(`SELECT assigned_agent_id FROM stories WHERE id = '${story.id}'`);
|
|
1341
|
+
const result = db.db.exec(`SELECT assigned_agent_id FROM stories WHERE id = '${story.id}'`);
|
|
1236
1342
|
expect(result[0].values[0][0]).toBe('agent-1');
|
|
1237
1343
|
});
|
|
1238
|
-
it('should verify story assignment changes status', () => {
|
|
1239
|
-
const team = createTeam(db, {
|
|
1344
|
+
it('should verify story assignment changes status', async () => {
|
|
1345
|
+
const team = await createTeam(db, {
|
|
1240
1346
|
name: 'Test Team',
|
|
1241
1347
|
repoUrl: 'https://github.com/test/repo',
|
|
1242
1348
|
repoPath: 'test',
|
|
1243
1349
|
});
|
|
1244
|
-
const story = createStory(db, {
|
|
1350
|
+
const story = await createStory(db, {
|
|
1245
1351
|
teamId: team.id,
|
|
1246
1352
|
title: 'Story',
|
|
1247
1353
|
description: 'Test',
|
|
1248
1354
|
});
|
|
1249
|
-
updateStory(db, story.id, { status: 'planned' });
|
|
1355
|
+
await updateStory(db, story.id, { status: 'planned' });
|
|
1250
1356
|
// Change status to in_progress
|
|
1251
|
-
updateStory(db, story.id, { status: 'in_progress' });
|
|
1357
|
+
await updateStory(db, story.id, { status: 'in_progress' });
|
|
1252
1358
|
// Verify status changed
|
|
1253
|
-
const result = db.exec(`SELECT status FROM stories WHERE id = '${story.id}'`);
|
|
1359
|
+
const result = db.db.exec(`SELECT status FROM stories WHERE id = '${story.id}'`);
|
|
1254
1360
|
expect(result[0].values[0][0]).toBe('in_progress');
|
|
1255
1361
|
});
|
|
1256
|
-
it('should skip stories with unsatisfied dependencies', () => {
|
|
1257
|
-
const team = createTeam(db, {
|
|
1362
|
+
it('should skip stories with unsatisfied dependencies', async () => {
|
|
1363
|
+
const team = await createTeam(db, {
|
|
1258
1364
|
name: 'Test Team',
|
|
1259
1365
|
repoUrl: 'https://github.com/test/repo',
|
|
1260
1366
|
repoPath: 'test',
|
|
1261
1367
|
});
|
|
1262
|
-
const storyA = createStory(db, {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1368
|
+
const storyA = await createStory(db, {
|
|
1369
|
+
teamId: team.id,
|
|
1370
|
+
title: 'Story A',
|
|
1371
|
+
description: 'Test',
|
|
1372
|
+
});
|
|
1373
|
+
await updateStory(db, storyA.id, { status: 'planned' });
|
|
1374
|
+
const storyB = await createStory(db, {
|
|
1375
|
+
teamId: team.id,
|
|
1376
|
+
title: 'Story B',
|
|
1377
|
+
description: 'Test',
|
|
1378
|
+
});
|
|
1379
|
+
await updateStory(db, storyB.id, { status: 'planned' });
|
|
1266
1380
|
// B depends on A, but A is still planned
|
|
1267
|
-
addStoryDependency(db, storyB.id, storyA.id);
|
|
1381
|
+
await addStoryDependency(db, storyB.id, storyA.id);
|
|
1268
1382
|
// B should not be ready for assignment because A is not merged yet
|
|
1269
|
-
const satisfied = areDependenciesSatisfied(db, storyB.id);
|
|
1383
|
+
const satisfied = await areDependenciesSatisfied(db, storyB.id);
|
|
1270
1384
|
expect(satisfied).toBe(false);
|
|
1271
1385
|
});
|
|
1272
|
-
it('should allow stories when dependencies are in terminal states', () => {
|
|
1273
|
-
const team = createTeam(db, {
|
|
1386
|
+
it('should allow stories when dependencies are in terminal states', async () => {
|
|
1387
|
+
const team = await createTeam(db, {
|
|
1274
1388
|
name: 'Test Team',
|
|
1275
1389
|
repoUrl: 'https://github.com/test/repo',
|
|
1276
1390
|
repoPath: 'test',
|
|
1277
1391
|
});
|
|
1278
1392
|
// Test with merged status (terminal state)
|
|
1279
|
-
const storyA = createStory(db, {
|
|
1280
|
-
|
|
1393
|
+
const storyA = await createStory(db, {
|
|
1394
|
+
teamId: team.id,
|
|
1395
|
+
title: 'Story A',
|
|
1396
|
+
description: 'Test',
|
|
1397
|
+
});
|
|
1398
|
+
const storyB = await createStory(db, {
|
|
1399
|
+
teamId: team.id,
|
|
1400
|
+
title: 'Story B',
|
|
1401
|
+
description: 'Test',
|
|
1402
|
+
});
|
|
1281
1403
|
// Update A to merged status
|
|
1282
|
-
updateStory(db, storyA.id, { status: 'merged' });
|
|
1404
|
+
await updateStory(db, storyA.id, { status: 'merged' });
|
|
1283
1405
|
// B depends on A, and A is merged
|
|
1284
|
-
addStoryDependency(db, storyB.id, storyA.id);
|
|
1406
|
+
await addStoryDependency(db, storyB.id, storyA.id);
|
|
1285
1407
|
// B should be ready for assignment
|
|
1286
|
-
const satisfied = areDependenciesSatisfied(db, storyB.id);
|
|
1408
|
+
const satisfied = await areDependenciesSatisfied(db, storyB.id);
|
|
1287
1409
|
expect(satisfied).toBe(true);
|
|
1288
1410
|
});
|
|
1289
|
-
it('should map claude model IDs to claude runtime shorthands', () => {
|
|
1411
|
+
it('should map claude model IDs to claude runtime shorthands', async () => {
|
|
1290
1412
|
const runtimeModel = scheduler.getRuntimeModel('claude-sonnet-4-5-20250929', 'claude');
|
|
1291
1413
|
expect(runtimeModel).toBe('sonnet');
|
|
1292
1414
|
});
|
|
1293
|
-
it('should remap unsupported codex mini model and preserve gemini runtime model', () => {
|
|
1415
|
+
it('should remap unsupported codex mini model and preserve gemini runtime model', async () => {
|
|
1294
1416
|
const codexModel = scheduler.getRuntimeModel('gpt-4o-mini', 'codex');
|
|
1295
1417
|
const geminiModel = scheduler.getRuntimeModel('gemini-2.5-pro', 'gemini');
|
|
1296
1418
|
expect(codexModel).toBe('gpt-5.2-codex');
|
|
1297
1419
|
expect(geminiModel).toBe('gemini-2.5-pro');
|
|
1298
1420
|
});
|
|
1299
|
-
it('should not fallback unknown claude models to haiku', () => {
|
|
1421
|
+
it('should not fallback unknown claude models to haiku', async () => {
|
|
1300
1422
|
const runtimeModel = scheduler.getRuntimeModel('claude-custom-model', 'claude');
|
|
1301
1423
|
expect(runtimeModel).toBe('claude-custom-model');
|
|
1302
1424
|
});
|
|
1303
|
-
it('should detect godmode is active when an active requirement has godmode enabled', () => {
|
|
1425
|
+
it('should detect godmode is active when an active requirement has godmode enabled', async () => {
|
|
1304
1426
|
// Create a requirement with godmode and set it to planning status
|
|
1305
|
-
const req = createRequirement(db, {
|
|
1427
|
+
const req = await createRequirement(db, {
|
|
1306
1428
|
title: 'Godmode Requirement',
|
|
1307
1429
|
description: 'Test requirement with godmode',
|
|
1308
1430
|
godmode: true,
|
|
1309
1431
|
});
|
|
1310
1432
|
db.run(`UPDATE requirements SET status = 'planning' WHERE id = ?`, [req.id]);
|
|
1311
1433
|
// Godmode should be detected as active
|
|
1312
|
-
const isGodmodeActive = scheduler.isGodmodeActive();
|
|
1434
|
+
const isGodmodeActive = await scheduler.isGodmodeActive();
|
|
1313
1435
|
expect(isGodmodeActive).toBe(true);
|
|
1314
1436
|
});
|
|
1315
|
-
it('should detect godmode even when all stories have moved to in_progress', () => {
|
|
1316
|
-
const team = createTeam(db, {
|
|
1437
|
+
it('should detect godmode even when all stories have moved to in_progress', async () => {
|
|
1438
|
+
const team = await createTeam(db, {
|
|
1317
1439
|
name: 'Test Team',
|
|
1318
1440
|
repoUrl: 'https://github.com/test/repo',
|
|
1319
1441
|
repoPath: 'test',
|
|
1320
1442
|
});
|
|
1321
1443
|
// Create a godmode requirement in in_progress status
|
|
1322
|
-
const req = createRequirement(db, {
|
|
1444
|
+
const req = await createRequirement(db, {
|
|
1323
1445
|
title: 'Godmode Requirement',
|
|
1324
1446
|
description: 'Test requirement with godmode',
|
|
1325
1447
|
godmode: true,
|
|
1326
1448
|
});
|
|
1327
1449
|
db.run(`UPDATE requirements SET status = 'in_progress' WHERE id = ?`, [req.id]);
|
|
1328
1450
|
// Create a story that has moved to in_progress (no longer planned)
|
|
1329
|
-
const story = createStory(db, {
|
|
1451
|
+
const story = await createStory(db, {
|
|
1330
1452
|
requirementId: req.id,
|
|
1331
1453
|
teamId: team.id,
|
|
1332
1454
|
title: 'Godmode Story',
|
|
1333
1455
|
description: 'Test',
|
|
1334
1456
|
});
|
|
1335
|
-
updateStory(db, story.id, { status: 'in_progress' });
|
|
1457
|
+
await updateStory(db, story.id, { status: 'in_progress' });
|
|
1336
1458
|
// Godmode should still be active even though no stories are planned
|
|
1337
|
-
const isGodmodeActive = scheduler.isGodmodeActive();
|
|
1459
|
+
const isGodmodeActive = await scheduler.isGodmodeActive();
|
|
1338
1460
|
expect(isGodmodeActive).toBe(true);
|
|
1339
1461
|
});
|
|
1340
|
-
it('should not detect godmode when no requirements have godmode enabled', () => {
|
|
1462
|
+
it('should not detect godmode when no requirements have godmode enabled', async () => {
|
|
1341
1463
|
// Create a normal requirement (without godmode) in planning status
|
|
1342
|
-
const req = createRequirement(db, {
|
|
1464
|
+
const req = await createRequirement(db, {
|
|
1343
1465
|
title: 'Normal Requirement',
|
|
1344
1466
|
description: 'Test requirement without godmode',
|
|
1345
1467
|
godmode: false,
|
|
1346
1468
|
});
|
|
1347
1469
|
db.run(`UPDATE requirements SET status = 'planning' WHERE id = ?`, [req.id]);
|
|
1348
1470
|
// Godmode should not be detected as active
|
|
1349
|
-
const isGodmodeActive = scheduler.isGodmodeActive();
|
|
1471
|
+
const isGodmodeActive = await scheduler.isGodmodeActive();
|
|
1350
1472
|
expect(isGodmodeActive).toBe(false);
|
|
1351
1473
|
});
|
|
1352
|
-
it('should not detect godmode when godmode requirement is completed', () => {
|
|
1474
|
+
it('should not detect godmode when godmode requirement is completed', async () => {
|
|
1353
1475
|
// Create a godmode requirement that is already completed
|
|
1354
|
-
const req = createRequirement(db, {
|
|
1476
|
+
const req = await createRequirement(db, {
|
|
1355
1477
|
title: 'Godmode Requirement',
|
|
1356
1478
|
description: 'Test requirement with godmode',
|
|
1357
1479
|
godmode: true,
|
|
1358
1480
|
});
|
|
1359
1481
|
db.run(`UPDATE requirements SET status = 'completed' WHERE id = ?`, [req.id]);
|
|
1360
1482
|
// Godmode should not be active for completed requirements
|
|
1361
|
-
const isGodmodeActive = scheduler.isGodmodeActive();
|
|
1483
|
+
const isGodmodeActive = await scheduler.isGodmodeActive();
|
|
1362
1484
|
expect(isGodmodeActive).toBe(false);
|
|
1363
1485
|
});
|
|
1364
|
-
it('should not detect godmode when no requirements exist', () => {
|
|
1486
|
+
it('should not detect godmode when no requirements exist', async () => {
|
|
1365
1487
|
// No requirements created, so godmode cannot be active
|
|
1366
|
-
const isGodmodeActive = scheduler.isGodmodeActive();
|
|
1488
|
+
const isGodmodeActive = await scheduler.isGodmodeActive();
|
|
1367
1489
|
expect(isGodmodeActive).toBe(false);
|
|
1368
1490
|
});
|
|
1369
1491
|
});
|
|
1370
1492
|
describe('Scheduler Agent Reassignment for Working Agents with NULL currentStoryId', () => {
|
|
1371
|
-
it('should consider working agents with null current_story_id as available for assignment', () => {
|
|
1372
|
-
const team = createTeam(db, {
|
|
1493
|
+
it('should consider working agents with null current_story_id as available for assignment', async () => {
|
|
1494
|
+
const team = await createTeam(db, {
|
|
1373
1495
|
name: 'Test Team',
|
|
1374
1496
|
repoUrl: 'https://github.com/test/repo',
|
|
1375
1497
|
repoPath: 'test',
|
|
1376
1498
|
});
|
|
1377
1499
|
// Create a working agent with no current story (effectively idle)
|
|
1378
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1500
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1379
1501
|
VALUES (?, ?, ?, ?, NULL, datetime('now'), datetime('now'))`, ['senior-orphan-1', 'senior', team.id, 'working']);
|
|
1380
1502
|
// Query agents using the same filter logic from assignStories
|
|
1381
|
-
const result = db.exec(`SELECT id, type, status, current_story_id FROM agents
|
|
1503
|
+
const result = db.db.exec(`SELECT id, type, status, current_story_id FROM agents
|
|
1382
1504
|
WHERE team_id = '${team.id}' AND type != 'qa'
|
|
1383
1505
|
AND (status = 'idle' OR (status = 'working' AND current_story_id IS NULL))`);
|
|
1384
1506
|
expect(result[0].values).toHaveLength(1);
|
|
1385
1507
|
expect(result[0].values[0][0]).toBe('senior-orphan-1');
|
|
1386
1508
|
});
|
|
1387
|
-
it('should not consider working agents with a current story as available', () => {
|
|
1388
|
-
const team = createTeam(db, {
|
|
1509
|
+
it('should not consider working agents with a current story as available', async () => {
|
|
1510
|
+
const team = await createTeam(db, {
|
|
1389
1511
|
name: 'Test Team',
|
|
1390
1512
|
repoUrl: 'https://github.com/test/repo',
|
|
1391
1513
|
repoPath: 'test',
|
|
1392
1514
|
});
|
|
1393
|
-
const story = createStory(db, { teamId: team.id, title: 'Active', description: 'Test' });
|
|
1515
|
+
const story = await createStory(db, { teamId: team.id, title: 'Active', description: 'Test' });
|
|
1394
1516
|
// Create a working agent with a current story
|
|
1395
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1517
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1396
1518
|
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))`, ['senior-busy-1', 'senior', team.id, 'working', story.id]);
|
|
1397
1519
|
// Query agents using the same filter logic from assignStories
|
|
1398
|
-
const result = db.exec(`SELECT id, type, status, current_story_id FROM agents
|
|
1520
|
+
const result = db.db.exec(`SELECT id, type, status, current_story_id FROM agents
|
|
1399
1521
|
WHERE team_id = '${team.id}' AND type != 'qa'
|
|
1400
1522
|
AND (status = 'idle' OR (status = 'working' AND current_story_id IS NULL))`);
|
|
1401
1523
|
// Should not include the busy agent
|
|
@@ -1426,18 +1548,18 @@ describe('Scheduler checkMergeQueue', () => {
|
|
|
1426
1548
|
};
|
|
1427
1549
|
it('should spawn QA when a queued PR exists for an in_progress story', async () => {
|
|
1428
1550
|
ensurePullRequestsTable();
|
|
1429
|
-
const team = createTeam(db, {
|
|
1551
|
+
const team = await createTeam(db, {
|
|
1430
1552
|
name: 'Test Team',
|
|
1431
1553
|
repoUrl: 'https://github.com/test/repo',
|
|
1432
1554
|
repoPath: 'test',
|
|
1433
1555
|
});
|
|
1434
|
-
const story = createStory(db, {
|
|
1556
|
+
const story = await createStory(db, {
|
|
1435
1557
|
teamId: team.id,
|
|
1436
1558
|
title: 'Queued PR Story',
|
|
1437
1559
|
description: 'Story has queued PR but stale in_progress status',
|
|
1438
1560
|
});
|
|
1439
|
-
updateStory(db, story.id, { status: 'in_progress' });
|
|
1440
|
-
createPullRequest(db, {
|
|
1561
|
+
await updateStory(db, story.id, { status: 'in_progress' });
|
|
1562
|
+
await createPullRequest(db, {
|
|
1441
1563
|
storyId: story.id,
|
|
1442
1564
|
teamId: team.id,
|
|
1443
1565
|
branchName: 'feature/queued-pr-story',
|
|
@@ -1457,18 +1579,18 @@ describe('Scheduler checkMergeQueue', () => {
|
|
|
1457
1579
|
});
|
|
1458
1580
|
it('should not spawn QA for merged stories even if PR row is still queued', async () => {
|
|
1459
1581
|
ensurePullRequestsTable();
|
|
1460
|
-
const team = createTeam(db, {
|
|
1582
|
+
const team = await createTeam(db, {
|
|
1461
1583
|
name: 'Test Team',
|
|
1462
1584
|
repoUrl: 'https://github.com/test/repo',
|
|
1463
1585
|
repoPath: 'test',
|
|
1464
1586
|
});
|
|
1465
|
-
const story = createStory(db, {
|
|
1587
|
+
const story = await createStory(db, {
|
|
1466
1588
|
teamId: team.id,
|
|
1467
1589
|
title: 'Merged Story',
|
|
1468
1590
|
description: 'Merged stories should not drive QA scaling',
|
|
1469
1591
|
});
|
|
1470
|
-
updateStory(db, story.id, { status: 'merged' });
|
|
1471
|
-
createPullRequest(db, {
|
|
1592
|
+
await updateStory(db, story.id, { status: 'merged' });
|
|
1593
|
+
await createPullRequest(db, {
|
|
1472
1594
|
storyId: story.id,
|
|
1473
1595
|
teamId: team.id,
|
|
1474
1596
|
branchName: 'feature/merged-story',
|
|
@@ -1488,19 +1610,19 @@ describe('Scheduler checkMergeQueue', () => {
|
|
|
1488
1610
|
});
|
|
1489
1611
|
describe('Scheduler checkScaling', () => {
|
|
1490
1612
|
it('should spawn a new indexed senior when the base senior is busy', async () => {
|
|
1491
|
-
const team = createTeam(db, {
|
|
1613
|
+
const team = await createTeam(db, {
|
|
1492
1614
|
name: 'Busy Senior Team',
|
|
1493
1615
|
repoUrl: 'https://github.com/test/repo',
|
|
1494
1616
|
repoPath: 'test',
|
|
1495
1617
|
});
|
|
1496
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1618
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1497
1619
|
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))`, ['senior-busy-1', 'senior', team.id, 'working', 'STORY-OLD']);
|
|
1498
|
-
const story = createStory(db, {
|
|
1620
|
+
const story = await createStory(db, {
|
|
1499
1621
|
teamId: team.id,
|
|
1500
1622
|
title: 'Needs Senior',
|
|
1501
1623
|
description: 'High complexity story',
|
|
1502
1624
|
});
|
|
1503
|
-
updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1625
|
+
await updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1504
1626
|
const spawnSeniorSpy = vi
|
|
1505
1627
|
.spyOn(scheduler, 'spawnSenior')
|
|
1506
1628
|
.mockImplementation(async (...args) => {
|
|
@@ -1519,16 +1641,16 @@ describe('Scheduler checkScaling', () => {
|
|
|
1519
1641
|
const result = await scheduler.assignStories();
|
|
1520
1642
|
expect(result.assigned).toBe(1);
|
|
1521
1643
|
expect(spawnSeniorSpy).toHaveBeenCalledTimes(1);
|
|
1522
|
-
const updatedStory = getStoryById(db, story.id);
|
|
1644
|
+
const updatedStory = (await getStoryById(db, story.id));
|
|
1523
1645
|
expect(updatedStory.status).toBe('in_progress');
|
|
1524
1646
|
expect(updatedStory.assigned_agent_id).toBe('senior-spawned-2');
|
|
1525
|
-
const busySeniorRow = db.exec(`SELECT status, current_story_id FROM agents WHERE id = 'senior-busy-1'`)[0]?.values[0];
|
|
1647
|
+
const busySeniorRow = db.db.exec(`SELECT status, current_story_id FROM agents WHERE id = 'senior-busy-1'`)[0]?.values[0];
|
|
1526
1648
|
expect(busySeniorRow?.[0]).toBe('working');
|
|
1527
1649
|
expect(busySeniorRow?.[1]).toBe('STORY-OLD');
|
|
1528
1650
|
spawnSeniorSpy.mockRestore();
|
|
1529
1651
|
});
|
|
1530
1652
|
it('should choose next senior index from max active index when index 1 is absent', async () => {
|
|
1531
|
-
const team = createTeam(db, {
|
|
1653
|
+
const team = await createTeam(db, {
|
|
1532
1654
|
name: 'Gap Index Team',
|
|
1533
1655
|
repoUrl: 'https://github.com/test/repo',
|
|
1534
1656
|
repoPath: 'test',
|
|
@@ -1546,12 +1668,12 @@ describe('Scheduler checkScaling', () => {
|
|
|
1546
1668
|
`STORY-EXISTING-${index}`,
|
|
1547
1669
|
]);
|
|
1548
1670
|
}
|
|
1549
|
-
const story = createStory(db, {
|
|
1671
|
+
const story = await createStory(db, {
|
|
1550
1672
|
teamId: team.id,
|
|
1551
1673
|
title: 'Gap Index Story',
|
|
1552
1674
|
description: 'Requires spawning the next indexed senior',
|
|
1553
1675
|
});
|
|
1554
|
-
updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1676
|
+
await updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1555
1677
|
const spawnSeniorSpy = vi
|
|
1556
1678
|
.spyOn(scheduler, 'spawnSenior')
|
|
1557
1679
|
.mockImplementation(async (...args) => {
|
|
@@ -1571,29 +1693,29 @@ describe('Scheduler checkScaling', () => {
|
|
|
1571
1693
|
const result = await scheduler.assignStories();
|
|
1572
1694
|
expect(result.assigned).toBe(1);
|
|
1573
1695
|
expect(spawnSeniorSpy).toHaveBeenCalledTimes(1);
|
|
1574
|
-
expect(getStoryById(db, story.id)?.assigned_agent_id).toBe('senior-gap-6');
|
|
1696
|
+
expect((await getStoryById(db, story.id))?.assigned_agent_id).toBe('senior-gap-6');
|
|
1575
1697
|
spawnSeniorSpy.mockRestore();
|
|
1576
1698
|
});
|
|
1577
1699
|
it('should not assign multiple stories to the same senior in one cycle', async () => {
|
|
1578
|
-
const team = createTeam(db, {
|
|
1700
|
+
const team = await createTeam(db, {
|
|
1579
1701
|
name: 'Single Senior Team',
|
|
1580
1702
|
repoUrl: 'https://github.com/test/repo',
|
|
1581
1703
|
repoPath: 'test',
|
|
1582
1704
|
});
|
|
1583
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1705
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, current_story_id, created_at, updated_at)
|
|
1584
1706
|
VALUES (?, ?, ?, ?, NULL, datetime('now'), datetime('now'))`, ['senior-single-1', 'senior', team.id, 'idle']);
|
|
1585
|
-
const story1 = createStory(db, {
|
|
1707
|
+
const story1 = await createStory(db, {
|
|
1586
1708
|
teamId: team.id,
|
|
1587
1709
|
title: 'High Complexity 1',
|
|
1588
1710
|
description: 'Needs senior',
|
|
1589
1711
|
});
|
|
1590
|
-
updateStory(db, story1.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1591
|
-
const story2 = createStory(db, {
|
|
1712
|
+
await updateStory(db, story1.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1713
|
+
const story2 = await createStory(db, {
|
|
1592
1714
|
teamId: team.id,
|
|
1593
1715
|
title: 'High Complexity 2',
|
|
1594
1716
|
description: 'Needs senior too',
|
|
1595
1717
|
});
|
|
1596
|
-
updateStory(db, story2.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1718
|
+
await updateStory(db, story2.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1597
1719
|
const spawnSeniorSpy = vi
|
|
1598
1720
|
.spyOn(scheduler, 'spawnSenior')
|
|
1599
1721
|
.mockRejectedValue(new Error('senior capacity exhausted'));
|
|
@@ -1601,34 +1723,34 @@ describe('Scheduler checkScaling', () => {
|
|
|
1601
1723
|
expect(result.assigned).toBe(1);
|
|
1602
1724
|
expect(result.errors.some(e => e.includes('Failed to spawn Senior'))).toBe(true);
|
|
1603
1725
|
expect(spawnSeniorSpy).toHaveBeenCalledTimes(1);
|
|
1604
|
-
const updatedStory1 = getStoryById(db, story1.id);
|
|
1605
|
-
const updatedStory2 = getStoryById(db, story2.id);
|
|
1726
|
+
const updatedStory1 = (await getStoryById(db, story1.id));
|
|
1727
|
+
const updatedStory2 = (await getStoryById(db, story2.id));
|
|
1606
1728
|
const inProgress = [updatedStory1, updatedStory2].filter(s => s.status === 'in_progress');
|
|
1607
1729
|
const planned = [updatedStory1, updatedStory2].filter(s => s.status === 'planned');
|
|
1608
1730
|
expect(inProgress).toHaveLength(1);
|
|
1609
1731
|
expect(planned).toHaveLength(1);
|
|
1610
1732
|
expect(inProgress[0].assigned_agent_id).toBe('senior-single-1');
|
|
1611
1733
|
expect(planned[0].assigned_agent_id).toBeNull();
|
|
1612
|
-
const seniorRow = db.exec(`SELECT status, current_story_id FROM agents WHERE id = 'senior-single-1'`)[0]?.values[0];
|
|
1734
|
+
const seniorRow = db.db.exec(`SELECT status, current_story_id FROM agents WHERE id = 'senior-single-1'`)[0]?.values[0];
|
|
1613
1735
|
expect(seniorRow?.[0]).toBe('working');
|
|
1614
1736
|
expect(seniorRow?.[1]).toBe(inProgress[0].id);
|
|
1615
1737
|
spawnSeniorSpy.mockRestore();
|
|
1616
1738
|
});
|
|
1617
1739
|
it('should send an explicit assignment handoff to the assigned tmux session', async () => {
|
|
1618
|
-
const team = createTeam(db, {
|
|
1740
|
+
const team = await createTeam(db, {
|
|
1619
1741
|
name: 'Handoff Team',
|
|
1620
1742
|
repoUrl: 'https://github.com/test/repo',
|
|
1621
1743
|
repoPath: 'test',
|
|
1622
1744
|
});
|
|
1623
1745
|
const sessionName = 'hive-senior-handoff-team';
|
|
1624
|
-
db.run(`INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
|
|
1746
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
|
|
1625
1747
|
VALUES (?, ?, ?, ?, ?, NULL, datetime('now'), datetime('now'))`, ['senior-handoff-1', 'senior', team.id, sessionName, 'idle']);
|
|
1626
|
-
const story = createStory(db, {
|
|
1748
|
+
const story = await createStory(db, {
|
|
1627
1749
|
teamId: team.id,
|
|
1628
1750
|
title: 'Needs Context Reset',
|
|
1629
1751
|
description: 'Verify assignment handoff message is sent',
|
|
1630
1752
|
});
|
|
1631
|
-
updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1753
|
+
await updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1632
1754
|
const isRunningSpy = vi.spyOn(tmuxModule, 'isTmuxSessionRunning').mockResolvedValue(true);
|
|
1633
1755
|
const sendSpy = vi.spyOn(tmuxModule, 'sendToTmuxSession').mockResolvedValue();
|
|
1634
1756
|
const result = await scheduler.assignStories();
|
|
@@ -1642,49 +1764,49 @@ describe('Scheduler checkScaling', () => {
|
|
|
1642
1764
|
sendSpy.mockRestore();
|
|
1643
1765
|
});
|
|
1644
1766
|
it('should reject spawning a senior on a busy existing session', async () => {
|
|
1645
|
-
const team = createTeam(db, {
|
|
1767
|
+
const team = await createTeam(db, {
|
|
1646
1768
|
name: 'Spawn Guard Team',
|
|
1647
1769
|
repoUrl: 'https://github.com/test/repo',
|
|
1648
1770
|
repoPath: 'test',
|
|
1649
1771
|
});
|
|
1650
1772
|
const hiveDir = join(mockConfig.rootDir, '.hive');
|
|
1651
1773
|
const expectedSession = generateSessionName('senior', team.name, undefined, hiveDir);
|
|
1652
|
-
db.run(`INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
|
|
1774
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, tmux_session, status, current_story_id, created_at, updated_at)
|
|
1653
1775
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`, ['senior-guard-1', 'senior', team.id, expectedSession, 'working', 'STORY-ACTIVE']);
|
|
1654
1776
|
const isRunningSpy = vi.spyOn(tmuxModule, 'isTmuxSessionRunning').mockResolvedValue(true);
|
|
1655
1777
|
await expect(scheduler.spawnAgent('senior', team.id, team.name, team.repo_path)).rejects.toThrow(/busy session/i);
|
|
1656
1778
|
isRunningSpy.mockRestore();
|
|
1657
1779
|
});
|
|
1658
1780
|
it('should only spawn agents for assignable stories (unblocked dependencies)', async () => {
|
|
1659
|
-
const team = createTeam(db, {
|
|
1781
|
+
const team = await createTeam(db, {
|
|
1660
1782
|
name: 'Test Team',
|
|
1661
1783
|
repoUrl: 'https://github.com/test/repo',
|
|
1662
1784
|
repoPath: 'test',
|
|
1663
1785
|
});
|
|
1664
1786
|
// Create a blocker story that is not yet merged
|
|
1665
|
-
const blockerStory = createStory(db, {
|
|
1787
|
+
const blockerStory = await createStory(db, {
|
|
1666
1788
|
teamId: team.id,
|
|
1667
1789
|
title: 'Blocker Story',
|
|
1668
1790
|
description: 'Must be completed first',
|
|
1669
1791
|
});
|
|
1670
|
-
updateStory(db, blockerStory.id, { status: 'planned', storyPoints: 10 });
|
|
1792
|
+
await updateStory(db, blockerStory.id, { status: 'planned', storyPoints: 10 });
|
|
1671
1793
|
// Create 4 stories that depend on the blocker (cannot be assigned yet)
|
|
1672
1794
|
for (let i = 1; i <= 4; i++) {
|
|
1673
|
-
const story = createStory(db, {
|
|
1795
|
+
const story = await createStory(db, {
|
|
1674
1796
|
teamId: team.id,
|
|
1675
1797
|
title: `Blocked Story ${i}`,
|
|
1676
1798
|
description: 'Depends on blocker',
|
|
1677
1799
|
});
|
|
1678
|
-
updateStory(db, story.id, { status: 'planned', storyPoints: 10 });
|
|
1679
|
-
addStoryDependency(db, story.id, blockerStory.id);
|
|
1800
|
+
await updateStory(db, story.id, { status: 'planned', storyPoints: 10 });
|
|
1801
|
+
await addStoryDependency(db, story.id, blockerStory.id);
|
|
1680
1802
|
}
|
|
1681
1803
|
// Create 1 story with no dependencies (can be assigned)
|
|
1682
|
-
const unblockedStory = createStory(db, {
|
|
1804
|
+
const unblockedStory = await createStory(db, {
|
|
1683
1805
|
teamId: team.id,
|
|
1684
1806
|
title: 'Unblocked Story',
|
|
1685
1807
|
description: 'No dependencies',
|
|
1686
1808
|
});
|
|
1687
|
-
updateStory(db, unblockedStory.id, { status: 'planned', storyPoints: 10 });
|
|
1809
|
+
await updateStory(db, unblockedStory.id, { status: 'planned', storyPoints: 10 });
|
|
1688
1810
|
// Total: 50 story points, but only 10 are assignable
|
|
1689
1811
|
// With senior_capacity: 50, this should spawn 1 senior (10/50 = 0.2, ceil = 1)
|
|
1690
1812
|
// NOT 1 senior (50/50 = 1)
|
|
@@ -1702,27 +1824,27 @@ describe('Scheduler checkScaling', () => {
|
|
|
1702
1824
|
spawnSeniorSpy.mockRestore();
|
|
1703
1825
|
});
|
|
1704
1826
|
it('should not spawn agents when all stories are blocked', async () => {
|
|
1705
|
-
const team = createTeam(db, {
|
|
1827
|
+
const team = await createTeam(db, {
|
|
1706
1828
|
name: 'Test Team',
|
|
1707
1829
|
repoUrl: 'https://github.com/test/repo',
|
|
1708
1830
|
repoPath: 'test',
|
|
1709
1831
|
});
|
|
1710
1832
|
// Create a blocker story that is not yet merged
|
|
1711
|
-
const blockerStory = createStory(db, {
|
|
1833
|
+
const blockerStory = await createStory(db, {
|
|
1712
1834
|
teamId: team.id,
|
|
1713
1835
|
title: 'Blocker Story',
|
|
1714
1836
|
description: 'Must be completed first',
|
|
1715
1837
|
});
|
|
1716
|
-
updateStory(db, blockerStory.id, { status: 'planned', storyPoints: 10 });
|
|
1838
|
+
await updateStory(db, blockerStory.id, { status: 'planned', storyPoints: 10 });
|
|
1717
1839
|
// Create stories that all depend on the blocker
|
|
1718
1840
|
for (let i = 1; i <= 5; i++) {
|
|
1719
|
-
const story = createStory(db, {
|
|
1841
|
+
const story = await createStory(db, {
|
|
1720
1842
|
teamId: team.id,
|
|
1721
1843
|
title: `Blocked Story ${i}`,
|
|
1722
1844
|
description: 'Depends on blocker',
|
|
1723
1845
|
});
|
|
1724
|
-
updateStory(db, story.id, { status: 'planned', storyPoints: 10 });
|
|
1725
|
-
addStoryDependency(db, story.id, blockerStory.id);
|
|
1846
|
+
await updateStory(db, story.id, { status: 'planned', storyPoints: 10 });
|
|
1847
|
+
await addStoryDependency(db, story.id, blockerStory.id);
|
|
1726
1848
|
}
|
|
1727
1849
|
// Mock spawnSenior to track calls
|
|
1728
1850
|
const spawnSeniorSpy = vi.spyOn(scheduler, 'spawnSenior').mockResolvedValue({
|
|
@@ -1749,20 +1871,20 @@ describe('Scheduler Markdown File Writing', () => {
|
|
|
1749
1871
|
rmSync(storiesDir, { recursive: true, force: true });
|
|
1750
1872
|
});
|
|
1751
1873
|
it('should write markdown files when assigning stories via scheduler', async () => {
|
|
1752
|
-
const team = createTeam(db, {
|
|
1874
|
+
const team = await createTeam(db, {
|
|
1753
1875
|
name: 'MD Write Team',
|
|
1754
1876
|
repoUrl: 'https://github.com/test/repo',
|
|
1755
1877
|
repoPath: 'test',
|
|
1756
1878
|
});
|
|
1757
1879
|
// Create an idle senior agent
|
|
1758
|
-
db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
1880
|
+
db.db.run(`INSERT INTO agents (id, type, team_id, status, created_at, updated_at)
|
|
1759
1881
|
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))`, ['senior-md-1', 'senior', team.id, 'idle']);
|
|
1760
|
-
const story = createStory(db, {
|
|
1882
|
+
const story = await createStory(db, {
|
|
1761
1883
|
teamId: team.id,
|
|
1762
1884
|
title: 'Markdown Test Story',
|
|
1763
1885
|
description: 'Should get a markdown file on assignment',
|
|
1764
1886
|
});
|
|
1765
|
-
updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1887
|
+
await updateStory(db, story.id, { status: 'planned', complexityScore: 10, storyPoints: 8 });
|
|
1766
1888
|
// Create scheduler with rootDir pointing to a temp dir that has .hive/stories/
|
|
1767
1889
|
const hiveRoot = join(tmpdir(), `hive-md-root-${Date.now()}`);
|
|
1768
1890
|
const hiveStoriesDir = join(hiveRoot, '.hive', 'stories');
|
|
@@ -1779,27 +1901,27 @@ describe('Scheduler Markdown File Writing', () => {
|
|
|
1779
1901
|
const mdPath = join(hiveStoriesDir, `${story.id}.md`);
|
|
1780
1902
|
expect(existsSync(mdPath)).toBe(true);
|
|
1781
1903
|
// Verify markdown_path was set in DB
|
|
1782
|
-
const updatedStory = getStoryById(db, story.id);
|
|
1904
|
+
const updatedStory = await getStoryById(db, story.id);
|
|
1783
1905
|
expect(updatedStory?.markdown_path).toBe(mdPath);
|
|
1784
1906
|
expect(updatedStory?.status).toBe('in_progress');
|
|
1785
1907
|
rmSync(hiveRoot, { recursive: true, force: true });
|
|
1786
1908
|
});
|
|
1787
1909
|
});
|
|
1788
1910
|
describe('Scheduler Target Branch Propagation', () => {
|
|
1789
|
-
it('should retrieve target_branch from requirement when creating story', () => {
|
|
1790
|
-
const team = createTeam(db, {
|
|
1911
|
+
it('should retrieve target_branch from requirement when creating story', async () => {
|
|
1912
|
+
const team = await createTeam(db, {
|
|
1791
1913
|
name: 'Test Team',
|
|
1792
1914
|
repoUrl: 'https://github.com/test/repo',
|
|
1793
1915
|
repoPath: 'test',
|
|
1794
1916
|
});
|
|
1795
1917
|
// Create a requirement with custom target_branch
|
|
1796
|
-
const requirement = createRequirement(db, {
|
|
1918
|
+
const requirement = await createRequirement(db, {
|
|
1797
1919
|
title: 'Feature for Release Branch',
|
|
1798
1920
|
description: 'Test feature',
|
|
1799
1921
|
targetBranch: 'release/v2.0',
|
|
1800
1922
|
});
|
|
1801
1923
|
// Create a story linked to this requirement
|
|
1802
|
-
const story = createStory(db, {
|
|
1924
|
+
const story = await createStory(db, {
|
|
1803
1925
|
teamId: team.id,
|
|
1804
1926
|
requirementId: requirement.id,
|
|
1805
1927
|
title: 'Story for Release',
|
|
@@ -1809,51 +1931,51 @@ describe('Scheduler Target Branch Propagation', () => {
|
|
|
1809
1931
|
expect(story.requirement_id).toBe(requirement.id);
|
|
1810
1932
|
expect(story.team_id).toBe(team.id);
|
|
1811
1933
|
// Verify we can retrieve the requirement and its target_branch
|
|
1812
|
-
const retrievedReq = db.exec(`SELECT target_branch FROM requirements WHERE id = '${requirement.id}'`)[0]?.values[0];
|
|
1934
|
+
const retrievedReq = db.db.exec(`SELECT target_branch FROM requirements WHERE id = '${requirement.id}'`)[0]?.values[0];
|
|
1813
1935
|
expect(retrievedReq?.[0]).toBe('release/v2.0');
|
|
1814
1936
|
});
|
|
1815
|
-
it('should use default target_branch (main) when requirement has no custom branch', () => {
|
|
1937
|
+
it('should use default target_branch (main) when requirement has no custom branch', async () => {
|
|
1816
1938
|
// Create a requirement without specifying target_branch
|
|
1817
|
-
const requirement = createRequirement(db, {
|
|
1939
|
+
const requirement = await createRequirement(db, {
|
|
1818
1940
|
title: 'Feature for Main Branch',
|
|
1819
1941
|
description: 'Test feature',
|
|
1820
1942
|
});
|
|
1821
1943
|
// Verify the requirement defaults to main branch
|
|
1822
|
-
const retrievedReq = db.exec(`SELECT target_branch FROM requirements WHERE id = '${requirement.id}'`)[0]?.values[0];
|
|
1944
|
+
const retrievedReq = db.db.exec(`SELECT target_branch FROM requirements WHERE id = '${requirement.id}'`)[0]?.values[0];
|
|
1823
1945
|
expect(retrievedReq?.[0]).toBe('main');
|
|
1824
1946
|
});
|
|
1825
|
-
it('should handle stories with different target branches from different requirements', () => {
|
|
1826
|
-
const team = createTeam(db, {
|
|
1947
|
+
it('should handle stories with different target branches from different requirements', async () => {
|
|
1948
|
+
const team = await createTeam(db, {
|
|
1827
1949
|
name: 'Test Team',
|
|
1828
1950
|
repoUrl: 'https://github.com/test/repo',
|
|
1829
1951
|
repoPath: 'test',
|
|
1830
1952
|
});
|
|
1831
1953
|
// Create two requirements with different target branches
|
|
1832
|
-
const req1 = createRequirement(db, {
|
|
1954
|
+
const req1 = await createRequirement(db, {
|
|
1833
1955
|
title: 'Main Feature',
|
|
1834
1956
|
description: 'Goes to main',
|
|
1835
1957
|
targetBranch: 'main',
|
|
1836
1958
|
});
|
|
1837
|
-
const req2 = createRequirement(db, {
|
|
1959
|
+
const req2 = await createRequirement(db, {
|
|
1838
1960
|
title: 'Staging Feature',
|
|
1839
1961
|
description: 'Goes to staging',
|
|
1840
1962
|
targetBranch: 'staging',
|
|
1841
1963
|
});
|
|
1842
1964
|
// Create stories for each requirement
|
|
1843
|
-
const story1 = createStory(db, {
|
|
1965
|
+
const story1 = await createStory(db, {
|
|
1844
1966
|
teamId: team.id,
|
|
1845
1967
|
requirementId: req1.id,
|
|
1846
1968
|
title: 'Story 1',
|
|
1847
1969
|
description: 'Test',
|
|
1848
1970
|
});
|
|
1849
|
-
const story2 = createStory(db, {
|
|
1971
|
+
const story2 = await createStory(db, {
|
|
1850
1972
|
teamId: team.id,
|
|
1851
1973
|
requirementId: req2.id,
|
|
1852
1974
|
title: 'Story 2',
|
|
1853
1975
|
description: 'Test',
|
|
1854
1976
|
});
|
|
1855
1977
|
// Verify each story can access its requirement's target_branch via JOIN
|
|
1856
|
-
const result = db.exec(`SELECT s.id, r.target_branch
|
|
1978
|
+
const result = db.db.exec(`SELECT s.id, r.target_branch
|
|
1857
1979
|
FROM stories s
|
|
1858
1980
|
LEFT JOIN requirements r ON s.requirement_id = r.id
|
|
1859
1981
|
WHERE s.id IN ('${story1.id}', '${story2.id}')
|
|
@@ -1863,21 +1985,21 @@ describe('Scheduler Target Branch Propagation', () => {
|
|
|
1863
1985
|
expect(branches).toContain('main');
|
|
1864
1986
|
expect(branches).toContain('staging');
|
|
1865
1987
|
});
|
|
1866
|
-
it('should handle stories without a linked requirement (null requirement_id)', () => {
|
|
1867
|
-
const team = createTeam(db, {
|
|
1988
|
+
it('should handle stories without a linked requirement (null requirement_id)', async () => {
|
|
1989
|
+
const team = await createTeam(db, {
|
|
1868
1990
|
name: 'Test Team',
|
|
1869
1991
|
repoUrl: 'https://github.com/test/repo',
|
|
1870
1992
|
repoPath: 'test',
|
|
1871
1993
|
});
|
|
1872
1994
|
// Create a story without a requirement
|
|
1873
|
-
const story = createStory(db, {
|
|
1995
|
+
const story = await createStory(db, {
|
|
1874
1996
|
teamId: team.id,
|
|
1875
1997
|
title: 'Standalone Story',
|
|
1876
1998
|
description: 'No requirement',
|
|
1877
1999
|
});
|
|
1878
2000
|
expect(story.requirement_id).toBeNull();
|
|
1879
2001
|
// When joining with requirements, should get null for target_branch
|
|
1880
|
-
const result = db.exec(`SELECT s.id, r.target_branch
|
|
2002
|
+
const result = db.db.exec(`SELECT s.id, r.target_branch
|
|
1881
2003
|
FROM stories s
|
|
1882
2004
|
LEFT JOIN requirements r ON s.requirement_id = r.id
|
|
1883
2005
|
WHERE s.id = '${story.id}'`);
|