agentic-orchestrator 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +15 -0
- package/CLAUDE.md +126 -0
- package/README.md +166 -25
- package/agentic/orchestrator/adapters.yaml +3 -0
- package/agentic/orchestrator/gates.yaml +47 -0
- package/agentic/orchestrator/policy.yaml +89 -0
- package/agentic/orchestrator/schemas/adapters.schema.json +12 -0
- package/agentic/orchestrator/schemas/gates.schema.json +6 -1
- package/agentic/orchestrator/schemas/index.schema.json +14 -0
- package/agentic/orchestrator/schemas/multi-project.schema.json +41 -0
- package/agentic/orchestrator/schemas/policy.schema.json +449 -52
- package/agentic/orchestrator/schemas/state.schema.json +16 -0
- package/agentic/orchestrator/tools/catalog.json +68 -0
- package/agentic/orchestrator/tools/schemas/input/cost.get.input.schema.json +10 -0
- package/agentic/orchestrator/tools/schemas/input/cost.record.input.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/input/feature.send_message.input.schema.json +11 -0
- package/agentic/orchestrator/tools/schemas/input/performance.get_analytics.input.schema.json +10 -0
- package/agentic/orchestrator/tools/schemas/input/performance.record_outcome.input.schema.json +18 -0
- package/agentic/orchestrator/tools/schemas/output/cost.get.output.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/output/cost.record.output.schema.json +13 -0
- package/agentic/orchestrator/tools/schemas/output/feature.ready_to_merge.output.schema.json +7 -0
- package/agentic/orchestrator/tools/schemas/output/feature.send_message.output.schema.json +23 -0
- package/agentic/orchestrator/tools/schemas/output/performance.get_analytics.output.schema.json +46 -0
- package/agentic/orchestrator/tools/schemas/output/performance.record_outcome.output.schema.json +10 -0
- package/agentic/orchestrator/tools.md +5 -0
- package/apps/control-plane/scripts/validate-architecture-rules.mjs +28 -2
- package/apps/control-plane/scripts/validate-docker-mcp-contract.mjs +12 -0
- package/apps/control-plane/scripts/validate-mcp-contracts.ts +92 -0
- package/apps/control-plane/src/application/adapters/adapter-registry.ts +169 -0
- package/apps/control-plane/src/application/multi-project-loader.ts +119 -0
- package/apps/control-plane/src/application/services/activity-monitor-service.ts +199 -0
- package/apps/control-plane/src/application/services/cost-tracking-service.ts +82 -0
- package/apps/control-plane/src/application/services/dependency-scheduler-service.ts +86 -0
- package/apps/control-plane/src/application/services/feature-deletion-service.ts +8 -7
- package/apps/control-plane/src/application/services/gate-interpolation-service.ts +15 -0
- package/apps/control-plane/src/application/services/gate-service.ts +38 -2
- package/apps/control-plane/src/application/services/instance-isolation-service.ts +18 -0
- package/apps/control-plane/src/application/services/issue-tracker-service.ts +469 -0
- package/apps/control-plane/src/application/services/merge-service.ts +67 -3
- package/apps/control-plane/src/application/services/notifier-service.ts +295 -0
- package/apps/control-plane/src/application/services/performance-analytics-service.ts +122 -0
- package/apps/control-plane/src/application/services/plan-service.ts +51 -0
- package/apps/control-plane/src/application/services/pr-monitor-service.ts +262 -0
- package/apps/control-plane/src/application/services/reactions-service.ts +175 -0
- package/apps/control-plane/src/application/services/reporting-service.ts +17 -2
- package/apps/control-plane/src/application/services/run-lease-service.ts +16 -38
- package/apps/control-plane/src/application/tools/tool-metadata.ts +4 -1
- package/apps/control-plane/src/cli/aop.ts +1 -1
- package/apps/control-plane/src/cli/attach-command-handler.ts +120 -0
- package/apps/control-plane/src/cli/cleanup-command-handler.ts +190 -0
- package/apps/control-plane/src/cli/cli-argument-parser.ts +69 -3
- package/apps/control-plane/src/cli/dashboard-command-handler.ts +57 -0
- package/apps/control-plane/src/cli/help-command-handler.ts +163 -0
- package/apps/control-plane/src/cli/init-command-handler.ts +609 -0
- package/apps/control-plane/src/cli/resume-command-handler.ts +1 -0
- package/apps/control-plane/src/cli/retry-command-handler.ts +138 -0
- package/apps/control-plane/src/cli/run-command-handler.ts +115 -3
- package/apps/control-plane/src/cli/send-command-handler.ts +65 -0
- package/apps/control-plane/src/cli/status-command-handler.ts +102 -2
- package/apps/control-plane/src/cli/types.ts +26 -1
- package/apps/control-plane/src/core/constants.ts +8 -2
- package/apps/control-plane/src/core/error-codes.ts +3 -1
- package/apps/control-plane/src/core/gates.ts +170 -50
- package/apps/control-plane/src/core/kernel.ts +280 -5
- package/apps/control-plane/src/core/path-layout.ts +12 -0
- package/apps/control-plane/src/core/tool-caller.ts +36 -0
- package/apps/control-plane/src/core/workspace-hooks.ts +87 -0
- package/apps/control-plane/src/interfaces/cli/bootstrap.ts +258 -9
- package/apps/control-plane/src/providers/providers.ts +235 -14
- package/apps/control-plane/src/supervisor/build-wave-executor.ts +129 -8
- package/apps/control-plane/src/supervisor/qa-wave-executor.ts +123 -5
- package/apps/control-plane/src/supervisor/run-coordinator.ts +143 -6
- package/apps/control-plane/src/supervisor/runtime.ts +135 -6
- package/apps/control-plane/src/supervisor/types.ts +12 -21
- package/apps/control-plane/src/supervisor/worker-decision-loop.ts +8 -0
- package/apps/control-plane/test/activity-monitor.spec.ts +294 -0
- package/apps/control-plane/test/adapter-registry.spec.ts +132 -0
- package/apps/control-plane/test/batch-operations.spec.ts +112 -0
- package/apps/control-plane/test/bootstrap-attach.spec.ts +102 -0
- package/apps/control-plane/test/bootstrap-edge-cases.spec.ts +252 -0
- package/apps/control-plane/test/bootstrap.spec.ts +560 -0
- package/apps/control-plane/test/cleanup-command.spec.ts +301 -0
- package/apps/control-plane/test/cli-helpers.spec.ts +404 -1
- package/apps/control-plane/test/cli.unit.spec.ts +182 -1
- package/apps/control-plane/test/collision-queue.spec.ts +104 -1
- package/apps/control-plane/test/core-utils.spec.ts +175 -2
- package/apps/control-plane/test/cost-tracking.spec.ts +143 -0
- package/apps/control-plane/test/dashboard-api.integration.spec.ts +247 -0
- package/apps/control-plane/test/dashboard-client.spec.ts +116 -0
- package/apps/control-plane/test/dashboard-command.spec.ts +103 -0
- package/apps/control-plane/test/dependency-scheduler.spec.ts +189 -0
- package/apps/control-plane/test/epoch-tracking.spec.ts +4 -4
- package/apps/control-plane/test/feature-deletion-service.spec.ts +422 -0
- package/apps/control-plane/test/feature-lifecycle.spec.ts +202 -0
- package/apps/control-plane/test/git-spawn-error.spec.ts +24 -0
- package/apps/control-plane/test/incremental-gates.spec.ts +137 -0
- package/apps/control-plane/test/init-wizard.spec.ts +506 -0
- package/apps/control-plane/test/instance-isolation.spec.ts +83 -0
- package/apps/control-plane/test/issue-tracker.spec.ts +890 -0
- package/apps/control-plane/test/kernel.coverage.spec.ts +3 -5
- package/apps/control-plane/test/kernel.coverage2.spec.ts +871 -0
- package/apps/control-plane/test/kernel.spec.ts +13 -11
- package/apps/control-plane/test/lock-service.spec.ts +508 -0
- package/apps/control-plane/test/mcp-helpers.spec.ts +176 -0
- package/apps/control-plane/test/mcp.spec.ts +50 -15
- package/apps/control-plane/test/merge-service.spec.ts +67 -4
- package/apps/control-plane/test/multi-project.spec.ts +372 -0
- package/apps/control-plane/test/notifier-service.spec.ts +388 -0
- package/apps/control-plane/test/parallel-gates.spec.ts +312 -0
- package/apps/control-plane/test/patch-service.spec.ts +253 -0
- package/apps/control-plane/test/performance-analytics.spec.ts +338 -0
- package/apps/control-plane/test/planning-wave-executor.spec.ts +168 -0
- package/apps/control-plane/test/pr-monitor.spec.ts +385 -0
- package/apps/control-plane/test/providers.spec.ts +344 -1
- package/apps/control-plane/test/reactions.spec.ts +392 -0
- package/apps/control-plane/test/resume-command.spec.ts +390 -0
- package/apps/control-plane/test/run-coordinator.spec.ts +481 -2
- package/apps/control-plane/test/schema-date-time.spec.ts +46 -0
- package/apps/control-plane/test/service-retry-paths.spec.ts +30 -0
- package/apps/control-plane/test/services.spec.ts +95 -2
- package/apps/control-plane/test/session-management.spec.ts +450 -0
- package/apps/control-plane/test/spec-ingestion.spec.ts +190 -0
- package/apps/control-plane/test/supervisor-collaborators.spec.ts +699 -2
- package/apps/control-plane/test/supervisor.spec.ts +36 -30
- package/apps/control-plane/test/supervisor.unit.spec.ts +405 -0
- package/apps/control-plane/test/worker-decision-loop.spec.ts +57 -0
- package/apps/control-plane/test/workspace-hooks.spec.ts +177 -0
- package/apps/control-plane/vitest.config.ts +21 -5
- package/dist/apps/control-plane/application/adapters/adapter-registry.d.ts +44 -0
- package/dist/apps/control-plane/application/adapters/adapter-registry.js +76 -0
- package/dist/apps/control-plane/application/adapters/adapter-registry.js.map +1 -0
- package/dist/apps/control-plane/application/multi-project-loader.d.ts +31 -0
- package/dist/apps/control-plane/application/multi-project-loader.js +82 -0
- package/dist/apps/control-plane/application/multi-project-loader.js.map +1 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.d.ts +43 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js +132 -0
- package/dist/apps/control-plane/application/services/activity-monitor-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.d.ts +28 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js +48 -0
- package/dist/apps/control-plane/application/services/cost-tracking-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.d.ts +26 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.js +75 -0
- package/dist/apps/control-plane/application/services/dependency-scheduler-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.d.ts +2 -0
- package/dist/apps/control-plane/application/services/feature-deletion-service.js +6 -7
- package/dist/apps/control-plane/application/services/feature-deletion-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/gate-interpolation-service.d.ts +7 -0
- package/dist/apps/control-plane/application/services/gate-interpolation-service.js +7 -0
- package/dist/apps/control-plane/application/services/gate-interpolation-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/gate-service.js +32 -2
- package/dist/apps/control-plane/application/services/gate-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/instance-isolation-service.d.ts +11 -0
- package/dist/apps/control-plane/application/services/instance-isolation-service.js +17 -0
- package/dist/apps/control-plane/application/services/instance-isolation-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.d.ts +65 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js +358 -0
- package/dist/apps/control-plane/application/services/issue-tracker-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/merge-service.d.ts +4 -0
- package/dist/apps/control-plane/application/services/merge-service.js +44 -2
- package/dist/apps/control-plane/application/services/merge-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/notifier-service.d.ts +74 -0
- package/dist/apps/control-plane/application/services/notifier-service.js +212 -0
- package/dist/apps/control-plane/application/services/notifier-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.d.ts +39 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js +75 -0
- package/dist/apps/control-plane/application/services/performance-analytics-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/plan-service.d.ts +1 -0
- package/dist/apps/control-plane/application/services/plan-service.js +53 -0
- package/dist/apps/control-plane/application/services/plan-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/pr-monitor-service.d.ts +44 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js +192 -0
- package/dist/apps/control-plane/application/services/pr-monitor-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/reactions-service.d.ts +67 -0
- package/dist/apps/control-plane/application/services/reactions-service.js +114 -0
- package/dist/apps/control-plane/application/services/reactions-service.js.map +1 -0
- package/dist/apps/control-plane/application/services/reporting-service.d.ts +1 -0
- package/dist/apps/control-plane/application/services/reporting-service.js +13 -2
- package/dist/apps/control-plane/application/services/reporting-service.js.map +1 -1
- package/dist/apps/control-plane/application/services/run-lease-service.d.ts +2 -0
- package/dist/apps/control-plane/application/services/run-lease-service.js +14 -38
- package/dist/apps/control-plane/application/services/run-lease-service.js.map +1 -1
- package/dist/apps/control-plane/application/tools/tool-metadata.js +3 -1
- package/dist/apps/control-plane/application/tools/tool-metadata.js.map +1 -1
- package/dist/apps/control-plane/cli/aop.d.ts +1 -1
- package/dist/apps/control-plane/cli/aop.js +1 -1
- package/dist/apps/control-plane/cli/attach-command-handler.d.ts +12 -0
- package/dist/apps/control-plane/cli/attach-command-handler.js +98 -0
- package/dist/apps/control-plane/cli/attach-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.d.ts +12 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.js +162 -0
- package/dist/apps/control-plane/cli/cleanup-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/cli-argument-parser.js +73 -3
- package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
- package/dist/apps/control-plane/cli/dashboard-command-handler.d.ts +7 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js +45 -0
- package/dist/apps/control-plane/cli/dashboard-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/help-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/help-command-handler.js +146 -0
- package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/init-command-handler.d.ts +26 -0
- package/dist/apps/control-plane/cli/init-command-handler.js +517 -0
- package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/resume-command-handler.js +1 -1
- package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/retry-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/retry-command-handler.js +111 -0
- package/dist/apps/control-plane/cli/retry-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/run-command-handler.d.ts +5 -0
- package/dist/apps/control-plane/cli/run-command-handler.js +82 -3
- package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/send-command-handler.d.ts +8 -0
- package/dist/apps/control-plane/cli/send-command-handler.js +55 -0
- package/dist/apps/control-plane/cli/send-command-handler.js.map +1 -0
- package/dist/apps/control-plane/cli/status-command-handler.d.ts +12 -1
- package/dist/apps/control-plane/cli/status-command-handler.js +55 -2
- package/dist/apps/control-plane/cli/status-command-handler.js.map +1 -1
- package/dist/apps/control-plane/cli/types.d.ts +26 -1
- package/dist/apps/control-plane/cli/types.js +15 -1
- package/dist/apps/control-plane/cli/types.js.map +1 -1
- package/dist/apps/control-plane/core/constants.d.ts +6 -0
- package/dist/apps/control-plane/core/constants.js +8 -2
- package/dist/apps/control-plane/core/constants.js.map +1 -1
- package/dist/apps/control-plane/core/error-codes.d.ts +2 -0
- package/dist/apps/control-plane/core/error-codes.js +3 -1
- package/dist/apps/control-plane/core/error-codes.js.map +1 -1
- package/dist/apps/control-plane/core/gates.d.ts +4 -0
- package/dist/apps/control-plane/core/gates.js +140 -43
- package/dist/apps/control-plane/core/gates.js.map +1 -1
- package/dist/apps/control-plane/core/kernel.d.ts +50 -1
- package/dist/apps/control-plane/core/kernel.js +220 -7
- package/dist/apps/control-plane/core/kernel.js.map +1 -1
- package/dist/apps/control-plane/core/path-layout.d.ts +3 -0
- package/dist/apps/control-plane/core/path-layout.js +9 -0
- package/dist/apps/control-plane/core/path-layout.js.map +1 -1
- package/dist/apps/control-plane/core/tool-caller.d.ts +32 -0
- package/dist/apps/control-plane/core/tool-caller.js +2 -0
- package/dist/apps/control-plane/core/tool-caller.js.map +1 -0
- package/dist/apps/control-plane/core/workspace-hooks.d.ts +20 -0
- package/dist/apps/control-plane/core/workspace-hooks.js +69 -0
- package/dist/apps/control-plane/core/workspace-hooks.js.map +1 -0
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js +245 -9
- package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
- package/dist/apps/control-plane/providers/providers.d.ts +42 -3
- package/dist/apps/control-plane/providers/providers.js +216 -5
- package/dist/apps/control-plane/providers/providers.js.map +1 -1
- package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/build-wave-executor.js +115 -6
- package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js +109 -5
- package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
- package/dist/apps/control-plane/supervisor/run-coordinator.d.ts +15 -0
- package/dist/apps/control-plane/supervisor/run-coordinator.js +132 -6
- package/dist/apps/control-plane/supervisor/run-coordinator.js.map +1 -1
- package/dist/apps/control-plane/supervisor/runtime.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/runtime.js +110 -6
- package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
- package/dist/apps/control-plane/supervisor/types.d.ts +9 -16
- package/dist/apps/control-plane/supervisor/types.js.map +1 -1
- package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +3 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js +5 -0
- package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
- package/eslint.config.mjs +2 -1
- package/package.json +12 -2
- package/packages/web-dashboard/next-env.d.ts +5 -0
- package/packages/web-dashboard/next.config.js +7 -0
- package/packages/web-dashboard/package.json +26 -0
- package/packages/web-dashboard/src/app/api/actions/route.ts +64 -0
- package/packages/web-dashboard/src/app/api/events/route.ts +51 -0
- package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +256 -0
- package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +10 -0
- package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +25 -0
- package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +63 -0
- package/packages/web-dashboard/src/app/api/features/[id]/route.ts +16 -0
- package/packages/web-dashboard/src/app/api/projects/route.ts +31 -0
- package/packages/web-dashboard/src/app/api/status/route.ts +15 -0
- package/packages/web-dashboard/src/app/globals.css +2 -0
- package/packages/web-dashboard/src/app/layout.tsx +15 -0
- package/packages/web-dashboard/src/app/page.tsx +393 -0
- package/packages/web-dashboard/src/lib/aop-client.ts +244 -0
- package/packages/web-dashboard/src/lib/multi-project-config.ts +116 -0
- package/packages/web-dashboard/src/lib/orchestrator-tools.ts +284 -0
- package/packages/web-dashboard/src/lib/types.ts +58 -0
- package/packages/web-dashboard/tsconfig.json +40 -0
- package/packages/web-dashboard/vitest.config.ts +6 -0
- package/spec-files/completed/agentic_orchestrator_feature_gaps_closure_spec.md +1764 -0
- package/spec-files/outstanding/agentic_orchestrator_enterprise_governance_dashboard_spec.md +348 -0
- package/spec-files/outstanding/agentic_orchestrator_knowledge_canary_spec.md +344 -0
- package/spec-files/outstanding/agentic_orchestrator_observability_integrity_diagnostics_spec.md +374 -0
- package/spec-files/outstanding/agentic_orchestrator_performance_improvements_spec.md +1059 -0
- package/spec-files/outstanding/agentic_orchestrator_planning_review_quality_spec.md +466 -0
- package/spec-files/outstanding/agentic_orchestrator_quality_adoption_execution_spec.md +198 -0
- package/spec-files/outstanding/agentic_orchestrator_validator_hardening_spec.md +365 -0
- package/spec-files/progress.md +481 -52
- /package/spec-files/{agentic_orchestrator_cli_delete_command_spec.md → completed/agentic_orchestrator_cli_delete_command_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_dot_aop_generated_artifacts_spec.md → completed/agentic_orchestrator_dot_aop_generated_artifacts_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_mcp_formalization_spec.md → completed/agentic_orchestrator_mcp_formalization_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_oop_refactor_spec.md → completed/agentic_orchestrator_oop_refactor_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_single_global_orchestrator_spec.md → completed/agentic_orchestrator_single_global_orchestrator_spec.md} +0 -0
- /package/spec-files/{agentic_orchestrator_spec.md → completed/agentic_orchestrator_spec.md} +0 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
GitHubIssueTracker,
|
|
4
|
+
LinearIssueTracker,
|
|
5
|
+
JiraIssueTracker,
|
|
6
|
+
createIssueTracker
|
|
7
|
+
} from '../src/application/services/issue-tracker-service.js';
|
|
8
|
+
import type { GhRunner, HttpRunner } from '../src/application/services/issue-tracker-service.js';
|
|
9
|
+
|
|
10
|
+
function makeGhRunner(stdout: string, exitCode = 0): GhRunner {
|
|
11
|
+
return vi.fn(async () => ({ stdout, exitCode }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeHttpRunner(
|
|
15
|
+
responses: Array<{ status?: number; ok?: boolean; body?: unknown }>
|
|
16
|
+
): HttpRunner & ReturnType<typeof vi.fn> {
|
|
17
|
+
const queue = [...responses];
|
|
18
|
+
const runner = vi.fn(async () => {
|
|
19
|
+
const next = queue.shift() ?? { status: 500, ok: false, body: '' };
|
|
20
|
+
const body =
|
|
21
|
+
typeof next.body === 'string'
|
|
22
|
+
? next.body
|
|
23
|
+
: next.body === undefined
|
|
24
|
+
? ''
|
|
25
|
+
: JSON.stringify(next.body);
|
|
26
|
+
return {
|
|
27
|
+
status: next.status ?? 200,
|
|
28
|
+
ok: next.ok ?? true,
|
|
29
|
+
body
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
return runner as HttpRunner & ReturnType<typeof vi.fn>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('G9: Multi-Tracker Support', () => {
|
|
36
|
+
describe('GitHubIssueTracker', () => {
|
|
37
|
+
it('getIssue parses gh CLI output', async () => {
|
|
38
|
+
const payload = JSON.stringify({
|
|
39
|
+
number: 42,
|
|
40
|
+
title: 'Add dark mode',
|
|
41
|
+
body: 'Users want dark mode.',
|
|
42
|
+
state: 'OPEN',
|
|
43
|
+
url: 'https://github.com/org/repo/issues/42'
|
|
44
|
+
});
|
|
45
|
+
const runner = makeGhRunner(payload);
|
|
46
|
+
const tracker = new GitHubIssueTracker({ repo: 'org/repo' }, runner);
|
|
47
|
+
|
|
48
|
+
const issue = await tracker.getIssue('42');
|
|
49
|
+
expect(issue.id).toBe('42');
|
|
50
|
+
expect(issue.title).toBe('Add dark mode');
|
|
51
|
+
expect(issue.status).toBe('open');
|
|
52
|
+
expect(issue.url).toContain('issues/42');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('getIssue returns empty fallback when gh is not available', async () => {
|
|
56
|
+
const runner = makeGhRunner('', 127);
|
|
57
|
+
const tracker = new GitHubIssueTracker({}, runner);
|
|
58
|
+
|
|
59
|
+
const issue = await tracker.getIssue('99');
|
|
60
|
+
expect(issue.id).toBe('99');
|
|
61
|
+
expect(issue.title).toBe('');
|
|
62
|
+
expect(issue.status).toBe('unknown');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('updateIssueStatus and addComment route through gh CLI args', async () => {
|
|
66
|
+
const runner = makeGhRunner('');
|
|
67
|
+
const tracker = new GitHubIssueTracker({ repo: 'org/repo' }, runner);
|
|
68
|
+
|
|
69
|
+
await tracker.updateIssueStatus('42', 'merged');
|
|
70
|
+
await tracker.addComment('42', 'AOP: status -> merged');
|
|
71
|
+
|
|
72
|
+
expect(runner).toHaveBeenCalledWith(expect.arrayContaining(['issue', 'edit', '42', '--state', 'closed']));
|
|
73
|
+
expect(runner).toHaveBeenCalledWith(
|
|
74
|
+
expect.arrayContaining(['issue', 'comment', '42', '--body', 'AOP: status -> merged'])
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('GIVEN_no_ghRunner_injected_WHEN_getIssue_called_THEN_returns_fallback_issue', async () => {
|
|
79
|
+
const tracker = new GitHubIssueTracker({});
|
|
80
|
+
const issue = await tracker.getIssue('999');
|
|
81
|
+
expect(issue.id).toBe('999');
|
|
82
|
+
expect(issue.status).toBe('unknown');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('LinearIssueTracker', () => {
|
|
87
|
+
it('GIVEN_linear_issue_exists_WHEN_getIssue_called_THEN_parses_graphql_response', async () => {
|
|
88
|
+
const httpRunner = makeHttpRunner([
|
|
89
|
+
{
|
|
90
|
+
body: {
|
|
91
|
+
data: {
|
|
92
|
+
issueByIdentifier: {
|
|
93
|
+
id: 'lin_uuid_1',
|
|
94
|
+
identifier: 'LIN-123',
|
|
95
|
+
title: 'Ship feature flag',
|
|
96
|
+
description: 'Implement rollout toggle',
|
|
97
|
+
url: 'https://linear.app/acme/issue/LIN-123',
|
|
98
|
+
state: { name: 'In Progress' }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]);
|
|
104
|
+
const tracker = new LinearIssueTracker(
|
|
105
|
+
{
|
|
106
|
+
token: 'linear-token',
|
|
107
|
+
base_url: 'https://api.linear.app/graphql'
|
|
108
|
+
},
|
|
109
|
+
httpRunner
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const issue = await tracker.getIssue('LIN-123');
|
|
113
|
+
|
|
114
|
+
expect(issue).toMatchObject({
|
|
115
|
+
id: 'LIN-123',
|
|
116
|
+
title: 'Ship feature flag',
|
|
117
|
+
body: 'Implement rollout toggle',
|
|
118
|
+
status: 'in progress',
|
|
119
|
+
url: 'https://linear.app/acme/issue/LIN-123'
|
|
120
|
+
});
|
|
121
|
+
expect(httpRunner).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(httpRunner).toHaveBeenCalledWith(
|
|
123
|
+
'https://api.linear.app/graphql',
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: expect.objectContaining({
|
|
127
|
+
Authorization: 'Bearer linear-token'
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('GIVEN_state_id_mapping_WHEN_updateIssueStatus_called_THEN_issues_graphql_issueUpdate_mutation', async () => {
|
|
134
|
+
const httpRunner = makeHttpRunner([
|
|
135
|
+
{
|
|
136
|
+
body: {
|
|
137
|
+
data: {
|
|
138
|
+
issueByIdentifier: {
|
|
139
|
+
id: 'lin_uuid_2',
|
|
140
|
+
identifier: 'LIN-200',
|
|
141
|
+
title: 'Refactor queue',
|
|
142
|
+
description: '',
|
|
143
|
+
url: 'https://linear.app/acme/issue/LIN-200',
|
|
144
|
+
state: { name: 'Todo' }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
body: {
|
|
151
|
+
data: {
|
|
152
|
+
issueUpdate: {
|
|
153
|
+
success: true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
]);
|
|
159
|
+
const tracker = new LinearIssueTracker(
|
|
160
|
+
{
|
|
161
|
+
state_id_building: 'state_in_progress'
|
|
162
|
+
},
|
|
163
|
+
httpRunner
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await tracker.updateIssueStatus('LIN-200', 'building');
|
|
167
|
+
|
|
168
|
+
expect(httpRunner).toHaveBeenCalledTimes(2);
|
|
169
|
+
const secondCallBody = JSON.parse(String(httpRunner.mock.calls[1]?.[1]?.body));
|
|
170
|
+
expect(secondCallBody.query).toContain('issueUpdate');
|
|
171
|
+
expect(secondCallBody.variables).toMatchObject({
|
|
172
|
+
id: 'lin_uuid_2',
|
|
173
|
+
stateId: 'state_in_progress'
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('GIVEN_issue_exists_WHEN_addComment_called_THEN_posts_commentCreate_mutation', async () => {
|
|
178
|
+
const httpRunner = makeHttpRunner([
|
|
179
|
+
{
|
|
180
|
+
body: {
|
|
181
|
+
data: {
|
|
182
|
+
issueByIdentifier: {
|
|
183
|
+
id: 'lin_uuid_3',
|
|
184
|
+
identifier: 'LIN-300',
|
|
185
|
+
title: 'Fix race condition',
|
|
186
|
+
description: '',
|
|
187
|
+
url: 'https://linear.app/acme/issue/LIN-300',
|
|
188
|
+
state: { name: 'Todo' }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
body: {
|
|
195
|
+
data: {
|
|
196
|
+
commentCreate: {
|
|
197
|
+
success: true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
]);
|
|
203
|
+
const tracker = new LinearIssueTracker({}, httpRunner);
|
|
204
|
+
|
|
205
|
+
await tracker.addComment('LIN-300', 'AOP: feature status changed to `qa`');
|
|
206
|
+
|
|
207
|
+
expect(httpRunner).toHaveBeenCalledTimes(2);
|
|
208
|
+
const secondCallBody = JSON.parse(String(httpRunner.mock.calls[1]?.[1]?.body));
|
|
209
|
+
expect(secondCallBody.query).toContain('commentCreate');
|
|
210
|
+
expect(secondCallBody.variables).toMatchObject({
|
|
211
|
+
issueId: 'lin_uuid_3',
|
|
212
|
+
body: 'AOP: feature status changed to `qa`'
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('GIVEN_linear_api_failure_WHEN_getIssue_called_THEN_returns_unknown_fallback', async () => {
|
|
217
|
+
const httpRunner = makeHttpRunner([{ ok: false, status: 500, body: '' }]);
|
|
218
|
+
const tracker = new LinearIssueTracker({}, httpRunner);
|
|
219
|
+
|
|
220
|
+
const issue = await tracker.getIssue('LIN-500');
|
|
221
|
+
expect(issue).toEqual({
|
|
222
|
+
id: 'LIN-500',
|
|
223
|
+
title: '',
|
|
224
|
+
body: '',
|
|
225
|
+
status: 'unknown',
|
|
226
|
+
url: ''
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('JiraIssueTracker', () => {
|
|
232
|
+
it('GIVEN_jira_issue_exists_WHEN_getIssue_called_THEN_parses_rest_fields', async () => {
|
|
233
|
+
const httpRunner = makeHttpRunner([
|
|
234
|
+
{
|
|
235
|
+
body: {
|
|
236
|
+
key: 'PROJ-42',
|
|
237
|
+
fields: {
|
|
238
|
+
summary: 'Fix flaky pipeline',
|
|
239
|
+
description: 'Investigate and stabilize',
|
|
240
|
+
status: { name: 'In Progress' }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
]);
|
|
245
|
+
const tracker = new JiraIssueTracker(
|
|
246
|
+
{
|
|
247
|
+
base_url: 'https://jira.example.com',
|
|
248
|
+
email: 'dev@example.com',
|
|
249
|
+
token: 'jira-token'
|
|
250
|
+
},
|
|
251
|
+
httpRunner
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const issue = await tracker.getIssue('PROJ-42');
|
|
255
|
+
expect(issue).toMatchObject({
|
|
256
|
+
id: 'PROJ-42',
|
|
257
|
+
title: 'Fix flaky pipeline',
|
|
258
|
+
body: 'Investigate and stabilize',
|
|
259
|
+
status: 'in progress',
|
|
260
|
+
url: 'https://jira.example.com/browse/PROJ-42'
|
|
261
|
+
});
|
|
262
|
+
expect(httpRunner).toHaveBeenCalledWith(
|
|
263
|
+
'https://jira.example.com/rest/api/2/issue/PROJ-42?fields=summary,description,status',
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
method: 'GET',
|
|
266
|
+
headers: expect.objectContaining({
|
|
267
|
+
Authorization: expect.stringContaining('Basic ')
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('GIVEN_matching_transition_WHEN_updateIssueStatus_called_THEN_posts_transition_update', async () => {
|
|
274
|
+
const httpRunner = makeHttpRunner([
|
|
275
|
+
{
|
|
276
|
+
body: {
|
|
277
|
+
transitions: [
|
|
278
|
+
{ id: '11', name: 'To Do' },
|
|
279
|
+
{ id: '22', name: 'In Progress' },
|
|
280
|
+
{ id: '33', name: 'Done' }
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
body: {}
|
|
286
|
+
}
|
|
287
|
+
]);
|
|
288
|
+
const tracker = new JiraIssueTracker(
|
|
289
|
+
{
|
|
290
|
+
base_url: 'https://jira.example.com',
|
|
291
|
+
transition_building: 'In Progress'
|
|
292
|
+
},
|
|
293
|
+
httpRunner
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
await tracker.updateIssueStatus('PROJ-7', 'building');
|
|
297
|
+
|
|
298
|
+
expect(httpRunner).toHaveBeenCalledTimes(2);
|
|
299
|
+
expect(httpRunner).toHaveBeenNthCalledWith(
|
|
300
|
+
2,
|
|
301
|
+
'https://jira.example.com/rest/api/2/issue/PROJ-7/transitions',
|
|
302
|
+
expect.objectContaining({
|
|
303
|
+
method: 'POST',
|
|
304
|
+
body: JSON.stringify({ transition: { id: '22' } })
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('GIVEN_comment_WHEN_addComment_called_THEN_posts_comment_payload', async () => {
|
|
310
|
+
const httpRunner = makeHttpRunner([{ body: {} }]);
|
|
311
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com' }, httpRunner);
|
|
312
|
+
|
|
313
|
+
await tracker.addComment('PROJ-8', 'AOP: feature status changed to `merged`');
|
|
314
|
+
|
|
315
|
+
expect(httpRunner).toHaveBeenCalledWith(
|
|
316
|
+
'https://jira.example.com/rest/api/2/issue/PROJ-8/comment',
|
|
317
|
+
expect.objectContaining({
|
|
318
|
+
method: 'POST',
|
|
319
|
+
body: JSON.stringify({ body: 'AOP: feature status changed to `merged`' })
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('createIssueTracker factory', () => {
|
|
326
|
+
it('returns tracker instances for known types and honors enabled=false', () => {
|
|
327
|
+
const github = createIssueTracker({ type: 'github', config: { repo: 'o/r' } });
|
|
328
|
+
const linear = createIssueTracker({ type: 'linear' });
|
|
329
|
+
const jira = createIssueTracker({ type: 'jira' });
|
|
330
|
+
const disabled = createIssueTracker({ type: 'github', enabled: false });
|
|
331
|
+
|
|
332
|
+
expect(github).toBeInstanceOf(GitHubIssueTracker);
|
|
333
|
+
expect(linear).toBeInstanceOf(LinearIssueTracker);
|
|
334
|
+
expect(jira).toBeInstanceOf(JiraIssueTracker);
|
|
335
|
+
expect(disabled).toBeUndefined();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('returns undefined for unknown type and undefined config', () => {
|
|
339
|
+
expect(createIssueTracker({ type: 'unknown-tracker' })).toBeUndefined();
|
|
340
|
+
expect(createIssueTracker(undefined)).toBeUndefined();
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('issue-tracker-service branch coverage', () => {
|
|
346
|
+
describe('createGhRunner', () => {
|
|
347
|
+
it('GIVEN_function_provided_WHEN_createGhRunner_called_THEN_returns_that_function', async () => {
|
|
348
|
+
const { createGhRunner } = await import('../src/application/services/issue-tracker-service.js');
|
|
349
|
+
const custom = vi.fn(async () => ({ stdout: 'ok', exitCode: 0 }));
|
|
350
|
+
const runner = createGhRunner(custom as GhRunner);
|
|
351
|
+
expect(runner).toBe(custom);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('GitHubIssueTracker', () => {
|
|
356
|
+
it('GIVEN_gh_not_found_WHEN_getIssue_called_THEN_returns_fallback', async () => {
|
|
357
|
+
const runner: GhRunner = async () => ({ stdout: '', exitCode: 127 });
|
|
358
|
+
const tracker = new GitHubIssueTracker({}, runner);
|
|
359
|
+
const issue = await tracker.getIssue('123');
|
|
360
|
+
expect(issue.id).toBe('123');
|
|
361
|
+
expect(issue.url).toBe('');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('GIVEN_invalid_json_WHEN_getIssue_called_THEN_returns_fallback', async () => {
|
|
365
|
+
const runner: GhRunner = async () => ({ stdout: '', exitCode: 0 });
|
|
366
|
+
const tracker = new GitHubIssueTracker({}, runner);
|
|
367
|
+
const issue = await tracker.getIssue('456');
|
|
368
|
+
expect(issue.id).toBe('456');
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('LinearIssueTracker', () => {
|
|
373
|
+
it('GIVEN_status_blocked_WHEN_updateIssueStatus_called_THEN_maps_to_blocked', async () => {
|
|
374
|
+
const calls: Array<[string, string, string]> = [];
|
|
375
|
+
const runner: HttpRunner = async (_url, init) => {
|
|
376
|
+
const body = JSON.parse(init.body as string) as Record<string, unknown>;
|
|
377
|
+
const vars = body['variables'] as Record<string, unknown> | undefined;
|
|
378
|
+
calls.push([body['query'] as string, vars?.['issueId'] as string, vars?.['stateId'] as string]);
|
|
379
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
|
|
380
|
+
};
|
|
381
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_blocked: 'state_b' }, runner);
|
|
382
|
+
await tracker.updateIssueStatus('LIN-1', 'blocked');
|
|
383
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('GIVEN_http_failure_WHEN_getIssue_called_THEN_returns_fallback_unknown', async () => {
|
|
387
|
+
const runner: HttpRunner = async () => { throw new Error('network error'); };
|
|
388
|
+
const tracker = new LinearIssueTracker({ token: 'tok' }, runner as never);
|
|
389
|
+
const issue = await tracker.getIssue('LIN-999');
|
|
390
|
+
expect(issue.id).toBe('LIN-999');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('JiraIssueTracker', () => {
|
|
395
|
+
it('GIVEN_status_planning_WHEN_updateIssueStatus_called_THEN_maps_to_to_do_transition', async () => {
|
|
396
|
+
const calls: Array<unknown> = [];
|
|
397
|
+
const runner: HttpRunner = async (_url, init) => {
|
|
398
|
+
calls.push(JSON.parse(init.body as string));
|
|
399
|
+
return { status: 204, ok: true, body: '' };
|
|
400
|
+
};
|
|
401
|
+
// Need to stub getTransitions first (GET /transitions returns match)
|
|
402
|
+
let callCount = 0;
|
|
403
|
+
const mockRunner: HttpRunner = async (url, init) => {
|
|
404
|
+
callCount += 1;
|
|
405
|
+
if (url.includes('transitions') && (!init.method || init.method === 'GET')) {
|
|
406
|
+
return {
|
|
407
|
+
status: 200, ok: true, body: JSON.stringify({
|
|
408
|
+
transitions: [{ id: 't1', name: 'To Do' }]
|
|
409
|
+
})
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return runner(url, init);
|
|
413
|
+
};
|
|
414
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok', email: 'me@example.com' }, mockRunner);
|
|
415
|
+
await tracker.updateIssueStatus('PROJ-1', 'planning');
|
|
416
|
+
expect(callCount).toBeGreaterThan(0);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('JiraIssueTracker additional branch coverage', () => {
|
|
422
|
+
it('GIVEN_requestJson_returns_null_WHEN_getIssue_called_THEN_returns_fallback', async () => {
|
|
423
|
+
// Return non-ok response → requestJson returns null
|
|
424
|
+
const runner: HttpRunner = async () => ({ status: 404, ok: false, body: '' });
|
|
425
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
426
|
+
const issue = await tracker.getIssue('PROJ-999');
|
|
427
|
+
expect(issue.id).toBe('PROJ-999');
|
|
428
|
+
expect(issue.status).toBe('unknown');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('GIVEN_no_matching_transition_WHEN_updateIssueStatus_called_THEN_returns_without_posting', async () => {
|
|
432
|
+
const calls: string[] = [];
|
|
433
|
+
const runner: HttpRunner = async (url, init) => {
|
|
434
|
+
calls.push(`${String(init.method ?? 'GET')} ${url}`);
|
|
435
|
+
// Only GET for transitions, no matching transition name
|
|
436
|
+
return {
|
|
437
|
+
status: 200, ok: true, body: JSON.stringify({ transitions: [{ id: 'x', name: 'To Do' }] })
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
441
|
+
// 'building' maps to 'in progress' but transitions only have 'To Do'
|
|
442
|
+
await tracker.updateIssueStatus('PROJ-1', 'building');
|
|
443
|
+
// Only GET should have been called (no POST)
|
|
444
|
+
expect(calls.every((c) => c.startsWith('GET'))).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('GIVEN_no_base_url_WHEN_requestJson_called_THEN_returns_null_and_getIssue_returns_fallback', async () => {
|
|
448
|
+
const runner: HttpRunner = async () => ({ status: 200, ok: true, body: '{}' });
|
|
449
|
+
const tracker = new JiraIssueTracker({}, runner); // no base_url
|
|
450
|
+
const issue = await tracker.getIssue('PROJ-0');
|
|
451
|
+
expect(issue.status).toBe('unknown');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('GIVEN_token_but_no_email_WHEN_authHeader_called_via_getIssue_THEN_uses_bearer_token', async () => {
|
|
455
|
+
let capturedAuth: string | undefined;
|
|
456
|
+
const runner: HttpRunner = async (_, init) => {
|
|
457
|
+
capturedAuth = (init.headers as Record<string, string>)?.['Authorization'];
|
|
458
|
+
return { status: 200, ok: true, body: JSON.stringify({ key: 'PROJ-1', fields: { summary: 'Test', status: { name: 'Open' } } }) };
|
|
459
|
+
};
|
|
460
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'mytoken' }, runner);
|
|
461
|
+
await tracker.getIssue('PROJ-1');
|
|
462
|
+
expect(capturedAuth).toBe('Bearer mytoken');
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('issue-tracker-service additional branch coverage', () => {
|
|
467
|
+
it('GIVEN_jira_object_description_WHEN_toJiraDescription_called_THEN_returns_json_stringified', async () => {
|
|
468
|
+
// toJiraDescription is called internally when updating an issue that has an object description field
|
|
469
|
+
const runner: HttpRunner = async () => ({
|
|
470
|
+
status: 200,
|
|
471
|
+
ok: true,
|
|
472
|
+
body: JSON.stringify({
|
|
473
|
+
key: 'PROJ-1',
|
|
474
|
+
fields: { summary: 'Test', description: { type: 'doc', content: [] }, status: { name: 'Open' } }
|
|
475
|
+
})
|
|
476
|
+
});
|
|
477
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
478
|
+
// getIssue passes the description through toJiraDescription when calling addComment
|
|
479
|
+
const issue = await tracker.getIssue('PROJ-1');
|
|
480
|
+
// description field (object) should be stringified
|
|
481
|
+
expect(typeof issue.title).toBe('string');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('GIVEN_jira_config_transition_override_WHEN_updateIssueStatus_THEN_uses_explicit_transition', async () => {
|
|
485
|
+
const calls: string[] = [];
|
|
486
|
+
const runner: HttpRunner = async (url, init) => {
|
|
487
|
+
calls.push(`${init.method} ${url}`);
|
|
488
|
+
if (init.method === 'GET') {
|
|
489
|
+
return {
|
|
490
|
+
status: 200,
|
|
491
|
+
ok: true,
|
|
492
|
+
body: JSON.stringify({ transitions: [{ id: '99', name: 'Custom Done' }] })
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return { status: 204, ok: true, body: '' };
|
|
496
|
+
};
|
|
497
|
+
// config has explicit transition for 'merged' → 'custom done'
|
|
498
|
+
const tracker = new JiraIssueTracker(
|
|
499
|
+
{ base_url: 'https://jira.example.com', token: 'tok', transition_merged: 'custom done' },
|
|
500
|
+
runner
|
|
501
|
+
);
|
|
502
|
+
await tracker.updateIssueStatus('PROJ-2', 'merged');
|
|
503
|
+
// POST should have been called since 'custom done' matches 'Custom Done' (case-insensitive)
|
|
504
|
+
expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('GIVEN_linear_planning_status_WHEN_updateIssueStatus_THEN_maps_to_backlog', async () => {
|
|
508
|
+
const bodies: string[] = [];
|
|
509
|
+
const runner: HttpRunner = async (_, init) => {
|
|
510
|
+
bodies.push(init.body ?? '');
|
|
511
|
+
// First call: resolveIssueNode → return a valid issue
|
|
512
|
+
if (bodies.length === 1) {
|
|
513
|
+
return {
|
|
514
|
+
status: 200, ok: true,
|
|
515
|
+
body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-1', identifier: 'LIN-1', title: 'T', description: '', url: '', state: { name: 'Backlog' } } } })
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
// Second call: mutation
|
|
519
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
|
|
520
|
+
};
|
|
521
|
+
// Provide state_id_backlog so resolveStateId returns non-null → mutation is called
|
|
522
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_id_backlog: 'state-backlog-id' }, runner);
|
|
523
|
+
await tracker.updateIssueStatus('LIN-1', 'planning');
|
|
524
|
+
// mapDefaultLinearStatus('planning') → 'backlog' → state_id_backlog → mutation called
|
|
525
|
+
expect(bodies.length).toBe(2);
|
|
526
|
+
expect(bodies[1]).toContain('issueUpdate');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('GIVEN_linear_qa_status_WHEN_updateIssueStatus_THEN_maps_to_in_review', async () => {
|
|
530
|
+
const bodies: string[] = [];
|
|
531
|
+
const runner: HttpRunner = async (_, init) => {
|
|
532
|
+
bodies.push(init.body ?? '');
|
|
533
|
+
if (bodies.length === 1) {
|
|
534
|
+
return {
|
|
535
|
+
status: 200, ok: true,
|
|
536
|
+
body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-2', identifier: 'LIN-2', title: 'T', description: '', url: '', state: { name: 'In Review' } } } })
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
|
|
540
|
+
};
|
|
541
|
+
// Provide state_id_in_review so resolveStateId returns non-null → mutation is called
|
|
542
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_id_in_review: 'state-inreview-id' }, runner);
|
|
543
|
+
await tracker.updateIssueStatus('LIN-2', 'qa');
|
|
544
|
+
// mapDefaultLinearStatus('qa') → 'in_review' → state_id_in_review → mutation called
|
|
545
|
+
expect(bodies.length).toBe(2);
|
|
546
|
+
expect(bodies[1]).toContain('issueUpdate');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('GIVEN_jira_building_status_WHEN_updateIssueStatus_THEN_maps_to_in_progress', async () => {
|
|
550
|
+
const calls: string[] = [];
|
|
551
|
+
const runner: HttpRunner = async (url, init) => {
|
|
552
|
+
calls.push(`${init.method} ${url}`);
|
|
553
|
+
if (init.method === 'GET') {
|
|
554
|
+
return {
|
|
555
|
+
status: 200,
|
|
556
|
+
ok: true,
|
|
557
|
+
body: JSON.stringify({ transitions: [{ id: '5', name: 'In Progress' }] })
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return { status: 204, ok: true, body: '' };
|
|
561
|
+
};
|
|
562
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
563
|
+
await tracker.updateIssueStatus('PROJ-3', 'building');
|
|
564
|
+
// mapDefaultJiraTransition('building') → 'in progress' → matches 'In Progress'
|
|
565
|
+
expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('GIVEN_jira_planning_status_WHEN_updateIssueStatus_THEN_maps_to_to_do', async () => {
|
|
569
|
+
const calls: string[] = [];
|
|
570
|
+
const runner: HttpRunner = async (url, init) => {
|
|
571
|
+
calls.push(`${init.method} ${url}`);
|
|
572
|
+
if (init.method === 'GET') {
|
|
573
|
+
return {
|
|
574
|
+
status: 200,
|
|
575
|
+
ok: true,
|
|
576
|
+
body: JSON.stringify({ transitions: [{ id: '1', name: 'To Do' }] })
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
return { status: 204, ok: true, body: '' };
|
|
580
|
+
};
|
|
581
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
582
|
+
await tracker.updateIssueStatus('PROJ-4', 'planning');
|
|
583
|
+
// mapDefaultJiraTransition('planning') → 'to do' → matches 'To Do'
|
|
584
|
+
expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('GIVEN_jira_blocked_status_WHEN_updateIssueStatus_THEN_maps_to_blocked', async () => {
|
|
588
|
+
const calls: string[] = [];
|
|
589
|
+
const runner: HttpRunner = async (url, init) => {
|
|
590
|
+
calls.push(`${init.method} ${url}`);
|
|
591
|
+
if (init.method === 'GET') {
|
|
592
|
+
return {
|
|
593
|
+
status: 200,
|
|
594
|
+
ok: true,
|
|
595
|
+
body: JSON.stringify({ transitions: [{ id: '7', name: 'Blocked' }] })
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return { status: 204, ok: true, body: '' };
|
|
599
|
+
};
|
|
600
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
601
|
+
await tracker.updateIssueStatus('PROJ-5', 'blocked');
|
|
602
|
+
expect(calls.some((c) => c.startsWith('POST'))).toBe(true);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
describe('Issue tracker additional branch coverage', () => {
|
|
607
|
+
it('GIVEN_createHttpRunner_called_without_fn_WHEN_fetch_throws_THEN_returns_zero_status', async () => {
|
|
608
|
+
const { createHttpRunner } = await import('../src/application/services/issue-tracker-service.js');
|
|
609
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network error'));
|
|
610
|
+
const runner = createHttpRunner();
|
|
611
|
+
const result = await runner('https://example.com', { method: 'GET' });
|
|
612
|
+
expect(result.status).toBe(0);
|
|
613
|
+
expect(result.ok).toBe(false);
|
|
614
|
+
expect(result.body).toBe('');
|
|
615
|
+
fetchSpy.mockRestore();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('GIVEN_linear_graphql_response_with_errors_array_WHEN_getIssue_THEN_returns_fallback', async () => {
|
|
619
|
+
const runner: HttpRunner = async () => ({
|
|
620
|
+
status: 200,
|
|
621
|
+
ok: true,
|
|
622
|
+
body: JSON.stringify({ data: {}, errors: [{ message: 'GraphQL error' }] })
|
|
623
|
+
});
|
|
624
|
+
const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
|
|
625
|
+
const issue = await tracker.getIssue('LIN-99');
|
|
626
|
+
expect(issue.id).toBe('LIN-99');
|
|
627
|
+
expect(issue.title).toBe('');
|
|
628
|
+
expect(issue.status).toBe('unknown');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('GIVEN_linear_response_with_non_object_parsed_WHEN_getIssue_THEN_returns_fallback', async () => {
|
|
632
|
+
const runner: HttpRunner = async () => ({
|
|
633
|
+
status: 200,
|
|
634
|
+
ok: true,
|
|
635
|
+
body: JSON.stringify({ data: null })
|
|
636
|
+
});
|
|
637
|
+
const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
|
|
638
|
+
const issue = await tracker.getIssue('LIN-100');
|
|
639
|
+
expect(issue.status).toBe('unknown');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('GIVEN_linear_with_unknown_status_WHEN_updateIssueStatus_THEN_uses_in_progress_default', async () => {
|
|
643
|
+
const bodies: string[] = [];
|
|
644
|
+
const runner: HttpRunner = async (_, init) => {
|
|
645
|
+
bodies.push(init.body ?? '');
|
|
646
|
+
if (bodies.length === 1) {
|
|
647
|
+
return {
|
|
648
|
+
status: 200, ok: true,
|
|
649
|
+
body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-3', identifier: 'LIN-3', title: 'T', description: '', url: '', state: null } } })
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
|
|
653
|
+
};
|
|
654
|
+
// Provide state_id_in_progress to cover the 'in_progress' default mapping for unknown status
|
|
655
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_id_in_progress: 'state-inprogress-id' }, runner);
|
|
656
|
+
await tracker.updateIssueStatus('LIN-3', 'some_random_unknown_status');
|
|
657
|
+
// mapDefaultLinearStatus returns 'in_progress' → state_id_in_progress → mutation called
|
|
658
|
+
expect(bodies.length).toBe(2);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('GIVEN_linear_resolveStateId_no_explicit_no_default_WHEN_updateIssueStatus_THEN_no_mutation', async () => {
|
|
662
|
+
let callCount = 0;
|
|
663
|
+
const runner: HttpRunner = async () => {
|
|
664
|
+
callCount += 1;
|
|
665
|
+
if (callCount === 1) {
|
|
666
|
+
return {
|
|
667
|
+
status: 200, ok: true,
|
|
668
|
+
body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-4', identifier: 'LIN-4', title: 'T', description: '', url: '', state: null } } })
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: {} }) };
|
|
672
|
+
};
|
|
673
|
+
// No state_id_* keys → resolveStateId returns null → no mutation
|
|
674
|
+
const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
|
|
675
|
+
await tracker.updateIssueStatus('LIN-4', 'some_status');
|
|
676
|
+
// Only resolveIssueNode call (2 queries: by identifier + by id), no mutation
|
|
677
|
+
expect(callCount).toBeLessThanOrEqual(3);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('GIVEN_jira_token_only_no_email_WHEN_requestJson_called_THEN_uses_bearer_auth', async () => {
|
|
681
|
+
const calls: { auth?: string }[] = [];
|
|
682
|
+
const runner: HttpRunner = async (_, init) => {
|
|
683
|
+
calls.push({ auth: (init.headers as Record<string, string>)['Authorization'] });
|
|
684
|
+
return { status: 200, ok: true, body: JSON.stringify({ key: 'PROJ-1', fields: { summary: 'Test', description: 'Body', status: { name: 'Open' } } }) };
|
|
685
|
+
};
|
|
686
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'my-token' }, runner);
|
|
687
|
+
const issue = await tracker.getIssue('PROJ-1');
|
|
688
|
+
expect(issue.title).toBe('Test');
|
|
689
|
+
expect(calls[0].auth).toMatch(/^Bearer /);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('GIVEN_jira_no_base_url_WHEN_getIssue_called_THEN_returns_fallback', async () => {
|
|
693
|
+
const runner: HttpRunner = vi.fn(async () => ({ status: 200, ok: true, body: '' }));
|
|
694
|
+
const tracker = new JiraIssueTracker({}, runner);
|
|
695
|
+
const issue = await tracker.getIssue('PROJ-1');
|
|
696
|
+
expect(issue.title).toBe('');
|
|
697
|
+
expect(issue.status).toBe('unknown');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('GIVEN_jira_no_matching_transition_WHEN_updateIssueStatus_THEN_no_post_made', async () => {
|
|
701
|
+
const calls: string[] = [];
|
|
702
|
+
const runner: HttpRunner = async (_url, init) => {
|
|
703
|
+
calls.push(init.method ?? 'GET');
|
|
704
|
+
return { status: 200, ok: true, body: JSON.stringify({ transitions: [{ id: '1', name: 'Open' }] }) };
|
|
705
|
+
};
|
|
706
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
707
|
+
await tracker.updateIssueStatus('PROJ-1', 'merged'); // mapDefaultJiraTransition → 'done', no matching transition
|
|
708
|
+
expect(calls.filter((m) => m === 'POST')).toHaveLength(0);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('GIVEN_jira_no_auth_WHEN_requestJson_called_THEN_no_authorization_header', async () => {
|
|
712
|
+
const calls: { hasAuth: boolean }[] = [];
|
|
713
|
+
const runner: HttpRunner = async (_, init) => {
|
|
714
|
+
calls.push({ hasAuth: 'Authorization' in (init.headers as Record<string, string>) });
|
|
715
|
+
return { status: 200, ok: true, body: JSON.stringify({ key: 'PROJ-1', fields: { summary: '', description: null, status: { name: 'Open' } } }) };
|
|
716
|
+
};
|
|
717
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com' }, runner);
|
|
718
|
+
await tracker.getIssue('PROJ-1');
|
|
719
|
+
expect(calls[0].hasAuth).toBe(false);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('GIVEN_linear_http_response_not_ok_WHEN_graphQl_called_THEN_returns_null_and_fallback', async () => {
|
|
723
|
+
const runner: HttpRunner = async () => ({ status: 500, ok: false, body: '' });
|
|
724
|
+
const tracker = new LinearIssueTracker({ token: 'tok' }, runner);
|
|
725
|
+
const issue = await tracker.getIssue('LIN-500');
|
|
726
|
+
expect(issue.status).toBe('unknown');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('GIVEN_jira_description_is_object_WHEN_getIssue_called_THEN_returns_json_string', async () => {
|
|
730
|
+
const runner: HttpRunner = async () => ({
|
|
731
|
+
status: 200, ok: true,
|
|
732
|
+
body: JSON.stringify({
|
|
733
|
+
key: 'PROJ-2',
|
|
734
|
+
fields: {
|
|
735
|
+
summary: 'Test',
|
|
736
|
+
description: { type: 'doc', content: [] },
|
|
737
|
+
status: { name: 'In Progress' }
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
});
|
|
741
|
+
const tracker = new JiraIssueTracker({ base_url: 'https://jira.example.com', token: 'tok' }, runner);
|
|
742
|
+
const issue = await tracker.getIssue('PROJ-2');
|
|
743
|
+
expect(issue.body).toContain('doc');
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
describe('LinearIssueTracker addComment and updateIssueStatus guard branches', () => {
|
|
748
|
+
it('GIVEN_issue_not_found_WHEN_addComment_called_THEN_returns_without_posting', async () => {
|
|
749
|
+
// resolveIssueNode returns null → !issue?.id guard returns early (lines 314-315)
|
|
750
|
+
const runner: HttpRunner = async () => ({
|
|
751
|
+
status: 200,
|
|
752
|
+
ok: true,
|
|
753
|
+
body: JSON.stringify({ data: { issueByIdentifier: null } })
|
|
754
|
+
});
|
|
755
|
+
// Should not throw and should not call graphql mutation
|
|
756
|
+
let callCount = 0;
|
|
757
|
+
const countingRunner: HttpRunner = async (url, init) => {
|
|
758
|
+
callCount++;
|
|
759
|
+
return runner(url, init);
|
|
760
|
+
};
|
|
761
|
+
const tracker2 = new LinearIssueTracker({}, countingRunner);
|
|
762
|
+
await tracker2.addComment('LIN-999', 'some comment');
|
|
763
|
+
// Only the resolveIssueNode queries are called, no mutation
|
|
764
|
+
expect(callCount).toBeLessThanOrEqual(2);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('GIVEN_resolved_issue_with_no_id_field_WHEN_addComment_called_THEN_returns_early', async () => {
|
|
768
|
+
// resolveIssueNode returns issue node but without id → !issue?.id guard
|
|
769
|
+
const runner: HttpRunner = async () => ({
|
|
770
|
+
status: 200,
|
|
771
|
+
ok: true,
|
|
772
|
+
body: JSON.stringify({ data: { issueByIdentifier: { identifier: 'LIN-1', title: 'T', description: '', url: '' } } })
|
|
773
|
+
});
|
|
774
|
+
const tracker = new LinearIssueTracker({}, runner);
|
|
775
|
+
await expect(tracker.addComment('LIN-1', 'comment')).resolves.toBeUndefined();
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('GIVEN_issue_not_found_WHEN_updateIssueStatus_called_THEN_returns_without_mutation', async () => {
|
|
779
|
+
const runner: HttpRunner = async () => ({
|
|
780
|
+
status: 200,
|
|
781
|
+
ok: true,
|
|
782
|
+
body: JSON.stringify({ data: { issueByIdentifier: null } })
|
|
783
|
+
});
|
|
784
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_id_building: 'state-id' }, runner);
|
|
785
|
+
await expect(tracker.updateIssueStatus('LIN-999', 'building')).resolves.toBeUndefined();
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
describe('LinearIssueTracker graphQl invalid body branch', () => {
|
|
790
|
+
it('GIVEN_response_body_is_invalid_json_WHEN_graphQl_called_THEN_returns_null_fallback', async () => {
|
|
791
|
+
// Returns ok:true but body is not valid JSON → tryParseJson returns null → lines 218-219 covered
|
|
792
|
+
const runner: HttpRunner = async () => ({
|
|
793
|
+
status: 200,
|
|
794
|
+
ok: true,
|
|
795
|
+
body: 'not valid json {{ response'
|
|
796
|
+
});
|
|
797
|
+
const tracker = new LinearIssueTracker({}, runner);
|
|
798
|
+
const issue = await tracker.getIssue('LIN-invalid-json');
|
|
799
|
+
expect(issue.status).toBe('unknown');
|
|
800
|
+
expect(issue.id).toBe('LIN-invalid-json');
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('GIVEN_response_body_is_empty_string_WHEN_graphQl_called_THEN_returns_null_fallback', async () => {
|
|
804
|
+
// tryParseJson returns null for empty string → lines 218-219 covered
|
|
805
|
+
const runner: HttpRunner = async () => ({
|
|
806
|
+
status: 200,
|
|
807
|
+
ok: true,
|
|
808
|
+
body: ''
|
|
809
|
+
});
|
|
810
|
+
const tracker = new LinearIssueTracker({}, runner);
|
|
811
|
+
const issue = await tracker.getIssue('LIN-empty-body');
|
|
812
|
+
expect(issue.status).toBe('unknown');
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
describe('LinearIssueTracker mapDefaultLinearStatus building branch', () => {
|
|
817
|
+
it('GIVEN_building_status_no_explicit_id_but_has_in_progress_WHEN_updateIssueStatus_THEN_uses_mapped_state', async () => {
|
|
818
|
+
// status='building' → normalizeAopStatus='building' → explicit (state_id_building) not in config
|
|
819
|
+
// → mapDefaultLinearStatus('building') → returns 'in_progress' → lines 168-169 covered
|
|
820
|
+
let callCount = 0;
|
|
821
|
+
const runner: HttpRunner = async () => {
|
|
822
|
+
callCount++;
|
|
823
|
+
if (callCount === 1) {
|
|
824
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-uuid-build', identifier: 'LIN-B', title: 'T', description: '', url: '', state: null } } }) };
|
|
825
|
+
}
|
|
826
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
|
|
827
|
+
};
|
|
828
|
+
// No state_id_building but has state_id_in_progress → mapDefaultLinearStatus('building') → 'in_progress'
|
|
829
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_id_in_progress: 'state-ip-id' }, runner);
|
|
830
|
+
await tracker.updateIssueStatus('LIN-B', 'building');
|
|
831
|
+
// Mutation should be called (callCount > 2 if resolveIssueNode needs 2 calls)
|
|
832
|
+
expect(callCount).toBeGreaterThan(1);
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
describe('LinearIssueTracker mapDefaultLinearStatus OR branches and lowerCaseStatus', () => {
|
|
837
|
+
it('GIVEN_status_closed_WHEN_updateIssueStatus_THEN_uses_done_default_mapping', async () => {
|
|
838
|
+
// 'closed' → normalized='closed' → line 155: merged||closed - 'merged' is FALSE, 'closed' is TRUE (Branch 0 covered)
|
|
839
|
+
let callCount = 0;
|
|
840
|
+
const runner: HttpRunner = async () => {
|
|
841
|
+
callCount++;
|
|
842
|
+
if (callCount === 1) {
|
|
843
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-closed', identifier: 'LIN-C', title: 'T', description: '', url: '', state: null } } }) };
|
|
844
|
+
}
|
|
845
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
|
|
846
|
+
};
|
|
847
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_id_done: 'state-done-id' }, runner);
|
|
848
|
+
await tracker.updateIssueStatus('LIN-C', 'closed');
|
|
849
|
+
expect(callCount).toBeGreaterThan(1);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('GIVEN_status_failed_WHEN_updateIssueStatus_THEN_uses_blocked_default_mapping', async () => {
|
|
853
|
+
// 'failed' → normalized='failed' → line 158: blocked||failed - 'blocked' is FALSE, 'failed' is TRUE (Branch 0 covered)
|
|
854
|
+
let callCount = 0;
|
|
855
|
+
const runner: HttpRunner = async () => {
|
|
856
|
+
callCount++;
|
|
857
|
+
if (callCount === 1) {
|
|
858
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-failed', identifier: 'LIN-F', title: 'T', description: '', url: '', state: null } } }) };
|
|
859
|
+
}
|
|
860
|
+
return { status: 200, ok: true, body: JSON.stringify({ data: { issueUpdate: { success: true } } }) };
|
|
861
|
+
};
|
|
862
|
+
const tracker = new LinearIssueTracker({ token: 'tok', state_id_blocked: 'state-blocked-id' }, runner);
|
|
863
|
+
await tracker.updateIssueStatus('LIN-F', 'failed');
|
|
864
|
+
expect(callCount).toBeGreaterThan(1);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it('GIVEN_issue_with_null_state_WHEN_getIssue_THEN_status_is_unknown', async () => {
|
|
868
|
+
// null state → state?.name = undefined → ?? '' → lowerCaseStatus('') → '' is falsy → 'unknown' (line 146 FALSE branch + line 291)
|
|
869
|
+
const runner: HttpRunner = async () => ({
|
|
870
|
+
status: 200,
|
|
871
|
+
ok: true,
|
|
872
|
+
body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-nostate', identifier: 'LIN-NS', title: 'No State', description: 'desc', url: 'https://x', state: null } } })
|
|
873
|
+
});
|
|
874
|
+
const tracker = new LinearIssueTracker({}, runner);
|
|
875
|
+
const issue = await tracker.getIssue('LIN-NS');
|
|
876
|
+
expect(issue.status).toBe('unknown');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('GIVEN_issue_without_identifier_WHEN_getIssue_THEN_falls_back_to_id_or_issueId', async () => {
|
|
880
|
+
// issue.identifier is falsy → falls back to issue.id (line 288 Block 100 Branch 0 covered)
|
|
881
|
+
const runner: HttpRunner = async () => ({
|
|
882
|
+
status: 200,
|
|
883
|
+
ok: true,
|
|
884
|
+
body: JSON.stringify({ data: { issueByIdentifier: { id: 'lin-raw-id', identifier: '', title: 'T', description: 'desc', url: 'https://x', state: { name: 'In Progress' } } } })
|
|
885
|
+
});
|
|
886
|
+
const tracker = new LinearIssueTracker({}, runner);
|
|
887
|
+
const issue = await tracker.getIssue('LIN-NOID');
|
|
888
|
+
expect(issue.id).toBe('lin-raw-id');
|
|
889
|
+
});
|
|
890
|
+
});
|