@ktpartners/dgs-platform 2.6.2
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/LICENSE +38 -0
- package/README.md +851 -0
- package/agents/dgs-codebase-cross-analyzer.md +183 -0
- package/agents/dgs-codebase-mapper.md +782 -0
- package/agents/dgs-codebase-synthesizer.md +156 -0
- package/agents/dgs-debugger.md +1256 -0
- package/agents/dgs-executor.md +550 -0
- package/agents/dgs-integration-checker.md +481 -0
- package/agents/dgs-nyquist-auditor.md +178 -0
- package/agents/dgs-phase-researcher.md +563 -0
- package/agents/dgs-phase-verifier.md +450 -0
- package/agents/dgs-plan-checker.md +708 -0
- package/agents/dgs-planner.md +1324 -0
- package/agents/dgs-project-researcher.md +631 -0
- package/agents/dgs-research-synthesizer.md +249 -0
- package/agents/dgs-roadmapper.md +652 -0
- package/agents/dgs-verifier.md +607 -0
- package/bin/install.js +2073 -0
- package/commands/dgs/add-doc.md +45 -0
- package/commands/dgs/add-idea.md +38 -0
- package/commands/dgs/add-phase.md +43 -0
- package/commands/dgs/add-repo.md +54 -0
- package/commands/dgs/add-tests.md +41 -0
- package/commands/dgs/add-todo.md +47 -0
- package/commands/dgs/approve-spec.md +38 -0
- package/commands/dgs/audit-milestone.md +36 -0
- package/commands/dgs/audit-phase.md +37 -0
- package/commands/dgs/cancel-job.md +23 -0
- package/commands/dgs/capture-principle.md +143 -0
- package/commands/dgs/check-todos.md +45 -0
- package/commands/dgs/cleanup.md +18 -0
- package/commands/dgs/complete-milestone.md +136 -0
- package/commands/dgs/complete-project.md +70 -0
- package/commands/dgs/consolidate-ideas.md +50 -0
- package/commands/dgs/create-milestone-job.md +37 -0
- package/commands/dgs/debug.md +164 -0
- package/commands/dgs/develop-idea.md +53 -0
- package/commands/dgs/discuss-idea.md +41 -0
- package/commands/dgs/discuss-phase.md +83 -0
- package/commands/dgs/execute-phase.md +41 -0
- package/commands/dgs/fast.md +38 -0
- package/commands/dgs/find-related-ideas.md +43 -0
- package/commands/dgs/health.md +28 -0
- package/commands/dgs/help.md +22 -0
- package/commands/dgs/import-spec.md +36 -0
- package/commands/dgs/init-product.md +28 -0
- package/commands/dgs/insert-phase.md +32 -0
- package/commands/dgs/join-discord.md +18 -0
- package/commands/dgs/list-docs.md +40 -0
- package/commands/dgs/list-ideas.md +42 -0
- package/commands/dgs/list-jobs.md +22 -0
- package/commands/dgs/list-phase-assumptions.md +46 -0
- package/commands/dgs/list-projects.md +57 -0
- package/commands/dgs/list-specs.md +40 -0
- package/commands/dgs/map-codebase.md +92 -0
- package/commands/dgs/new-milestone.md +44 -0
- package/commands/dgs/new-project.md +42 -0
- package/commands/dgs/node-repair.md +26 -0
- package/commands/dgs/overlap-check.md +20 -0
- package/commands/dgs/pause-work.md +38 -0
- package/commands/dgs/plan-milestone-gaps.md +34 -0
- package/commands/dgs/plan-phase.md +44 -0
- package/commands/dgs/progress.md +24 -0
- package/commands/dgs/quick.md +41 -0
- package/commands/dgs/reactivate-project.md +70 -0
- package/commands/dgs/reapply-patches.md +110 -0
- package/commands/dgs/refine-spec.md +38 -0
- package/commands/dgs/reject-idea.md +43 -0
- package/commands/dgs/remove-doc.md +44 -0
- package/commands/dgs/remove-phase.md +31 -0
- package/commands/dgs/remove-repo.md +69 -0
- package/commands/dgs/research-idea.md +43 -0
- package/commands/dgs/research-phase.md +189 -0
- package/commands/dgs/restore-idea.md +45 -0
- package/commands/dgs/resume-work.md +40 -0
- package/commands/dgs/rollback-job.md +24 -0
- package/commands/dgs/run-job.md +35 -0
- package/commands/dgs/search.md +40 -0
- package/commands/dgs/set-profile.md +34 -0
- package/commands/dgs/settings.md +38 -0
- package/commands/dgs/switch-project.md +58 -0
- package/commands/dgs/undo-consolidation.md +42 -0
- package/commands/dgs/update-idea.md +44 -0
- package/commands/dgs/update.md +37 -0
- package/commands/dgs/validate-phase.md +35 -0
- package/commands/dgs/verify-work.md +39 -0
- package/commands/dgs/write-spec.md +49 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-01-SUMMARY.md +84 -0
- package/deliver-great-systems/.planning/phases/09-backend-wiring-and-error-handling/09-02-SUMMARY.md +86 -0
- package/deliver-great-systems/.planning/phases/10-v1-to-v2-migration-flow/10-01-SUMMARY.md +85 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +1444 -0
- package/deliver-great-systems/bin/lib/auto-test.cjs +1365 -0
- package/deliver-great-systems/bin/lib/commands.cjs +570 -0
- package/deliver-great-systems/bin/lib/config.cjs +417 -0
- package/deliver-great-systems/bin/lib/conflict-agent.cjs +1063 -0
- package/deliver-great-systems/bin/lib/conflict-agent.test.cjs +554 -0
- package/deliver-great-systems/bin/lib/context.cjs +929 -0
- package/deliver-great-systems/bin/lib/context.test.cjs +693 -0
- package/deliver-great-systems/bin/lib/core.cjs +744 -0
- package/deliver-great-systems/bin/lib/core.test.cjs +822 -0
- package/deliver-great-systems/bin/lib/docs.cjs +919 -0
- package/deliver-great-systems/bin/lib/docs.test.cjs +211 -0
- package/deliver-great-systems/bin/lib/execution.cjs +705 -0
- package/deliver-great-systems/bin/lib/execution.test.cjs +1472 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +324 -0
- package/deliver-great-systems/bin/lib/ideas.cjs +1406 -0
- package/deliver-great-systems/bin/lib/ideas.test.cjs +1417 -0
- package/deliver-great-systems/bin/lib/identity.cjs +125 -0
- package/deliver-great-systems/bin/lib/init.cjs +1114 -0
- package/deliver-great-systems/bin/lib/init.test.cjs +1271 -0
- package/deliver-great-systems/bin/lib/jobs.cjs +2015 -0
- package/deliver-great-systems/bin/lib/jobs.test.cjs +2619 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.cjs +654 -0
- package/deliver-great-systems/bin/lib/merge-conflicts.test.cjs +370 -0
- package/deliver-great-systems/bin/lib/migration.cjs +352 -0
- package/deliver-great-systems/bin/lib/migration.test.cjs +582 -0
- package/deliver-great-systems/bin/lib/milestone.cjs +243 -0
- package/deliver-great-systems/bin/lib/overlap.cjs +437 -0
- package/deliver-great-systems/bin/lib/overlap.test.cjs +747 -0
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +384 -0
- package/deliver-great-systems/bin/lib/paths.cjs +144 -0
- package/deliver-great-systems/bin/lib/paths.test.cjs +486 -0
- package/deliver-great-systems/bin/lib/phase.cjs +910 -0
- package/deliver-great-systems/bin/lib/projects.cjs +691 -0
- package/deliver-great-systems/bin/lib/projects.test.cjs +871 -0
- package/deliver-great-systems/bin/lib/repos.cjs +1432 -0
- package/deliver-great-systems/bin/lib/repos.test.cjs +1882 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +305 -0
- package/deliver-great-systems/bin/lib/search.cjs +570 -0
- package/deliver-great-systems/bin/lib/specs.cjs +1303 -0
- package/deliver-great-systems/bin/lib/state.cjs +893 -0
- package/deliver-great-systems/bin/lib/template.cjs +228 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +291 -0
- package/deliver-great-systems/bin/lib/verify.cjs +796 -0
- package/deliver-great-systems/references/checkpoints.md +776 -0
- package/deliver-great-systems/references/conflict-resolution.md +66 -0
- package/deliver-great-systems/references/context-tiers.md +166 -0
- package/deliver-great-systems/references/continuation-format.md +249 -0
- package/deliver-great-systems/references/decimal-phase-calculation.md +67 -0
- package/deliver-great-systems/references/git-integration.md +250 -0
- package/deliver-great-systems/references/git-planning-commit.md +40 -0
- package/deliver-great-systems/references/model-profile-resolution.md +36 -0
- package/deliver-great-systems/references/model-profiles.md +95 -0
- package/deliver-great-systems/references/phase-argument-parsing.md +61 -0
- package/deliver-great-systems/references/planning-config.md +224 -0
- package/deliver-great-systems/references/questioning.md +162 -0
- package/deliver-great-systems/references/spec-review-loop.md +177 -0
- package/deliver-great-systems/references/tdd.md +265 -0
- package/deliver-great-systems/references/ui-brand.md +160 -0
- package/deliver-great-systems/references/verification-patterns.md +612 -0
- package/deliver-great-systems/templates/DEBUG.md +166 -0
- package/deliver-great-systems/templates/UAT.md +251 -0
- package/deliver-great-systems/templates/VALIDATION.md +95 -0
- package/deliver-great-systems/templates/claude-md.md +74 -0
- package/deliver-great-systems/templates/codebase/architecture.md +257 -0
- package/deliver-great-systems/templates/codebase/concerns.md +312 -0
- package/deliver-great-systems/templates/codebase/conventions.md +309 -0
- package/deliver-great-systems/templates/codebase/integrations.md +282 -0
- package/deliver-great-systems/templates/codebase/stack.md +188 -0
- package/deliver-great-systems/templates/codebase/structure.md +287 -0
- package/deliver-great-systems/templates/codebase/testing.md +482 -0
- package/deliver-great-systems/templates/config.json +38 -0
- package/deliver-great-systems/templates/context.md +354 -0
- package/deliver-great-systems/templates/continue-here.md +80 -0
- package/deliver-great-systems/templates/debug-subagent-prompt.md +93 -0
- package/deliver-great-systems/templates/discovery.md +148 -0
- package/deliver-great-systems/templates/milestone-archive.md +125 -0
- package/deliver-great-systems/templates/milestone.md +117 -0
- package/deliver-great-systems/templates/phase-prompt.md +615 -0
- package/deliver-great-systems/templates/planner-subagent-prompt.md +119 -0
- package/deliver-great-systems/templates/project.md +186 -0
- package/deliver-great-systems/templates/requirements.md +233 -0
- package/deliver-great-systems/templates/research-project/ARCHITECTURE.md +206 -0
- package/deliver-great-systems/templates/research-project/FEATURES.md +149 -0
- package/deliver-great-systems/templates/research-project/PITFALLS.md +202 -0
- package/deliver-great-systems/templates/research-project/STACK.md +122 -0
- package/deliver-great-systems/templates/research-project/SUMMARY.md +172 -0
- package/deliver-great-systems/templates/research.md +554 -0
- package/deliver-great-systems/templates/retrospective.md +54 -0
- package/deliver-great-systems/templates/roadmap.md +204 -0
- package/deliver-great-systems/templates/state.md +178 -0
- package/deliver-great-systems/templates/summary-complex.md +59 -0
- package/deliver-great-systems/templates/summary-minimal.md +41 -0
- package/deliver-great-systems/templates/summary-standard.md +48 -0
- package/deliver-great-systems/templates/summary.md +253 -0
- package/deliver-great-systems/templates/user-setup.md +313 -0
- package/deliver-great-systems/templates/verification-report.md +324 -0
- package/deliver-great-systems/workflows/add-doc.md +151 -0
- package/deliver-great-systems/workflows/add-idea.md +96 -0
- package/deliver-great-systems/workflows/add-phase.md +120 -0
- package/deliver-great-systems/workflows/add-tests.md +359 -0
- package/deliver-great-systems/workflows/add-todo.md +162 -0
- package/deliver-great-systems/workflows/approve-spec.md +194 -0
- package/deliver-great-systems/workflows/audit-milestone.md +364 -0
- package/deliver-great-systems/workflows/audit-phase.md +462 -0
- package/deliver-great-systems/workflows/cancel-job.md +108 -0
- package/deliver-great-systems/workflows/check-todos.md +181 -0
- package/deliver-great-systems/workflows/cleanup.md +247 -0
- package/deliver-great-systems/workflows/codereview.md +526 -0
- package/deliver-great-systems/workflows/complete-milestone.md +1298 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +365 -0
- package/deliver-great-systems/workflows/create-milestone-job.md +177 -0
- package/deliver-great-systems/workflows/develop-idea.md +544 -0
- package/deliver-great-systems/workflows/diagnose-issues.md +231 -0
- package/deliver-great-systems/workflows/discovery-phase.md +301 -0
- package/deliver-great-systems/workflows/discuss-idea.md +263 -0
- package/deliver-great-systems/workflows/discuss-phase.md +733 -0
- package/deliver-great-systems/workflows/execute-phase.md +571 -0
- package/deliver-great-systems/workflows/execute-plan.md +592 -0
- package/deliver-great-systems/workflows/find-related-ideas.md +271 -0
- package/deliver-great-systems/workflows/health.md +173 -0
- package/deliver-great-systems/workflows/help.md +997 -0
- package/deliver-great-systems/workflows/import-spec.md +381 -0
- package/deliver-great-systems/workflows/init-product.md +767 -0
- package/deliver-great-systems/workflows/insert-phase.md +138 -0
- package/deliver-great-systems/workflows/list-docs.md +119 -0
- package/deliver-great-systems/workflows/list-ideas.md +154 -0
- package/deliver-great-systems/workflows/list-jobs.md +89 -0
- package/deliver-great-systems/workflows/list-phase-assumptions.md +192 -0
- package/deliver-great-systems/workflows/list-specs.md +101 -0
- package/deliver-great-systems/workflows/map-codebase.md +621 -0
- package/deliver-great-systems/workflows/new-milestone.md +591 -0
- package/deliver-great-systems/workflows/new-project.md +1113 -0
- package/deliver-great-systems/workflows/node-repair.md +94 -0
- package/deliver-great-systems/workflows/overlap-check.md +86 -0
- package/deliver-great-systems/workflows/pause-work.md +134 -0
- package/deliver-great-systems/workflows/plan-milestone-gaps.md +306 -0
- package/deliver-great-systems/workflows/plan-phase.md +698 -0
- package/deliver-great-systems/workflows/progress.md +386 -0
- package/deliver-great-systems/workflows/quick.md +845 -0
- package/deliver-great-systems/workflows/refine-spec.md +275 -0
- package/deliver-great-systems/workflows/reject-idea.md +109 -0
- package/deliver-great-systems/workflows/remove-doc.md +117 -0
- package/deliver-great-systems/workflows/remove-phase.md +163 -0
- package/deliver-great-systems/workflows/research-idea.md +325 -0
- package/deliver-great-systems/workflows/research-phase.md +81 -0
- package/deliver-great-systems/workflows/restore-idea.md +101 -0
- package/deliver-great-systems/workflows/resume-project.md +311 -0
- package/deliver-great-systems/workflows/rollback-job.md +130 -0
- package/deliver-great-systems/workflows/run-job.md +498 -0
- package/deliver-great-systems/workflows/search.md +130 -0
- package/deliver-great-systems/workflows/set-profile.md +83 -0
- package/deliver-great-systems/workflows/settings.md +470 -0
- package/deliver-great-systems/workflows/transition.md +563 -0
- package/deliver-great-systems/workflows/undo-consolidation.md +155 -0
- package/deliver-great-systems/workflows/update-idea.md +157 -0
- package/deliver-great-systems/workflows/update.md +242 -0
- package/deliver-great-systems/workflows/validate-phase.md +177 -0
- package/deliver-great-systems/workflows/verify-phase.md +253 -0
- package/deliver-great-systems/workflows/verify-work.md +671 -0
- package/deliver-great-systems/workflows/write-spec.md +450 -0
- package/hooks/dist/dgs-check-update.js +62 -0
- package/hooks/dist/dgs-context-monitor.js +141 -0
- package/hooks/dist/dgs-statusline.js +115 -0
- package/package.json +60 -0
- package/scripts/build-hooks.js +43 -0
|
@@ -0,0 +1,2619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for jobs.cjs — Job file parse, update-step, move, find, header update,
|
|
3
|
+
* step insertion, and gap-fix operations
|
|
4
|
+
*
|
|
5
|
+
* Covers parseJobFile, updateJobStep, moveJobFile, findJobFile, updateJobHeader,
|
|
6
|
+
* insertJobSteps, buildGapFixSteps, insertGapFixSection, and cmd* CLI wrappers.
|
|
7
|
+
* Uses Node.js built-in test runner (node:test) and assert (node:assert).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
11
|
+
const assert = require('node:assert/strict');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { createFixture, createTempProject } = require('./test-helpers.cjs');
|
|
15
|
+
const { resetPaths } = require('./paths.cjs');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
parseJobFile, updateJobStep, moveJobFile,
|
|
19
|
+
generateMilestoneSteps, buildJobFileContent,
|
|
20
|
+
cmdJobsCreateMilestone, cmdJobsMilestonePreview,
|
|
21
|
+
findJobFile, updateJobHeader, insertJobSteps,
|
|
22
|
+
buildGapFixSteps, insertGapFixSection,
|
|
23
|
+
listJobs, cancelJob, recordStartShas, rollbackJob,
|
|
24
|
+
healthCheck, dryRunPreview, generateJobSummary,
|
|
25
|
+
} = require('./jobs.cjs');
|
|
26
|
+
|
|
27
|
+
// ─── Sample Job File Content ────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const WELL_FORMED_JOB = `# Milestone Job: v6.0
|
|
30
|
+
|
|
31
|
+
**Version:** v6.0
|
|
32
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
33
|
+
**Status:** in-progress
|
|
34
|
+
**Check:** true
|
|
35
|
+
|
|
36
|
+
## Steps
|
|
37
|
+
|
|
38
|
+
- [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
|
|
39
|
+
- [>] \`/dgs:plan-phase 42\` \u2014 started 2026-03-02T14:00:00Z
|
|
40
|
+
- [ ] \`/dgs:execute-phase 41\`
|
|
41
|
+
- [!] \`/dgs:plan-phase 43\` \u2014 failed 2026-03-02T15:00:00Z: Planning inconclusive after retry
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const ALL_COMPLETED_JOB = `# Milestone Job: v5.0
|
|
45
|
+
|
|
46
|
+
**Version:** v5.0
|
|
47
|
+
**Created:** 2026-03-01T08:00:00Z
|
|
48
|
+
**Status:** completed
|
|
49
|
+
**Check:** false
|
|
50
|
+
|
|
51
|
+
## Steps
|
|
52
|
+
|
|
53
|
+
- [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-01T09:00:00Z
|
|
54
|
+
- [x] \`/dgs:execute-phase 41\` \u2014 completed 2026-03-01T10:00:00Z
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const EMPTY_STEPS_JOB = `# Milestone Job: v7.0
|
|
58
|
+
|
|
59
|
+
**Version:** v7.0
|
|
60
|
+
**Created:** 2026-04-01T12:00:00Z
|
|
61
|
+
**Status:** pending
|
|
62
|
+
**Check:** true
|
|
63
|
+
|
|
64
|
+
## Steps
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const MISSING_VERSION_JOB = `# Milestone Job: v8.0
|
|
69
|
+
|
|
70
|
+
**Created:** 2026-04-01T12:00:00Z
|
|
71
|
+
**Status:** pending
|
|
72
|
+
**Check:** true
|
|
73
|
+
|
|
74
|
+
## Steps
|
|
75
|
+
|
|
76
|
+
- [ ] \`/dgs:plan-phase 50\`
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const MISSING_CREATED_JOB = `# Milestone Job: v8.0
|
|
80
|
+
|
|
81
|
+
**Version:** v8.0
|
|
82
|
+
**Status:** pending
|
|
83
|
+
**Check:** true
|
|
84
|
+
|
|
85
|
+
## Steps
|
|
86
|
+
|
|
87
|
+
- [ ] \`/dgs:plan-phase 50\`
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const MISSING_STATUS_JOB = `# Milestone Job: v8.0
|
|
91
|
+
|
|
92
|
+
**Version:** v8.0
|
|
93
|
+
**Created:** 2026-04-01T12:00:00Z
|
|
94
|
+
**Check:** true
|
|
95
|
+
|
|
96
|
+
## Steps
|
|
97
|
+
|
|
98
|
+
- [ ] \`/dgs:plan-phase 50\`
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const MISSING_CHECK_JOB = `# Milestone Job: v8.0
|
|
102
|
+
|
|
103
|
+
**Version:** v8.0
|
|
104
|
+
**Created:** 2026-04-01T12:00:00Z
|
|
105
|
+
**Status:** pending
|
|
106
|
+
|
|
107
|
+
## Steps
|
|
108
|
+
|
|
109
|
+
- [ ] \`/dgs:plan-phase 50\`
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const UNRECOGNIZED_COMMAND_JOB = `# Milestone Job: v6.0
|
|
113
|
+
|
|
114
|
+
**Version:** v6.0
|
|
115
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
116
|
+
**Status:** pending
|
|
117
|
+
**Check:** true
|
|
118
|
+
|
|
119
|
+
## Steps
|
|
120
|
+
|
|
121
|
+
- [ ] \`/dgs:plan-phase 41\`
|
|
122
|
+
- [ ] \`/dgs:unknown-cmd 42\`
|
|
123
|
+
- [ ] \`/dgs:execute-phase 41\`
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
const AUDIT_AND_COMPLETE_JOB = `# Milestone Job: v6.0
|
|
127
|
+
|
|
128
|
+
**Version:** v6.0
|
|
129
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
130
|
+
**Status:** pending
|
|
131
|
+
**Check:** true
|
|
132
|
+
|
|
133
|
+
## Steps
|
|
134
|
+
|
|
135
|
+
- [ ] \`/dgs:audit-milestone v6\`
|
|
136
|
+
- [ ] \`/dgs:complete-milestone v6\`
|
|
137
|
+
- [ ] \`/dgs:plan-milestone-gaps\`
|
|
138
|
+
- [ ] \`/dgs:discuss-phase 41\`
|
|
139
|
+
- [ ] \`/dgs:research-phase 42\`
|
|
140
|
+
- [ ] \`/dgs:verify-phase 43\`
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const UAT_WITH_HUMAN_NEEDED = `---
|
|
144
|
+
mode: auto-test
|
|
145
|
+
ai_verified: true
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
# UAT: Phase 50
|
|
149
|
+
|
|
150
|
+
## Tests
|
|
151
|
+
|
|
152
|
+
### 1. Build compiles successfully
|
|
153
|
+
expected: Build exits with code 0
|
|
154
|
+
result: pass
|
|
155
|
+
command: npm run build
|
|
156
|
+
|
|
157
|
+
### 2. Dashboard renders correctly
|
|
158
|
+
expected: Dashboard shows 3 cards in grid layout
|
|
159
|
+
result: human_needed
|
|
160
|
+
source: 50-01-PLAN.md
|
|
161
|
+
|
|
162
|
+
### 3. Login form accessible
|
|
163
|
+
expected: Screen reader announces form fields
|
|
164
|
+
result: human_needed
|
|
165
|
+
source: VALIDATION.md
|
|
166
|
+
|
|
167
|
+
## Summary
|
|
168
|
+
total: 3
|
|
169
|
+
passed: 1
|
|
170
|
+
issues: 0
|
|
171
|
+
human_needed: 2
|
|
172
|
+
pending: 0
|
|
173
|
+
skipped: 0
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const UAT_ALL_PASSED = `---
|
|
177
|
+
mode: auto-test
|
|
178
|
+
ai_verified: true
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
# UAT: Phase 50
|
|
182
|
+
|
|
183
|
+
## Tests
|
|
184
|
+
|
|
185
|
+
### 1. Build compiles successfully
|
|
186
|
+
expected: Build exits with code 0
|
|
187
|
+
result: pass
|
|
188
|
+
command: npm run build
|
|
189
|
+
|
|
190
|
+
### 2. Dashboard renders correctly
|
|
191
|
+
expected: Dashboard shows 3 cards in grid layout
|
|
192
|
+
result: pass
|
|
193
|
+
command: node -e "console.log('ok')"
|
|
194
|
+
|
|
195
|
+
## Summary
|
|
196
|
+
total: 2
|
|
197
|
+
passed: 2
|
|
198
|
+
issues: 0
|
|
199
|
+
human_needed: 0
|
|
200
|
+
pending: 0
|
|
201
|
+
skipped: 0
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
const UAT_MANUAL_WITH_HUMAN_NEEDED = `---
|
|
205
|
+
ai_verified: false
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
# UAT: Phase 50
|
|
209
|
+
|
|
210
|
+
## Tests
|
|
211
|
+
|
|
212
|
+
### 1. Visual check
|
|
213
|
+
expected: Looks correct
|
|
214
|
+
result: human_needed
|
|
215
|
+
source: manual-check
|
|
216
|
+
|
|
217
|
+
## Summary
|
|
218
|
+
total: 1
|
|
219
|
+
passed: 0
|
|
220
|
+
issues: 0
|
|
221
|
+
human_needed: 1
|
|
222
|
+
pending: 0
|
|
223
|
+
skipped: 0
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const ROADMAP_WITH_PHASE_50 = `# Roadmap
|
|
227
|
+
|
|
228
|
+
## v6.0 Auto-Verify (In Progress)
|
|
229
|
+
|
|
230
|
+
Phases 50-50
|
|
231
|
+
|
|
232
|
+
### Phase 50: Test Phase
|
|
233
|
+
|
|
234
|
+
- [ ] **Phase 50: Test Phase** - Testing
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
const COMPLETED_JOB_V6 = `# Milestone Job: v6.0
|
|
238
|
+
|
|
239
|
+
**Version:** v6.0
|
|
240
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
241
|
+
**Status:** completed
|
|
242
|
+
**Check:** true
|
|
243
|
+
|
|
244
|
+
## Steps
|
|
245
|
+
|
|
246
|
+
- [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T14:30:00Z
|
|
247
|
+
- [x] \`/dgs:execute-phase 50\` \u2014 completed 2026-03-02T15:00:00Z
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
const WELL_FORMED_JOB_WITH_CREATED_BY = `# Milestone Job: v6.0
|
|
251
|
+
|
|
252
|
+
**Version:** v6.0
|
|
253
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
254
|
+
**Created_by:** Adrian <adrian@example.com>
|
|
255
|
+
**Status:** in-progress
|
|
256
|
+
**Check:** true
|
|
257
|
+
|
|
258
|
+
## Steps
|
|
259
|
+
|
|
260
|
+
- [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
|
|
261
|
+
- [ ] \`/dgs:execute-phase 41\`
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
// ─── parseJobFile Tests ─────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
describe('jobs', () => {
|
|
267
|
+
|
|
268
|
+
describe('parseJobFile', () => {
|
|
269
|
+
let fixture;
|
|
270
|
+
|
|
271
|
+
afterEach(() => {
|
|
272
|
+
if (fixture) fixture.cleanup();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('parses well-formed file with mixed statuses', () => {
|
|
276
|
+
fixture = createFixture({
|
|
277
|
+
'job-v6.md': WELL_FORMED_JOB,
|
|
278
|
+
});
|
|
279
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job-v6.md'));
|
|
280
|
+
|
|
281
|
+
assert.equal(result.version, 'v6.0');
|
|
282
|
+
assert.equal(result.created, '2026-03-02T10:00:00Z');
|
|
283
|
+
assert.equal(result.status, 'in-progress');
|
|
284
|
+
assert.equal(result.check, true);
|
|
285
|
+
assert.equal(result.stepCount, 4);
|
|
286
|
+
assert.equal(result.completedCount, 1);
|
|
287
|
+
assert.equal(result.failedCount, 1);
|
|
288
|
+
assert.equal(result.inProgressCount, 1);
|
|
289
|
+
assert.equal(result.nextStepIndex, 2);
|
|
290
|
+
assert.equal(result.progress, 25);
|
|
291
|
+
|
|
292
|
+
// Step 0: completed
|
|
293
|
+
assert.equal(result.steps[0].index, 0);
|
|
294
|
+
assert.equal(result.steps[0].status, 'completed');
|
|
295
|
+
assert.equal(result.steps[0].command, 'plan-phase');
|
|
296
|
+
assert.equal(result.steps[0].args, '41');
|
|
297
|
+
assert.equal(result.steps[0].raw, '/dgs:plan-phase 41');
|
|
298
|
+
assert.equal(result.steps[0].timestamp, '2026-03-02T14:30:00Z');
|
|
299
|
+
assert.equal(result.steps[0].error, null);
|
|
300
|
+
|
|
301
|
+
// Step 1: in-progress
|
|
302
|
+
assert.equal(result.steps[1].index, 1);
|
|
303
|
+
assert.equal(result.steps[1].status, 'in-progress');
|
|
304
|
+
assert.equal(result.steps[1].command, 'plan-phase');
|
|
305
|
+
assert.equal(result.steps[1].args, '42');
|
|
306
|
+
assert.equal(result.steps[1].timestamp, '2026-03-02T14:00:00Z');
|
|
307
|
+
|
|
308
|
+
// Step 2: pending
|
|
309
|
+
assert.equal(result.steps[2].index, 2);
|
|
310
|
+
assert.equal(result.steps[2].status, 'pending');
|
|
311
|
+
assert.equal(result.steps[2].command, 'execute-phase');
|
|
312
|
+
assert.equal(result.steps[2].args, '41');
|
|
313
|
+
assert.equal(result.steps[2].timestamp, null);
|
|
314
|
+
|
|
315
|
+
// Step 3: failed with error
|
|
316
|
+
assert.equal(result.steps[3].index, 3);
|
|
317
|
+
assert.equal(result.steps[3].status, 'failed');
|
|
318
|
+
assert.equal(result.steps[3].command, 'plan-phase');
|
|
319
|
+
assert.equal(result.steps[3].args, '43');
|
|
320
|
+
assert.equal(result.steps[3].timestamp, '2026-03-02T15:00:00Z');
|
|
321
|
+
assert.equal(result.steps[3].error, 'Planning inconclusive after retry');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns nextStepIndex null and progress 100 when all steps completed', () => {
|
|
325
|
+
fixture = createFixture({
|
|
326
|
+
'job-v5.md': ALL_COMPLETED_JOB,
|
|
327
|
+
});
|
|
328
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job-v5.md'));
|
|
329
|
+
|
|
330
|
+
assert.equal(result.version, 'v5.0');
|
|
331
|
+
assert.equal(result.status, 'completed');
|
|
332
|
+
assert.equal(result.check, false);
|
|
333
|
+
assert.equal(result.stepCount, 2);
|
|
334
|
+
assert.equal(result.completedCount, 2);
|
|
335
|
+
assert.equal(result.failedCount, 0);
|
|
336
|
+
assert.equal(result.inProgressCount, 0);
|
|
337
|
+
assert.equal(result.nextStepIndex, null);
|
|
338
|
+
assert.equal(result.progress, 100);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('handles empty steps section', () => {
|
|
342
|
+
fixture = createFixture({
|
|
343
|
+
'job-v7.md': EMPTY_STEPS_JOB,
|
|
344
|
+
});
|
|
345
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job-v7.md'));
|
|
346
|
+
|
|
347
|
+
assert.equal(result.stepCount, 0);
|
|
348
|
+
assert.equal(result.nextStepIndex, null);
|
|
349
|
+
assert.equal(result.progress, 100);
|
|
350
|
+
assert.deepEqual(result.steps, []);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('hard-fails on missing Version field', () => {
|
|
354
|
+
fixture = createFixture({
|
|
355
|
+
'job-bad.md': MISSING_VERSION_JOB,
|
|
356
|
+
});
|
|
357
|
+
assert.throws(
|
|
358
|
+
() => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
|
|
359
|
+
(err) => err.message.includes('Version')
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('hard-fails on missing Created field', () => {
|
|
364
|
+
fixture = createFixture({
|
|
365
|
+
'job-bad.md': MISSING_CREATED_JOB,
|
|
366
|
+
});
|
|
367
|
+
assert.throws(
|
|
368
|
+
() => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
|
|
369
|
+
(err) => err.message.includes('Created')
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('hard-fails on missing Status field', () => {
|
|
374
|
+
fixture = createFixture({
|
|
375
|
+
'job-bad.md': MISSING_STATUS_JOB,
|
|
376
|
+
});
|
|
377
|
+
assert.throws(
|
|
378
|
+
() => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
|
|
379
|
+
(err) => err.message.includes('Status')
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('hard-fails on missing Check field', () => {
|
|
384
|
+
fixture = createFixture({
|
|
385
|
+
'job-bad.md': MISSING_CHECK_JOB,
|
|
386
|
+
});
|
|
387
|
+
assert.throws(
|
|
388
|
+
() => parseJobFile(path.join(fixture.cwd, 'job-bad.md')),
|
|
389
|
+
(err) => err.message.includes('Check')
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('warns on unrecognized step commands without hard error', () => {
|
|
394
|
+
fixture = createFixture({
|
|
395
|
+
'job-warn.md': UNRECOGNIZED_COMMAND_JOB,
|
|
396
|
+
});
|
|
397
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job-warn.md'));
|
|
398
|
+
|
|
399
|
+
assert.equal(result.stepCount, 3);
|
|
400
|
+
// Step 0: recognized command, no warning
|
|
401
|
+
assert.equal(result.steps[0].command, 'plan-phase');
|
|
402
|
+
assert.equal(result.steps[0].warning, undefined);
|
|
403
|
+
// Step 1: unrecognized command, should have warning
|
|
404
|
+
assert.equal(result.steps[1].command, 'unknown-cmd');
|
|
405
|
+
assert.ok(result.steps[1].warning, 'Unrecognized command should have a warning');
|
|
406
|
+
// Step 2: recognized command, no warning
|
|
407
|
+
assert.equal(result.steps[2].command, 'execute-phase');
|
|
408
|
+
assert.equal(result.steps[2].warning, undefined);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('parses version from header field, not filename', () => {
|
|
412
|
+
fixture = createFixture({
|
|
413
|
+
'job-wrong-name.md': WELL_FORMED_JOB,
|
|
414
|
+
});
|
|
415
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job-wrong-name.md'));
|
|
416
|
+
assert.equal(result.version, 'v6.0');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('parses all known DGS commands correctly', () => {
|
|
420
|
+
fixture = createFixture({
|
|
421
|
+
'job-cmds.md': AUDIT_AND_COMPLETE_JOB,
|
|
422
|
+
});
|
|
423
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job-cmds.md'));
|
|
424
|
+
|
|
425
|
+
assert.equal(result.steps[0].command, 'audit-milestone');
|
|
426
|
+
assert.equal(result.steps[0].args, 'v6');
|
|
427
|
+
assert.equal(result.steps[1].command, 'complete-milestone');
|
|
428
|
+
assert.equal(result.steps[1].args, 'v6');
|
|
429
|
+
assert.equal(result.steps[2].command, 'plan-milestone-gaps');
|
|
430
|
+
assert.equal(result.steps[2].args, '');
|
|
431
|
+
assert.equal(result.steps[3].command, 'discuss-phase');
|
|
432
|
+
assert.equal(result.steps[3].args, '41');
|
|
433
|
+
assert.equal(result.steps[4].command, 'research-phase');
|
|
434
|
+
assert.equal(result.steps[4].args, '42');
|
|
435
|
+
assert.equal(result.steps[5].command, 'verify-phase');
|
|
436
|
+
assert.equal(result.steps[5].args, '43');
|
|
437
|
+
|
|
438
|
+
// All known commands should NOT have warnings
|
|
439
|
+
for (const step of result.steps) {
|
|
440
|
+
assert.equal(step.warning, undefined, `${step.command} should be recognized`);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('hard-fails on non-existent file', () => {
|
|
445
|
+
assert.throws(
|
|
446
|
+
() => parseJobFile('/tmp/nonexistent-job-file-xyz.md'),
|
|
447
|
+
(err) => err.message.includes('not found') || err.message.includes('ENOENT')
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// ─── updateJobStep Tests ──────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
describe('updateJobStep', () => {
|
|
455
|
+
let fixture;
|
|
456
|
+
|
|
457
|
+
afterEach(() => {
|
|
458
|
+
if (fixture) fixture.cleanup();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('marks step as completed with timestamp', () => {
|
|
462
|
+
fixture = createFixture({
|
|
463
|
+
'job.md': WELL_FORMED_JOB,
|
|
464
|
+
});
|
|
465
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
466
|
+
updateJobStep(filePath, 2, 'completed', { timestamp: '2026-03-02T16:00:00Z' });
|
|
467
|
+
|
|
468
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
469
|
+
assert.ok(content.includes('- [x] `/dgs:execute-phase 41` \u2014 completed 2026-03-02T16:00:00Z'));
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('marks step as failed with error message', () => {
|
|
473
|
+
fixture = createFixture({
|
|
474
|
+
'job.md': WELL_FORMED_JOB,
|
|
475
|
+
});
|
|
476
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
477
|
+
updateJobStep(filePath, 2, 'failed', {
|
|
478
|
+
timestamp: '2026-03-02T16:00:00Z',
|
|
479
|
+
error: 'Some error occurred',
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
483
|
+
assert.ok(content.includes('- [!] `/dgs:execute-phase 41` \u2014 failed 2026-03-02T16:00:00Z: Some error occurred'));
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('marks step as in-progress with timestamp', () => {
|
|
487
|
+
fixture = createFixture({
|
|
488
|
+
'job.md': WELL_FORMED_JOB,
|
|
489
|
+
});
|
|
490
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
491
|
+
updateJobStep(filePath, 2, 'in-progress', { timestamp: '2026-03-02T16:00:00Z' });
|
|
492
|
+
|
|
493
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
494
|
+
assert.ok(content.includes('- [>] `/dgs:execute-phase 41` \u2014 started 2026-03-02T16:00:00Z'));
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('truncates error messages longer than 120 characters', () => {
|
|
498
|
+
fixture = createFixture({
|
|
499
|
+
'job.md': WELL_FORMED_JOB,
|
|
500
|
+
});
|
|
501
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
502
|
+
const longError = 'A'.repeat(200);
|
|
503
|
+
updateJobStep(filePath, 2, 'failed', {
|
|
504
|
+
timestamp: '2026-03-02T16:00:00Z',
|
|
505
|
+
error: longError,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
509
|
+
// Error should be truncated to ~120 chars + "..."
|
|
510
|
+
const stepLine = content.split('\n').find(l => l.includes('execute-phase 41'));
|
|
511
|
+
assert.ok(stepLine, 'Step line should exist');
|
|
512
|
+
const errorPart = stepLine.split(': ').slice(1).join(': ');
|
|
513
|
+
// The error on the line should be truncated
|
|
514
|
+
assert.ok(errorPart.length <= 130, `Error should be truncated, got length ${errorPart.length}`);
|
|
515
|
+
assert.ok(errorPart.endsWith('...'), 'Truncated error should end with ...');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('hard-errors on negative step index', () => {
|
|
519
|
+
fixture = createFixture({
|
|
520
|
+
'job.md': WELL_FORMED_JOB,
|
|
521
|
+
});
|
|
522
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
523
|
+
assert.throws(
|
|
524
|
+
() => updateJobStep(filePath, -1, 'completed', { timestamp: '2026-03-02T16:00:00Z' }),
|
|
525
|
+
(err) => err.message.includes('out of range')
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('hard-errors on step index >= stepCount', () => {
|
|
530
|
+
fixture = createFixture({
|
|
531
|
+
'job.md': WELL_FORMED_JOB,
|
|
532
|
+
});
|
|
533
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
534
|
+
assert.throws(
|
|
535
|
+
() => updateJobStep(filePath, 10, 'completed', { timestamp: '2026-03-02T16:00:00Z' }),
|
|
536
|
+
(err) => err.message.includes('out of range')
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('preserves all other content (header, other steps) exactly', () => {
|
|
541
|
+
fixture = createFixture({
|
|
542
|
+
'job.md': WELL_FORMED_JOB,
|
|
543
|
+
});
|
|
544
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
545
|
+
const before = fs.readFileSync(filePath, 'utf-8');
|
|
546
|
+
|
|
547
|
+
updateJobStep(filePath, 2, 'completed', { timestamp: '2026-03-02T16:00:00Z' });
|
|
548
|
+
|
|
549
|
+
const after = fs.readFileSync(filePath, 'utf-8');
|
|
550
|
+
|
|
551
|
+
// Header should be preserved
|
|
552
|
+
assert.ok(after.includes('# Milestone Job: v6.0'));
|
|
553
|
+
assert.ok(after.includes('**Version:** v6.0'));
|
|
554
|
+
assert.ok(after.includes('**Created:** 2026-03-02T10:00:00Z'));
|
|
555
|
+
assert.ok(after.includes('**Status:** in-progress'));
|
|
556
|
+
assert.ok(after.includes('**Check:** true'));
|
|
557
|
+
|
|
558
|
+
// Other steps should be preserved exactly
|
|
559
|
+
assert.ok(after.includes('- [x] `/dgs:plan-phase 41` \u2014 completed 2026-03-02T14:30:00Z'));
|
|
560
|
+
assert.ok(after.includes('- [>] `/dgs:plan-phase 42` \u2014 started 2026-03-02T14:00:00Z'));
|
|
561
|
+
assert.ok(after.includes('- [!] `/dgs:plan-phase 43` \u2014 failed 2026-03-02T15:00:00Z: Planning inconclusive after retry'));
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('resets a previously completed step to pending', () => {
|
|
565
|
+
fixture = createFixture({
|
|
566
|
+
'job.md': WELL_FORMED_JOB,
|
|
567
|
+
});
|
|
568
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
569
|
+
updateJobStep(filePath, 0, 'pending');
|
|
570
|
+
|
|
571
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
572
|
+
assert.ok(content.includes('- [ ] `/dgs:plan-phase 41`'));
|
|
573
|
+
// Should not have any annotation after backtick close
|
|
574
|
+
const stepLine = content.split('\n').find(l => l.includes('plan-phase 41') && l.startsWith('- [ ]'));
|
|
575
|
+
assert.ok(stepLine, 'Pending step line should exist');
|
|
576
|
+
assert.ok(!stepLine.includes('\u2014'), 'Pending step should have no annotation');
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ─── moveJobFile Tests ────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
describe('moveJobFile', () => {
|
|
583
|
+
let fixture;
|
|
584
|
+
|
|
585
|
+
afterEach(() => {
|
|
586
|
+
if (fixture) fixture.cleanup();
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('moves file from source to target directory', () => {
|
|
590
|
+
fixture = createFixture({
|
|
591
|
+
'pending/job-v6.md': WELL_FORMED_JOB,
|
|
592
|
+
'in-progress/': null,
|
|
593
|
+
});
|
|
594
|
+
const src = path.join(fixture.cwd, 'pending', 'job-v6.md');
|
|
595
|
+
const targetDir = path.join(fixture.cwd, 'in-progress');
|
|
596
|
+
|
|
597
|
+
const result = moveJobFile(src, targetDir);
|
|
598
|
+
|
|
599
|
+
assert.equal(result.moved, true);
|
|
600
|
+
assert.ok(!fs.existsSync(src), 'Source file should be removed');
|
|
601
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'job-v6.md')), 'File should exist in target');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('auto-creates target directory if it does not exist', () => {
|
|
605
|
+
fixture = createFixture({
|
|
606
|
+
'pending/job-v6.md': WELL_FORMED_JOB,
|
|
607
|
+
});
|
|
608
|
+
const src = path.join(fixture.cwd, 'pending', 'job-v6.md');
|
|
609
|
+
const targetDir = path.join(fixture.cwd, 'completed');
|
|
610
|
+
|
|
611
|
+
const result = moveJobFile(src, targetDir);
|
|
612
|
+
|
|
613
|
+
assert.equal(result.moved, true);
|
|
614
|
+
assert.ok(fs.existsSync(targetDir), 'Target directory should have been created');
|
|
615
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'job-v6.md')), 'File should exist in target');
|
|
616
|
+
assert.ok(!fs.existsSync(src), 'Source file should be removed');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('returns no-op with warning when file already in target directory', () => {
|
|
620
|
+
fixture = createFixture({
|
|
621
|
+
'in-progress/job-v6.md': WELL_FORMED_JOB,
|
|
622
|
+
});
|
|
623
|
+
const src = path.join(fixture.cwd, 'in-progress', 'job-v6.md');
|
|
624
|
+
const targetDir = path.join(fixture.cwd, 'in-progress');
|
|
625
|
+
|
|
626
|
+
const result = moveJobFile(src, targetDir);
|
|
627
|
+
|
|
628
|
+
assert.equal(result.moved, false);
|
|
629
|
+
assert.ok(result.warning, 'Should return a warning for no-op');
|
|
630
|
+
assert.ok(fs.existsSync(src), 'File should still exist');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('returns from and to paths in result', () => {
|
|
634
|
+
fixture = createFixture({
|
|
635
|
+
'pending/job-v6.md': WELL_FORMED_JOB,
|
|
636
|
+
'completed/': null,
|
|
637
|
+
});
|
|
638
|
+
const src = path.join(fixture.cwd, 'pending', 'job-v6.md');
|
|
639
|
+
const targetDir = path.join(fixture.cwd, 'completed');
|
|
640
|
+
|
|
641
|
+
const result = moveJobFile(src, targetDir);
|
|
642
|
+
|
|
643
|
+
assert.equal(result.moved, true);
|
|
644
|
+
assert.equal(result.from, src);
|
|
645
|
+
assert.equal(result.to, path.join(targetDir, 'job-v6.md'));
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('handles deeply nested target directory creation', () => {
|
|
649
|
+
fixture = createFixture({
|
|
650
|
+
'job-v6.md': WELL_FORMED_JOB,
|
|
651
|
+
});
|
|
652
|
+
const src = path.join(fixture.cwd, 'job-v6.md');
|
|
653
|
+
const targetDir = path.join(fixture.cwd, 'a', 'b', 'c', 'completed');
|
|
654
|
+
|
|
655
|
+
const result = moveJobFile(src, targetDir);
|
|
656
|
+
|
|
657
|
+
assert.equal(result.moved, true);
|
|
658
|
+
assert.ok(fs.existsSync(path.join(targetDir, 'job-v6.md')));
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// ─── generateMilestoneSteps Tests ──────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
describe('generateMilestoneSteps', () => {
|
|
665
|
+
|
|
666
|
+
it('produces 4 steps for unplanned phase (no_directory)', () => {
|
|
667
|
+
const phases = [
|
|
668
|
+
{ number: '50', name: 'Job Creation', disk_status: 'no_directory', roadmap_complete: false },
|
|
669
|
+
];
|
|
670
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
671
|
+
|
|
672
|
+
assert.equal(steps.length, 4);
|
|
673
|
+
assert.equal(steps[0].command, 'map-codebase');
|
|
674
|
+
assert.equal(steps[0].args, '50 --auto');
|
|
675
|
+
assert.equal(steps[1].command, 'plan-phase');
|
|
676
|
+
assert.equal(steps[1].args, '50 --non-interactive');
|
|
677
|
+
assert.equal(steps[2].command, 'execute-phase');
|
|
678
|
+
assert.equal(steps[2].args, '50 --non-interactive');
|
|
679
|
+
assert.equal(steps[3].command, 'verify-work');
|
|
680
|
+
assert.equal(steps[3].args, '50 --auto-test');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('produces 4 steps for unplanned phase (empty)', () => {
|
|
684
|
+
const phases = [
|
|
685
|
+
{ number: '51', name: 'Job Execution', disk_status: 'empty', roadmap_complete: false },
|
|
686
|
+
];
|
|
687
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
688
|
+
|
|
689
|
+
assert.equal(steps.length, 4);
|
|
690
|
+
assert.equal(steps[0].command, 'map-codebase');
|
|
691
|
+
assert.equal(steps[1].command, 'plan-phase');
|
|
692
|
+
assert.equal(steps[2].command, 'execute-phase');
|
|
693
|
+
assert.equal(steps[3].command, 'verify-work');
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('produces 4 steps for discussed phase (context but no plan)', () => {
|
|
697
|
+
const phases = [
|
|
698
|
+
{ number: '52', name: 'Silent Mode', disk_status: 'discussed', roadmap_complete: false },
|
|
699
|
+
];
|
|
700
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
701
|
+
|
|
702
|
+
assert.equal(steps.length, 4);
|
|
703
|
+
assert.equal(steps[0].command, 'map-codebase');
|
|
704
|
+
assert.equal(steps[1].command, 'plan-phase');
|
|
705
|
+
assert.equal(steps[2].command, 'execute-phase');
|
|
706
|
+
assert.equal(steps[3].command, 'verify-work');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('produces 4 steps for researched phase', () => {
|
|
710
|
+
const phases = [
|
|
711
|
+
{ number: '53', name: 'Audit Integration', disk_status: 'researched', roadmap_complete: false },
|
|
712
|
+
];
|
|
713
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
714
|
+
|
|
715
|
+
assert.equal(steps.length, 4);
|
|
716
|
+
assert.equal(steps[0].command, 'map-codebase');
|
|
717
|
+
assert.equal(steps[1].command, 'plan-phase');
|
|
718
|
+
assert.equal(steps[2].command, 'execute-phase');
|
|
719
|
+
assert.equal(steps[3].command, 'verify-work');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('produces 2 steps for planned phase', () => {
|
|
723
|
+
const phases = [
|
|
724
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
725
|
+
];
|
|
726
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
727
|
+
|
|
728
|
+
assert.equal(steps.length, 2);
|
|
729
|
+
assert.equal(steps[0].command, 'execute-phase');
|
|
730
|
+
assert.equal(steps[0].args, '50 --non-interactive');
|
|
731
|
+
assert.equal(steps[1].command, 'verify-work');
|
|
732
|
+
assert.equal(steps[1].args, '50 --auto-test');
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('produces 2 steps for partially executed phase', () => {
|
|
736
|
+
const phases = [
|
|
737
|
+
{ number: '50', name: 'Job Creation', disk_status: 'partial', roadmap_complete: false },
|
|
738
|
+
];
|
|
739
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
740
|
+
|
|
741
|
+
assert.equal(steps.length, 2);
|
|
742
|
+
assert.equal(steps[0].command, 'execute-phase');
|
|
743
|
+
assert.equal(steps[1].command, 'verify-work');
|
|
744
|
+
assert.equal(steps[1].args, '50 --auto-test');
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('produces 0 steps for completed phase (disk_status complete)', () => {
|
|
748
|
+
const phases = [
|
|
749
|
+
{ number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
|
|
750
|
+
];
|
|
751
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
752
|
+
|
|
753
|
+
assert.equal(steps.length, 0);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it('produces 0 steps for roadmap_complete phase even if disk_status is not complete', () => {
|
|
757
|
+
const phases = [
|
|
758
|
+
{ number: '49', name: 'Job File Format', disk_status: 'planned', roadmap_complete: true },
|
|
759
|
+
];
|
|
760
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
761
|
+
|
|
762
|
+
assert.equal(steps.length, 0);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('handles mixed phases correctly (complete + unplanned + planned)', () => {
|
|
766
|
+
const phases = [
|
|
767
|
+
{ number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
|
|
768
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
769
|
+
{ number: '51', name: 'Job Execution', disk_status: 'no_directory', roadmap_complete: false },
|
|
770
|
+
];
|
|
771
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
772
|
+
|
|
773
|
+
// Phase 49: 0 steps (complete)
|
|
774
|
+
// Phase 50: 2 steps (planned -> execute + verify)
|
|
775
|
+
// Phase 51: 4 steps (unplanned -> map + plan + execute + verify)
|
|
776
|
+
assert.equal(steps.length, 6);
|
|
777
|
+
assert.equal(steps[0].command, 'execute-phase');
|
|
778
|
+
assert.equal(steps[0].args, '50 --non-interactive');
|
|
779
|
+
assert.equal(steps[1].command, 'verify-work');
|
|
780
|
+
assert.equal(steps[1].args, '50 --auto-test');
|
|
781
|
+
assert.equal(steps[2].command, 'map-codebase');
|
|
782
|
+
assert.equal(steps[2].args, '51 --auto');
|
|
783
|
+
assert.equal(steps[3].command, 'plan-phase');
|
|
784
|
+
assert.equal(steps[3].args, '51 --non-interactive');
|
|
785
|
+
assert.equal(steps[4].command, 'execute-phase');
|
|
786
|
+
assert.equal(steps[4].args, '51 --non-interactive');
|
|
787
|
+
assert.equal(steps[5].command, 'verify-work');
|
|
788
|
+
assert.equal(steps[5].args, '51 --auto-test');
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it('appends audit and complete steps when check=true', () => {
|
|
792
|
+
const phases = [
|
|
793
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
794
|
+
];
|
|
795
|
+
const steps = generateMilestoneSteps(phases, { check: true, version: 'v6.0' });
|
|
796
|
+
|
|
797
|
+
// 2 phase steps + 2 check steps = 4
|
|
798
|
+
assert.equal(steps.length, 4);
|
|
799
|
+
assert.equal(steps[2].command, 'audit-milestone');
|
|
800
|
+
assert.equal(steps[2].args, 'v6.0');
|
|
801
|
+
assert.equal(steps[3].command, 'complete-milestone');
|
|
802
|
+
assert.equal(steps[3].args, 'v6.0');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('does NOT append audit/complete steps when check=false', () => {
|
|
806
|
+
const phases = [
|
|
807
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
808
|
+
];
|
|
809
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
810
|
+
|
|
811
|
+
assert.equal(steps.length, 2);
|
|
812
|
+
// No audit/complete steps
|
|
813
|
+
assert.ok(!steps.some(s => s.command === 'audit-milestone'));
|
|
814
|
+
assert.ok(!steps.some(s => s.command === 'complete-milestone'));
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('returns only audit+complete when all phases complete and check=true', () => {
|
|
818
|
+
const phases = [
|
|
819
|
+
{ number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
|
|
820
|
+
{ number: '50', name: 'Job Creation', disk_status: 'complete', roadmap_complete: true },
|
|
821
|
+
];
|
|
822
|
+
const steps = generateMilestoneSteps(phases, { check: true, version: 'v6.0' });
|
|
823
|
+
|
|
824
|
+
assert.equal(steps.length, 2);
|
|
825
|
+
assert.equal(steps[0].command, 'audit-milestone');
|
|
826
|
+
assert.equal(steps[1].command, 'complete-milestone');
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('returns empty array when all phases complete and check=false', () => {
|
|
830
|
+
const phases = [
|
|
831
|
+
{ number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
|
|
832
|
+
{ number: '50', name: 'Job Creation', disk_status: 'complete', roadmap_complete: true },
|
|
833
|
+
];
|
|
834
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
835
|
+
|
|
836
|
+
assert.equal(steps.length, 0);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('handles decimal phase numbers in correct sequence order', () => {
|
|
840
|
+
const phases = [
|
|
841
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
842
|
+
{ number: '50.1', name: 'Hotfix', disk_status: 'no_directory', roadmap_complete: false },
|
|
843
|
+
{ number: '51', name: 'Job Execution', disk_status: 'planned', roadmap_complete: false },
|
|
844
|
+
];
|
|
845
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
846
|
+
|
|
847
|
+
// Phase 50: 2 steps (execute + verify), Phase 50.1: 4 steps (map + plan + execute + verify), Phase 51: 2 steps (execute + verify) = 8 total
|
|
848
|
+
assert.equal(steps.length, 8);
|
|
849
|
+
// First phase should be 50 (execute-phase)
|
|
850
|
+
assert.equal(steps[0].args, '50 --non-interactive');
|
|
851
|
+
// Then 50.1 (map-codebase at index 2)
|
|
852
|
+
assert.equal(steps[2].command, 'map-codebase');
|
|
853
|
+
assert.equal(steps[2].args, '50.1 --auto');
|
|
854
|
+
// Then 51 (execute-phase at index 6)
|
|
855
|
+
assert.equal(steps[6].args, '51 --non-interactive');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('includes correct flags on plan-phase (--non-interactive), execute-phase (--non-interactive), and map-codebase (--auto) steps', () => {
|
|
859
|
+
const phases = [
|
|
860
|
+
{ number: '50', name: 'Job Creation', disk_status: 'no_directory', roadmap_complete: false },
|
|
861
|
+
];
|
|
862
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
863
|
+
|
|
864
|
+
const mapStep = steps.find(s => s.command === 'map-codebase');
|
|
865
|
+
const planStep = steps.find(s => s.command === 'plan-phase');
|
|
866
|
+
const executeStep = steps.find(s => s.command === 'execute-phase');
|
|
867
|
+
const verifyStep = steps.find(s => s.command === 'verify-work');
|
|
868
|
+
|
|
869
|
+
assert.ok(mapStep.args.includes('--auto'), 'map-codebase should have --auto');
|
|
870
|
+
assert.ok(planStep.args.includes('--non-interactive'), 'plan-phase should have --non-interactive');
|
|
871
|
+
assert.ok(executeStep.args.includes('--non-interactive'), 'execute-phase should have --non-interactive');
|
|
872
|
+
assert.ok(verifyStep.args.includes('--auto-test'), 'verify-work should have --auto-test');
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('sorts phases by number numerically, not lexicographically', () => {
|
|
876
|
+
const phases = [
|
|
877
|
+
{ number: '51', name: 'Job Execution', disk_status: 'planned', roadmap_complete: false },
|
|
878
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
879
|
+
{ number: '49', name: 'Job File Format', disk_status: 'complete', roadmap_complete: true },
|
|
880
|
+
];
|
|
881
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
882
|
+
|
|
883
|
+
// Phase 49 complete (0 steps), Phase 50 planned (2 steps), Phase 51 planned (2 steps) = 4
|
|
884
|
+
assert.equal(steps.length, 4);
|
|
885
|
+
assert.equal(steps[0].args, '50 --non-interactive');
|
|
886
|
+
assert.equal(steps[1].args, '50 --auto-test');
|
|
887
|
+
assert.equal(steps[2].args, '51 --non-interactive');
|
|
888
|
+
assert.equal(steps[3].args, '51 --auto-test');
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('returns steps as objects with command and args properties', () => {
|
|
892
|
+
const phases = [
|
|
893
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
894
|
+
];
|
|
895
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
896
|
+
|
|
897
|
+
for (const step of steps) {
|
|
898
|
+
assert.ok(typeof step.command === 'string', 'step should have command string');
|
|
899
|
+
assert.ok(typeof step.args === 'string', 'step should have args string');
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('map-codebase step has --auto flag', () => {
|
|
904
|
+
const phases = [
|
|
905
|
+
{ number: '50', name: 'Job Creation', disk_status: 'no_directory', roadmap_complete: false },
|
|
906
|
+
{ number: '51', name: 'Job Execution', disk_status: 'empty', roadmap_complete: false },
|
|
907
|
+
];
|
|
908
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
909
|
+
|
|
910
|
+
const mapSteps = steps.filter(s => s.command === 'map-codebase');
|
|
911
|
+
assert.ok(mapSteps.length > 0, 'Should have map-codebase steps');
|
|
912
|
+
for (const step of mapSteps) {
|
|
913
|
+
assert.ok(step.args.includes('--auto'), `map-codebase step for ${step.args} should have --auto`);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('verify-work steps include --auto-test flag', () => {
|
|
918
|
+
const phases = [
|
|
919
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
920
|
+
{ number: '51', name: 'Job Execution', disk_status: 'no_directory', roadmap_complete: false },
|
|
921
|
+
];
|
|
922
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
923
|
+
|
|
924
|
+
const verifySteps = steps.filter(s => s.command === 'verify-work');
|
|
925
|
+
assert.ok(verifySteps.length > 0, 'Should have verify-work steps');
|
|
926
|
+
for (const step of verifySteps) {
|
|
927
|
+
assert.ok(step.args.includes('--auto-test'), `verify-work step for ${step.args} should have --auto-test`);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('no map-codebase for NEEDS_EXECUTION phases', () => {
|
|
932
|
+
const phases = [
|
|
933
|
+
{ number: '50', name: 'Job Creation', disk_status: 'planned', roadmap_complete: false },
|
|
934
|
+
{ number: '51', name: 'Job Execution', disk_status: 'partial', roadmap_complete: false },
|
|
935
|
+
];
|
|
936
|
+
const steps = generateMilestoneSteps(phases, { check: false, version: 'v6.0' });
|
|
937
|
+
|
|
938
|
+
const mapSteps = steps.filter(s => s.command === 'map-codebase');
|
|
939
|
+
assert.equal(mapSteps.length, 0, 'Planned and partial phases should NOT have map-codebase steps');
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
// ─── buildJobFileContent Tests ─────────────────────────────────────────────
|
|
944
|
+
|
|
945
|
+
describe('buildJobFileContent', () => {
|
|
946
|
+
|
|
947
|
+
it('produces valid markdown with header fields', () => {
|
|
948
|
+
const steps = [
|
|
949
|
+
{ command: 'plan-phase', args: '50 --non-interactive' },
|
|
950
|
+
{ command: 'execute-phase', args: '50 --non-interactive' },
|
|
951
|
+
{ command: 'verify-work', args: '50' },
|
|
952
|
+
];
|
|
953
|
+
const content = buildJobFileContent('v6.0', true, steps);
|
|
954
|
+
|
|
955
|
+
assert.ok(content.includes('# Milestone Job: v6.0'), 'Should have title');
|
|
956
|
+
assert.ok(content.includes('**Version:** v6.0'), 'Should have version');
|
|
957
|
+
assert.ok(content.includes('**Status:** pending'), 'Should have status');
|
|
958
|
+
assert.ok(content.includes('**Check:** true'), 'Should have check=true');
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('has Created field with ISO timestamp', () => {
|
|
962
|
+
const content = buildJobFileContent('v6.0', true, []);
|
|
963
|
+
const createdMatch = content.match(/\*\*Created:\*\*\s*(\S+)/);
|
|
964
|
+
assert.ok(createdMatch, 'Should have Created field');
|
|
965
|
+
// Verify ISO format: YYYY-MM-DDTHH:MM:SSZ
|
|
966
|
+
assert.ok(createdMatch[1].match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/), 'Created should be ISO timestamp');
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it('produces checkbox lines for each step', () => {
|
|
970
|
+
const steps = [
|
|
971
|
+
{ command: 'plan-phase', args: '50 --non-interactive' },
|
|
972
|
+
{ command: 'execute-phase', args: '50 --non-interactive' },
|
|
973
|
+
{ command: 'verify-work', args: '50' },
|
|
974
|
+
];
|
|
975
|
+
const content = buildJobFileContent('v6.0', true, steps);
|
|
976
|
+
|
|
977
|
+
assert.ok(content.includes('- [ ] `/dgs:plan-phase 50 --non-interactive`'), 'plan-phase step');
|
|
978
|
+
assert.ok(content.includes('- [ ] `/dgs:execute-phase 50 --non-interactive`'), 'execute-phase step');
|
|
979
|
+
assert.ok(content.includes('- [ ] `/dgs:verify-work 50`'), 'verify-work step');
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it('includes ## Steps section heading', () => {
|
|
983
|
+
const content = buildJobFileContent('v6.0', true, []);
|
|
984
|
+
assert.ok(content.includes('## Steps'), 'Should have Steps heading');
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('sets Check to false when check param is false', () => {
|
|
988
|
+
const content = buildJobFileContent('v6.0', false, []);
|
|
989
|
+
assert.ok(content.includes('**Check:** false'), 'Should have check=false');
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('round-trips through parseJobFile without errors', () => {
|
|
993
|
+
const steps = [
|
|
994
|
+
{ command: 'plan-phase', args: '50 --non-interactive' },
|
|
995
|
+
{ command: 'execute-phase', args: '50 --non-interactive' },
|
|
996
|
+
{ command: 'verify-work', args: '50' },
|
|
997
|
+
{ command: 'audit-milestone', args: 'v6.0' },
|
|
998
|
+
{ command: 'complete-milestone', args: 'v6.0' },
|
|
999
|
+
];
|
|
1000
|
+
const content = buildJobFileContent('v6.0', true, steps);
|
|
1001
|
+
|
|
1002
|
+
// Write to temp file, parse it back
|
|
1003
|
+
const fixture = createFixture({
|
|
1004
|
+
'job-roundtrip.md': content,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
const parsed = parseJobFile(path.join(fixture.cwd, 'job-roundtrip.md'));
|
|
1009
|
+
assert.equal(parsed.version, 'v6.0');
|
|
1010
|
+
assert.equal(parsed.check, true);
|
|
1011
|
+
assert.equal(parsed.status, 'pending');
|
|
1012
|
+
assert.equal(parsed.stepCount, 5);
|
|
1013
|
+
assert.equal(parsed.steps[0].command, 'plan-phase');
|
|
1014
|
+
assert.equal(parsed.steps[0].args, '50 --non-interactive');
|
|
1015
|
+
assert.equal(parsed.steps[4].command, 'complete-milestone');
|
|
1016
|
+
assert.equal(parsed.steps[4].args, 'v6.0');
|
|
1017
|
+
} finally {
|
|
1018
|
+
fixture.cleanup();
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it('ends with trailing newline', () => {
|
|
1023
|
+
const content = buildJobFileContent('v6.0', true, []);
|
|
1024
|
+
assert.ok(content.endsWith('\n'), 'Should end with newline');
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// ─── cmdJobsCreateMilestone Tests ──────────────────────────────────────────
|
|
1029
|
+
|
|
1030
|
+
describe('cmdJobsCreateMilestone', () => {
|
|
1031
|
+
let fixture;
|
|
1032
|
+
|
|
1033
|
+
afterEach(() => {
|
|
1034
|
+
if (fixture) fixture.cleanup();
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
const FIXTURE_ROADMAP = `# Roadmap
|
|
1038
|
+
|
|
1039
|
+
## Milestones
|
|
1040
|
+
|
|
1041
|
+
- v6.0 Job Orchestration -- Phases 49-55 (in progress)
|
|
1042
|
+
|
|
1043
|
+
## Phases
|
|
1044
|
+
|
|
1045
|
+
### v6.0 Job Orchestration (In Progress)
|
|
1046
|
+
|
|
1047
|
+
- [x] **Phase 49: Job File Format & Infrastructure** (completed 2026-03-03)
|
|
1048
|
+
- [ ] **Phase 50: Job Creation**
|
|
1049
|
+
- [ ] **Phase 51: Job Execution Core**
|
|
1050
|
+
|
|
1051
|
+
### Phase 49: Job File Format & Infrastructure
|
|
1052
|
+
**Goal**: Job files can be created, parsed, updated, and moved
|
|
1053
|
+
**Depends on**: Nothing
|
|
1054
|
+
**Plans:** 2/2 plans complete
|
|
1055
|
+
Plans:
|
|
1056
|
+
- [x] 49-01-PLAN.md
|
|
1057
|
+
- [x] 49-02-PLAN.md
|
|
1058
|
+
|
|
1059
|
+
### Phase 50: Job Creation
|
|
1060
|
+
**Goal**: Users can generate a milestone build job
|
|
1061
|
+
**Depends on**: Phase 49
|
|
1062
|
+
Plans:
|
|
1063
|
+
- [ ] 50-01-PLAN.md
|
|
1064
|
+
- [ ] 50-02-PLAN.md
|
|
1065
|
+
|
|
1066
|
+
### Phase 51: Job Execution Core
|
|
1067
|
+
**Goal**: Users can execute a milestone job
|
|
1068
|
+
**Depends on**: Phase 50
|
|
1069
|
+
`;
|
|
1070
|
+
|
|
1071
|
+
it('auto-detects active milestone version from ROADMAP.md', () => {
|
|
1072
|
+
fixture = createFixture({
|
|
1073
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1074
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1075
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1076
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1077
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1078
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1079
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
const result = cmdJobsCreateMilestone(fixture.cwd, null, true, false);
|
|
1083
|
+
assert.equal(result.version, 'v6.0');
|
|
1084
|
+
assert.equal(result.created, true);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
it('writes job file to .planning/jobs/pending/', () => {
|
|
1088
|
+
fixture = createFixture({
|
|
1089
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1090
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1091
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1092
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1093
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1094
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1095
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
const result = cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
|
|
1099
|
+
assert.ok(result.file.includes('pending'));
|
|
1100
|
+
assert.ok(result.file.includes('milestone-v6.0.md'));
|
|
1101
|
+
|
|
1102
|
+
const jobFilePath = path.join(fixture.cwd, result.file);
|
|
1103
|
+
assert.ok(fs.existsSync(jobFilePath), 'Job file should exist on disk');
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('auto-creates .planning/jobs/pending/ directory', () => {
|
|
1107
|
+
fixture = createFixture({
|
|
1108
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1109
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1110
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1111
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1112
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1113
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1114
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// Ensure pending dir does not exist yet
|
|
1118
|
+
const pendingDir = path.join(fixture.cwd, '.planning', 'jobs', 'pending');
|
|
1119
|
+
assert.ok(!fs.existsSync(pendingDir), 'pending dir should not exist before call');
|
|
1120
|
+
|
|
1121
|
+
cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
|
|
1122
|
+
assert.ok(fs.existsSync(pendingDir), 'pending dir should be auto-created');
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it('returns JSON with expected fields', () => {
|
|
1126
|
+
fixture = createFixture({
|
|
1127
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1128
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1129
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1130
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1131
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1132
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1133
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
const result = cmdJobsCreateMilestone(fixture.cwd, 'v6.0', true, false);
|
|
1137
|
+
assert.equal(result.created, true);
|
|
1138
|
+
assert.equal(result.version, 'v6.0');
|
|
1139
|
+
assert.ok(typeof result.file === 'string');
|
|
1140
|
+
assert.ok(typeof result.step_count === 'number');
|
|
1141
|
+
assert.ok(typeof result.phase_count === 'number');
|
|
1142
|
+
assert.ok(Array.isArray(result.steps_preview));
|
|
1143
|
+
assert.ok(result.step_count > 0);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
it('throws error when no ROADMAP.md exists', () => {
|
|
1147
|
+
fixture = createFixture({
|
|
1148
|
+
'.planning/phases/': null,
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
assert.throws(
|
|
1152
|
+
() => cmdJobsCreateMilestone(fixture.cwd, null, true, false),
|
|
1153
|
+
(err) => err.message.includes('ROADMAP.md')
|
|
1154
|
+
);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it('throws error when specified version not found in ROADMAP', () => {
|
|
1158
|
+
fixture = createFixture({
|
|
1159
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1160
|
+
'.planning/phases/': null,
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
assert.throws(
|
|
1164
|
+
() => cmdJobsCreateMilestone(fixture.cwd, 'v99.0', true, false),
|
|
1165
|
+
(err) => err.message.includes('v99.0') || err.message.includes('not found')
|
|
1166
|
+
);
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// ─── cmdJobsMilestonePreview Tests ─────────────────────────────────────────
|
|
1171
|
+
|
|
1172
|
+
describe('cmdJobsMilestonePreview', () => {
|
|
1173
|
+
let fixture;
|
|
1174
|
+
|
|
1175
|
+
afterEach(() => {
|
|
1176
|
+
if (fixture) fixture.cleanup();
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
const FIXTURE_ROADMAP = `# Roadmap
|
|
1180
|
+
|
|
1181
|
+
## Milestones
|
|
1182
|
+
|
|
1183
|
+
- v6.0 Job Orchestration -- Phases 49-55 (in progress)
|
|
1184
|
+
|
|
1185
|
+
## Phases
|
|
1186
|
+
|
|
1187
|
+
### v6.0 Job Orchestration (In Progress)
|
|
1188
|
+
|
|
1189
|
+
- [x] **Phase 49: Job File Format & Infrastructure** (completed 2026-03-03)
|
|
1190
|
+
- [ ] **Phase 50: Job Creation**
|
|
1191
|
+
- [ ] **Phase 51: Job Execution Core**
|
|
1192
|
+
|
|
1193
|
+
### Phase 49: Job File Format & Infrastructure
|
|
1194
|
+
**Goal**: Job files can be created, parsed, updated, and moved
|
|
1195
|
+
**Depends on**: Nothing
|
|
1196
|
+
**Plans:** 2/2 plans complete
|
|
1197
|
+
Plans:
|
|
1198
|
+
- [x] 49-01-PLAN.md
|
|
1199
|
+
- [x] 49-02-PLAN.md
|
|
1200
|
+
|
|
1201
|
+
### Phase 50: Job Creation
|
|
1202
|
+
**Goal**: Users can generate a milestone build job
|
|
1203
|
+
**Depends on**: Phase 49
|
|
1204
|
+
Plans:
|
|
1205
|
+
- [ ] 50-01-PLAN.md
|
|
1206
|
+
- [ ] 50-02-PLAN.md
|
|
1207
|
+
|
|
1208
|
+
### Phase 51: Job Execution Core
|
|
1209
|
+
**Goal**: Users can execute a milestone job
|
|
1210
|
+
**Depends on**: Phase 50
|
|
1211
|
+
`;
|
|
1212
|
+
|
|
1213
|
+
it('returns preview JSON with expected fields', () => {
|
|
1214
|
+
fixture = createFixture({
|
|
1215
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1216
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1217
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1218
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1219
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1220
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1221
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
|
|
1225
|
+
assert.equal(result.preview, true);
|
|
1226
|
+
assert.equal(result.version, 'v6.0');
|
|
1227
|
+
assert.equal(result.check, true);
|
|
1228
|
+
assert.ok(typeof result.step_count === 'number');
|
|
1229
|
+
assert.ok(typeof result.phase_count === 'number');
|
|
1230
|
+
assert.ok(Array.isArray(result.steps_preview));
|
|
1231
|
+
assert.ok(typeof result.content === 'string');
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it('does NOT write any file to disk', () => {
|
|
1235
|
+
fixture = createFixture({
|
|
1236
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1237
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1238
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1239
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1240
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1241
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1242
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
|
|
1246
|
+
|
|
1247
|
+
const pendingDir = path.join(fixture.cwd, '.planning', 'jobs', 'pending');
|
|
1248
|
+
assert.ok(!fs.existsSync(pendingDir), 'No jobs directory should be created for preview');
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it('steps_preview is array of step command strings', () => {
|
|
1252
|
+
fixture = createFixture({
|
|
1253
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1254
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1255
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1256
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1257
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1258
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1259
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
|
|
1263
|
+
for (const preview of result.steps_preview) {
|
|
1264
|
+
assert.ok(typeof preview === 'string', 'Each preview should be a string');
|
|
1265
|
+
assert.ok(preview.startsWith('/dgs:'), 'Each preview should start with /dgs:');
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
it('content is the full markdown that would be written', () => {
|
|
1270
|
+
fixture = createFixture({
|
|
1271
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1272
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1273
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1274
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1275
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1276
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1277
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
|
|
1281
|
+
assert.ok(result.content.includes('# Milestone Job: v6.0'));
|
|
1282
|
+
assert.ok(result.content.includes('## Steps'));
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
it('phase_count counts distinct phases with steps', () => {
|
|
1286
|
+
fixture = createFixture({
|
|
1287
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1288
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1289
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1290
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1291
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1292
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1293
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
|
|
1297
|
+
// Phase 49 complete (0 steps), Phase 50 planned (2 steps), Phase 51 no_directory (3 steps) = 2 phases
|
|
1298
|
+
assert.equal(result.phase_count, 2);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
it('combines correctly with --no-check (omits audit/complete from preview)', () => {
|
|
1302
|
+
fixture = createFixture({
|
|
1303
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1304
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1305
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1306
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1307
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1308
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1309
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
const withCheck = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', true, false);
|
|
1313
|
+
const withoutCheck = cmdJobsMilestonePreview(fixture.cwd, 'v6.0', false, false);
|
|
1314
|
+
|
|
1315
|
+
assert.equal(withCheck.check, true);
|
|
1316
|
+
assert.equal(withoutCheck.check, false);
|
|
1317
|
+
assert.ok(withCheck.step_count > withoutCheck.step_count, 'check=true should have more steps');
|
|
1318
|
+
assert.ok(withCheck.steps_preview.some(s => s.includes('audit-milestone')), 'check=true should have audit');
|
|
1319
|
+
assert.ok(!withoutCheck.steps_preview.some(s => s.includes('audit-milestone')), 'check=false should NOT have audit');
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it('auto-detects milestone version same as create', () => {
|
|
1323
|
+
fixture = createFixture({
|
|
1324
|
+
'.planning/ROADMAP.md': FIXTURE_ROADMAP,
|
|
1325
|
+
'.planning/phases/49-job-file-format/49-01-PLAN.md': '',
|
|
1326
|
+
'.planning/phases/49-job-file-format/49-02-PLAN.md': '',
|
|
1327
|
+
'.planning/phases/49-job-file-format/49-01-SUMMARY.md': '',
|
|
1328
|
+
'.planning/phases/49-job-file-format/49-02-SUMMARY.md': '',
|
|
1329
|
+
'.planning/phases/50-job-creation/50-01-PLAN.md': '',
|
|
1330
|
+
'.planning/phases/50-job-creation/50-02-PLAN.md': '',
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
const result = cmdJobsMilestonePreview(fixture.cwd, null, true, false);
|
|
1334
|
+
assert.equal(result.version, 'v6.0');
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
it('throws error when no ROADMAP.md exists', () => {
|
|
1338
|
+
fixture = createFixture({
|
|
1339
|
+
'.planning/phases/': null,
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
assert.throws(
|
|
1343
|
+
() => cmdJobsMilestonePreview(fixture.cwd, null, true, false),
|
|
1344
|
+
(err) => err.message.includes('ROADMAP.md')
|
|
1345
|
+
);
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// ─── findJobFile Tests ──────────────────────────────────────────────────
|
|
1350
|
+
|
|
1351
|
+
describe('findJobFile', () => {
|
|
1352
|
+
let fixture;
|
|
1353
|
+
|
|
1354
|
+
afterEach(() => {
|
|
1355
|
+
if (fixture) fixture.cleanup();
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
it('finds job in in-progress/ when file exists there', () => {
|
|
1359
|
+
fixture = createFixture({
|
|
1360
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1361
|
+
});
|
|
1362
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1363
|
+
assert.equal(result.found, true);
|
|
1364
|
+
assert.equal(result.directory, 'in-progress');
|
|
1365
|
+
assert.ok(result.path.includes('in-progress'));
|
|
1366
|
+
assert.ok(result.path.includes('milestone-v6.0.md'));
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('finds job in pending/ when not in in-progress/', () => {
|
|
1370
|
+
fixture = createFixture({
|
|
1371
|
+
'.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1372
|
+
});
|
|
1373
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1374
|
+
assert.equal(result.found, true);
|
|
1375
|
+
assert.equal(result.directory, 'pending');
|
|
1376
|
+
assert.ok(result.path.includes('pending'));
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
it('prefers in-progress/ over pending/ when file exists in both', () => {
|
|
1380
|
+
fixture = createFixture({
|
|
1381
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1382
|
+
'.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1383
|
+
});
|
|
1384
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1385
|
+
assert.equal(result.found, true);
|
|
1386
|
+
assert.equal(result.directory, 'in-progress');
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('returns found:false when file exists in neither directory', () => {
|
|
1390
|
+
fixture = createFixture({
|
|
1391
|
+
'.planning/jobs/pending/': null,
|
|
1392
|
+
'.planning/jobs/in-progress/': null,
|
|
1393
|
+
});
|
|
1394
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1395
|
+
assert.equal(result.found, false);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it('checks completed/ as fallback for inspection', () => {
|
|
1399
|
+
fixture = createFixture({
|
|
1400
|
+
'.planning/jobs/completed/milestone-v6.0.md': ALL_COMPLETED_JOB,
|
|
1401
|
+
});
|
|
1402
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1403
|
+
assert.equal(result.found, true);
|
|
1404
|
+
assert.equal(result.directory, 'completed');
|
|
1405
|
+
assert.ok(result.path.includes('completed'));
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
it('handles version without v prefix (e.g., "6.0" -> milestone-v6.0.md)', () => {
|
|
1409
|
+
fixture = createFixture({
|
|
1410
|
+
'.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1411
|
+
});
|
|
1412
|
+
const result = findJobFile(fixture.cwd, '6.0');
|
|
1413
|
+
assert.equal(result.found, true);
|
|
1414
|
+
assert.ok(result.path.includes('milestone-v6.0.md'));
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
it('handles version with v prefix correctly', () => {
|
|
1418
|
+
fixture = createFixture({
|
|
1419
|
+
'.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1420
|
+
});
|
|
1421
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1422
|
+
assert.equal(result.found, true);
|
|
1423
|
+
assert.ok(result.path.includes('milestone-v6.0.md'));
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it('finds job in project subdirectory completed/', () => {
|
|
1427
|
+
fixture = createFixture({
|
|
1428
|
+
'.planning/projects/test-project/jobs/completed/milestone-v6.0.md': ALL_COMPLETED_JOB,
|
|
1429
|
+
});
|
|
1430
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1431
|
+
assert.equal(result.found, true);
|
|
1432
|
+
assert.equal(result.directory, 'completed');
|
|
1433
|
+
assert.ok(result.path.includes('projects'));
|
|
1434
|
+
assert.ok(result.path.includes('test-project'));
|
|
1435
|
+
assert.ok(result.path.includes('milestone-v6.0.md'));
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
it('prefers top-level jobs/ over project subdirectory', () => {
|
|
1439
|
+
fixture = createFixture({
|
|
1440
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1441
|
+
'.planning/projects/test-project/jobs/completed/milestone-v6.0.md': ALL_COMPLETED_JOB,
|
|
1442
|
+
});
|
|
1443
|
+
const result = findJobFile(fixture.cwd, 'v6.0');
|
|
1444
|
+
assert.equal(result.found, true);
|
|
1445
|
+
assert.equal(result.directory, 'in-progress');
|
|
1446
|
+
assert.ok(!result.path.includes('projects'), 'Should find top-level job first');
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it('finds job in project subdirectory in-progress/', () => {
|
|
1450
|
+
fixture = createFixture({
|
|
1451
|
+
'.planning/projects/my-app/jobs/in-progress/milestone-v7.0.md': WELL_FORMED_JOB,
|
|
1452
|
+
});
|
|
1453
|
+
const result = findJobFile(fixture.cwd, 'v7.0');
|
|
1454
|
+
assert.equal(result.found, true);
|
|
1455
|
+
assert.equal(result.directory, 'in-progress');
|
|
1456
|
+
assert.ok(result.path.includes('my-app'));
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('returns found:false when not in top-level or project subdirectories', () => {
|
|
1460
|
+
fixture = createFixture({
|
|
1461
|
+
'.planning/jobs/pending/': null,
|
|
1462
|
+
'.planning/projects/test-project/jobs/pending/': null,
|
|
1463
|
+
});
|
|
1464
|
+
const result = findJobFile(fixture.cwd, 'v99.0');
|
|
1465
|
+
assert.equal(result.found, false);
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// ─── updateJobHeader Tests ──────────────────────────────────────────────
|
|
1470
|
+
|
|
1471
|
+
describe('updateJobHeader', () => {
|
|
1472
|
+
let fixture;
|
|
1473
|
+
|
|
1474
|
+
afterEach(() => {
|
|
1475
|
+
if (fixture) fixture.cleanup();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
it('updates Status field from pending to in-progress', () => {
|
|
1479
|
+
const pendingJob = `# Milestone Job: v6.0
|
|
1480
|
+
|
|
1481
|
+
**Version:** v6.0
|
|
1482
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
1483
|
+
**Status:** pending
|
|
1484
|
+
**Check:** true
|
|
1485
|
+
|
|
1486
|
+
## Steps
|
|
1487
|
+
|
|
1488
|
+
- [ ] \`/dgs:plan-phase 41\`
|
|
1489
|
+
- [ ] \`/dgs:execute-phase 41\`
|
|
1490
|
+
`;
|
|
1491
|
+
fixture = createFixture({ 'job.md': pendingJob });
|
|
1492
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1493
|
+
|
|
1494
|
+
updateJobHeader(filePath, 'Status', 'in-progress');
|
|
1495
|
+
|
|
1496
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1497
|
+
assert.ok(content.includes('**Status:** in-progress'), 'Status should be updated');
|
|
1498
|
+
// Other header fields preserved
|
|
1499
|
+
assert.ok(content.includes('**Version:** v6.0'));
|
|
1500
|
+
assert.ok(content.includes('**Created:** 2026-03-02T10:00:00Z'));
|
|
1501
|
+
assert.ok(content.includes('**Check:** true'));
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
it('updates Status field from in-progress to completed preserving all other headers', () => {
|
|
1505
|
+
fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
|
|
1506
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1507
|
+
|
|
1508
|
+
updateJobHeader(filePath, 'Status', 'completed');
|
|
1509
|
+
|
|
1510
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1511
|
+
assert.ok(content.includes('**Status:** completed'), 'Status should be updated to completed');
|
|
1512
|
+
assert.ok(content.includes('**Version:** v6.0'), 'Version preserved');
|
|
1513
|
+
assert.ok(content.includes('**Created:** 2026-03-02T10:00:00Z'), 'Created preserved');
|
|
1514
|
+
assert.ok(content.includes('**Check:** true'), 'Check preserved');
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
it('updates Status field from in-progress to failed', () => {
|
|
1518
|
+
fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
|
|
1519
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1520
|
+
|
|
1521
|
+
updateJobHeader(filePath, 'Status', 'failed');
|
|
1522
|
+
|
|
1523
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1524
|
+
assert.ok(content.includes('**Status:** failed'), 'Status should be updated to failed');
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
it('throws Error if the field is not found in the file', () => {
|
|
1528
|
+
fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
|
|
1529
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1530
|
+
|
|
1531
|
+
assert.throws(
|
|
1532
|
+
() => updateJobHeader(filePath, 'NonExistent', 'value'),
|
|
1533
|
+
(err) => err.message.includes('NonExistent') || err.message.includes('not found')
|
|
1534
|
+
);
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it('preserves exact step line content (round-trip identical steps)', () => {
|
|
1538
|
+
fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
|
|
1539
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1540
|
+
|
|
1541
|
+
// Parse steps before
|
|
1542
|
+
const beforeParsed = parseJobFile(filePath);
|
|
1543
|
+
|
|
1544
|
+
// Update a header field
|
|
1545
|
+
updateJobHeader(filePath, 'Status', 'completed');
|
|
1546
|
+
|
|
1547
|
+
// Parse steps after
|
|
1548
|
+
const afterParsed = parseJobFile(filePath);
|
|
1549
|
+
|
|
1550
|
+
// Steps should be identical
|
|
1551
|
+
assert.equal(afterParsed.steps.length, beforeParsed.steps.length);
|
|
1552
|
+
for (let i = 0; i < beforeParsed.steps.length; i++) {
|
|
1553
|
+
assert.equal(afterParsed.steps[i].command, beforeParsed.steps[i].command);
|
|
1554
|
+
assert.equal(afterParsed.steps[i].args, beforeParsed.steps[i].args);
|
|
1555
|
+
assert.equal(afterParsed.steps[i].status, beforeParsed.steps[i].status);
|
|
1556
|
+
assert.equal(afterParsed.steps[i].timestamp, beforeParsed.steps[i].timestamp);
|
|
1557
|
+
assert.equal(afterParsed.steps[i].error, beforeParsed.steps[i].error);
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
// ─── insertJobSteps Tests ──────────────────────────────────────────────
|
|
1563
|
+
|
|
1564
|
+
describe('insertJobSteps', () => {
|
|
1565
|
+
let fixture;
|
|
1566
|
+
|
|
1567
|
+
afterEach(() => {
|
|
1568
|
+
if (fixture) fixture.cleanup();
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const SIX_STEP_JOB = `# Milestone Job: v6.0
|
|
1572
|
+
|
|
1573
|
+
**Version:** v6.0
|
|
1574
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
1575
|
+
**Status:** in-progress
|
|
1576
|
+
**Check:** true
|
|
1577
|
+
|
|
1578
|
+
## Steps
|
|
1579
|
+
|
|
1580
|
+
- [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T11:00:00Z
|
|
1581
|
+
- [x] \`/dgs:execute-phase 50 --auto\` \u2014 completed 2026-03-02T12:00:00Z
|
|
1582
|
+
- [x] \`/dgs:verify-work 50\` \u2014 completed 2026-03-02T13:00:00Z
|
|
1583
|
+
- [x] \`/dgs:plan-phase 51\` \u2014 completed 2026-03-02T14:00:00Z
|
|
1584
|
+
- [x] \`/dgs:execute-phase 51 --auto\` \u2014 completed 2026-03-02T15:00:00Z
|
|
1585
|
+
- [ ] \`/dgs:verify-work 51\`
|
|
1586
|
+
`;
|
|
1587
|
+
|
|
1588
|
+
it('inserts steps after a specific completed step', () => {
|
|
1589
|
+
fixture = createFixture({ 'job.md': SIX_STEP_JOB });
|
|
1590
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1591
|
+
|
|
1592
|
+
const newSteps = [
|
|
1593
|
+
{ command: 'plan-phase', args: '51.1 --non-interactive' },
|
|
1594
|
+
{ command: 'execute-phase', args: '51.1 --non-interactive' },
|
|
1595
|
+
{ command: 'verify-work', args: '51.1' },
|
|
1596
|
+
{ command: 'plan-phase', args: '51.2 --non-interactive' },
|
|
1597
|
+
];
|
|
1598
|
+
|
|
1599
|
+
const result = insertJobSteps(filePath, 4, newSteps);
|
|
1600
|
+
|
|
1601
|
+
assert.equal(result.inserted, true);
|
|
1602
|
+
assert.equal(result.count, 4);
|
|
1603
|
+
assert.equal(result.startIndex, 5);
|
|
1604
|
+
|
|
1605
|
+
// Verify the file content
|
|
1606
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1607
|
+
const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
|
|
1608
|
+
assert.equal(stepLines.length, 10, 'Should have 6 original + 4 new = 10 steps');
|
|
1609
|
+
|
|
1610
|
+
// The new steps should be pending with correct commands
|
|
1611
|
+
assert.ok(stepLines[5].includes('plan-phase 51.1 --non-interactive'), 'First inserted step');
|
|
1612
|
+
assert.ok(stepLines[6].includes('execute-phase 51.1 --non-interactive'), 'Second inserted step');
|
|
1613
|
+
assert.ok(stepLines[7].includes('verify-work 51.1'), 'Third inserted step');
|
|
1614
|
+
assert.ok(stepLines[8].includes('plan-phase 51.2 --non-interactive'), 'Fourth inserted step');
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
it('each new step is formatted as pending checkbox with /dgs: prefix', () => {
|
|
1618
|
+
fixture = createFixture({ 'job.md': SIX_STEP_JOB });
|
|
1619
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1620
|
+
|
|
1621
|
+
const newSteps = [
|
|
1622
|
+
{ command: 'plan-phase', args: '52 --non-interactive' },
|
|
1623
|
+
];
|
|
1624
|
+
|
|
1625
|
+
insertJobSteps(filePath, 0, newSteps);
|
|
1626
|
+
|
|
1627
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1628
|
+
const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
|
|
1629
|
+
// Inserted step at index 1 (after step 0)
|
|
1630
|
+
assert.ok(stepLines[1].includes('- [ ] `/dgs:plan-phase 52 --non-interactive`'), 'Step should be formatted correctly');
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it('inserting after the last step appends at the end of steps section', () => {
|
|
1634
|
+
fixture = createFixture({ 'job.md': SIX_STEP_JOB });
|
|
1635
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1636
|
+
|
|
1637
|
+
const newSteps = [
|
|
1638
|
+
{ command: 'audit-milestone', args: 'v6.0' },
|
|
1639
|
+
];
|
|
1640
|
+
|
|
1641
|
+
const result = insertJobSteps(filePath, 5, newSteps);
|
|
1642
|
+
|
|
1643
|
+
assert.equal(result.inserted, true);
|
|
1644
|
+
assert.equal(result.count, 1);
|
|
1645
|
+
assert.equal(result.startIndex, 6);
|
|
1646
|
+
|
|
1647
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1648
|
+
const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
|
|
1649
|
+
assert.equal(stepLines.length, 7, 'Should have 6 + 1 = 7 steps');
|
|
1650
|
+
assert.ok(stepLines[6].includes('audit-milestone v6.0'), 'Last step should be the new one');
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
it('inserting with afterStepIndex -1 inserts before all existing steps', () => {
|
|
1654
|
+
fixture = createFixture({ 'job.md': SIX_STEP_JOB });
|
|
1655
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1656
|
+
|
|
1657
|
+
const newSteps = [
|
|
1658
|
+
{ command: 'plan-phase', args: '49 --non-interactive' },
|
|
1659
|
+
];
|
|
1660
|
+
|
|
1661
|
+
const result = insertJobSteps(filePath, -1, newSteps);
|
|
1662
|
+
|
|
1663
|
+
assert.equal(result.inserted, true);
|
|
1664
|
+
assert.equal(result.count, 1);
|
|
1665
|
+
assert.equal(result.startIndex, 0);
|
|
1666
|
+
|
|
1667
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1668
|
+
const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
|
|
1669
|
+
assert.equal(stepLines.length, 7, 'Should have 6 + 1 = 7 steps');
|
|
1670
|
+
assert.ok(stepLines[0].includes('plan-phase 49 --non-interactive'), 'First step should be the new one');
|
|
1671
|
+
// Original first step should now be second
|
|
1672
|
+
assert.ok(stepLines[1].includes('plan-phase 50'), 'Original first step should be second');
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
it('preserves all existing steps and header content exactly', () => {
|
|
1676
|
+
fixture = createFixture({ 'job.md': SIX_STEP_JOB });
|
|
1677
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1678
|
+
|
|
1679
|
+
// Parse before
|
|
1680
|
+
const beforeParsed = parseJobFile(filePath);
|
|
1681
|
+
|
|
1682
|
+
const newSteps = [
|
|
1683
|
+
{ command: 'plan-phase', args: '52 --non-interactive' },
|
|
1684
|
+
];
|
|
1685
|
+
|
|
1686
|
+
insertJobSteps(filePath, 2, newSteps);
|
|
1687
|
+
|
|
1688
|
+
// Parse after
|
|
1689
|
+
const afterParsed = parseJobFile(filePath);
|
|
1690
|
+
|
|
1691
|
+
// Header should be preserved
|
|
1692
|
+
assert.equal(afterParsed.version, beforeParsed.version);
|
|
1693
|
+
assert.equal(afterParsed.status, beforeParsed.status);
|
|
1694
|
+
assert.equal(afterParsed.check, beforeParsed.check);
|
|
1695
|
+
|
|
1696
|
+
// Original steps should still be there (just shifted)
|
|
1697
|
+
assert.equal(afterParsed.stepCount, beforeParsed.stepCount + 1);
|
|
1698
|
+
|
|
1699
|
+
// Steps 0-2 should be identical to before
|
|
1700
|
+
for (let i = 0; i <= 2; i++) {
|
|
1701
|
+
assert.equal(afterParsed.steps[i].command, beforeParsed.steps[i].command);
|
|
1702
|
+
assert.equal(afterParsed.steps[i].args, beforeParsed.steps[i].args);
|
|
1703
|
+
assert.equal(afterParsed.steps[i].status, beforeParsed.steps[i].status);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Steps 4-6 after insert should correspond to original 3-5
|
|
1707
|
+
for (let i = 3; i < beforeParsed.steps.length; i++) {
|
|
1708
|
+
assert.equal(afterParsed.steps[i + 1].command, beforeParsed.steps[i].command);
|
|
1709
|
+
assert.equal(afterParsed.steps[i + 1].args, beforeParsed.steps[i].args);
|
|
1710
|
+
assert.equal(afterParsed.steps[i + 1].status, beforeParsed.steps[i].status);
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
it('returns { inserted: true, count: N, startIndex: M }', () => {
|
|
1715
|
+
fixture = createFixture({ 'job.md': SIX_STEP_JOB });
|
|
1716
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1717
|
+
|
|
1718
|
+
const newSteps = [
|
|
1719
|
+
{ command: 'plan-phase', args: '52 --non-interactive' },
|
|
1720
|
+
{ command: 'execute-phase', args: '52 --non-interactive' },
|
|
1721
|
+
];
|
|
1722
|
+
|
|
1723
|
+
const result = insertJobSteps(filePath, 3, newSteps);
|
|
1724
|
+
|
|
1725
|
+
assert.equal(result.inserted, true);
|
|
1726
|
+
assert.equal(result.count, 2);
|
|
1727
|
+
assert.equal(result.startIndex, 4);
|
|
1728
|
+
});
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
// ─── parseJobFile GapFixCycle Tests ───────────────────────────────────
|
|
1732
|
+
|
|
1733
|
+
describe('parseJobFile GapFixCycle header', () => {
|
|
1734
|
+
let fixture;
|
|
1735
|
+
|
|
1736
|
+
afterEach(() => {
|
|
1737
|
+
if (fixture) fixture.cleanup();
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
it('returns gapFixCycle: 0 when no GapFixCycle header field exists', () => {
|
|
1741
|
+
fixture = createFixture({ 'job.md': WELL_FORMED_JOB });
|
|
1742
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1743
|
+
|
|
1744
|
+
const result = parseJobFile(filePath);
|
|
1745
|
+
assert.equal(result.gapFixCycle, 0, 'Should default to 0 when GapFixCycle header absent');
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
it('returns gapFixCycle: 2 when **GapFixCycle:** 2 header line exists', () => {
|
|
1749
|
+
const jobWithGapFix = `# Milestone Job: v6.0
|
|
1750
|
+
|
|
1751
|
+
**Version:** v6.0
|
|
1752
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
1753
|
+
**Status:** in-progress
|
|
1754
|
+
**Check:** true
|
|
1755
|
+
**GapFixCycle:** 2
|
|
1756
|
+
|
|
1757
|
+
## Steps
|
|
1758
|
+
|
|
1759
|
+
- [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
|
|
1760
|
+
- [ ] \`/dgs:execute-phase 41\`
|
|
1761
|
+
`;
|
|
1762
|
+
fixture = createFixture({ 'job.md': jobWithGapFix });
|
|
1763
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1764
|
+
|
|
1765
|
+
const result = parseJobFile(filePath);
|
|
1766
|
+
assert.equal(result.gapFixCycle, 2, 'Should parse GapFixCycle as 2');
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
// ─── buildGapFixSteps Tests ───────────────────────────────────────────
|
|
1771
|
+
|
|
1772
|
+
describe('buildGapFixSteps', () => {
|
|
1773
|
+
it('generates plan-phase + execute-phase per phase, ending with audit-milestone', () => {
|
|
1774
|
+
const newPhases = [
|
|
1775
|
+
{ number: '53.1', name: 'Fix auth gaps' },
|
|
1776
|
+
{ number: '53.2', name: 'Fix data gaps' },
|
|
1777
|
+
];
|
|
1778
|
+
|
|
1779
|
+
const steps = buildGapFixSteps(newPhases, 'v6.0');
|
|
1780
|
+
|
|
1781
|
+
assert.equal(steps.length, 5, 'Should have 5 steps (2 phases x 2 + 1 audit)');
|
|
1782
|
+
|
|
1783
|
+
assert.equal(steps[0].command, 'plan-phase');
|
|
1784
|
+
assert.equal(steps[0].args, '53.1 --non-interactive');
|
|
1785
|
+
|
|
1786
|
+
assert.equal(steps[1].command, 'execute-phase');
|
|
1787
|
+
assert.equal(steps[1].args, '53.1 --non-interactive');
|
|
1788
|
+
|
|
1789
|
+
assert.equal(steps[2].command, 'plan-phase');
|
|
1790
|
+
assert.equal(steps[2].args, '53.2 --non-interactive');
|
|
1791
|
+
|
|
1792
|
+
assert.equal(steps[3].command, 'execute-phase');
|
|
1793
|
+
assert.equal(steps[3].args, '53.2 --non-interactive');
|
|
1794
|
+
|
|
1795
|
+
assert.equal(steps[4].command, 'audit-milestone');
|
|
1796
|
+
assert.equal(steps[4].args, 'v6.0');
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
it('does not include discuss-phase or verify-work steps', () => {
|
|
1800
|
+
const newPhases = [
|
|
1801
|
+
{ number: '53.1', name: 'Fix auth gaps' },
|
|
1802
|
+
];
|
|
1803
|
+
|
|
1804
|
+
const steps = buildGapFixSteps(newPhases, 'v6.0');
|
|
1805
|
+
|
|
1806
|
+
const commands = steps.map(s => s.command);
|
|
1807
|
+
assert.ok(!commands.includes('discuss-phase'), 'Should not include discuss-phase');
|
|
1808
|
+
assert.ok(!commands.includes('verify-work'), 'Should not include verify-work');
|
|
1809
|
+
});
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// ─── insertGapFixSection Tests ────────────────────────────────────────
|
|
1813
|
+
|
|
1814
|
+
describe('insertGapFixSection', () => {
|
|
1815
|
+
let fixture;
|
|
1816
|
+
|
|
1817
|
+
afterEach(() => {
|
|
1818
|
+
if (fixture) fixture.cleanup();
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
const AUDIT_JOB = `# Milestone Job: v6.0
|
|
1822
|
+
|
|
1823
|
+
**Version:** v6.0
|
|
1824
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
1825
|
+
**Status:** in-progress
|
|
1826
|
+
**Check:** true
|
|
1827
|
+
|
|
1828
|
+
## Steps
|
|
1829
|
+
|
|
1830
|
+
- [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T11:00:00Z
|
|
1831
|
+
- [x] \`/dgs:execute-phase 50 --auto\` \u2014 completed 2026-03-02T12:00:00Z
|
|
1832
|
+
- [x] \`/dgs:verify-work 50\` \u2014 completed 2026-03-02T13:00:00Z
|
|
1833
|
+
- [x] \`/dgs:audit-milestone v6.0\` \u2014 completed 2026-03-02T14:00:00Z
|
|
1834
|
+
- [ ] \`/dgs:complete-milestone v6.0\`
|
|
1835
|
+
`;
|
|
1836
|
+
|
|
1837
|
+
it('inserts section marker and gap-fix steps after the triggering audit step', () => {
|
|
1838
|
+
fixture = createFixture({ 'job.md': AUDIT_JOB });
|
|
1839
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1840
|
+
|
|
1841
|
+
const newPhases = [
|
|
1842
|
+
{ number: '50.1', name: 'Fix auth' },
|
|
1843
|
+
{ number: '50.2', name: 'Fix data' },
|
|
1844
|
+
];
|
|
1845
|
+
|
|
1846
|
+
const result = insertGapFixSection(filePath, 3, 1, newPhases, 'v6.0');
|
|
1847
|
+
|
|
1848
|
+
assert.equal(result.inserted, true);
|
|
1849
|
+
assert.equal(result.count, 5, 'Should have 5 steps (2x plan+execute + 1 audit)');
|
|
1850
|
+
assert.equal(result.cycleNumber, 1);
|
|
1851
|
+
assert.ok(result.sectionMarker.includes('Gap-Fix Cycle 1'), 'Marker should include cycle number');
|
|
1852
|
+
assert.ok(result.sectionMarker.includes('2 gaps'), 'Marker should include gap count');
|
|
1853
|
+
|
|
1854
|
+
// Check file content
|
|
1855
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1856
|
+
assert.ok(content.includes('--- Gap-Fix Cycle 1'), 'File should contain section marker');
|
|
1857
|
+
assert.ok(content.includes('Fix auth'), 'Marker should include phase name');
|
|
1858
|
+
assert.ok(content.includes('Fix data'), 'Marker should include phase name');
|
|
1859
|
+
|
|
1860
|
+
// Section marker should appear between the audit step and the gap-fix steps
|
|
1861
|
+
const lines = content.split('\n');
|
|
1862
|
+
const auditLine = lines.findIndex(l => l.includes('audit-milestone v6.0') && l.includes('[x]'));
|
|
1863
|
+
const markerLine = lines.findIndex(l => l.includes('--- Gap-Fix Cycle 1'));
|
|
1864
|
+
assert.ok(markerLine > auditLine, 'Section marker should be after the audit step');
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
it('writes the GapFixCycle header field', () => {
|
|
1868
|
+
fixture = createFixture({ 'job.md': AUDIT_JOB });
|
|
1869
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1870
|
+
|
|
1871
|
+
const newPhases = [
|
|
1872
|
+
{ number: '50.1', name: 'Fix auth' },
|
|
1873
|
+
];
|
|
1874
|
+
|
|
1875
|
+
insertGapFixSection(filePath, 3, 1, newPhases, 'v6.0');
|
|
1876
|
+
|
|
1877
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1878
|
+
assert.ok(content.includes('**GapFixCycle:** 1'), 'Should add GapFixCycle header');
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
it('places section marker + steps between audit step and subsequent steps', () => {
|
|
1882
|
+
fixture = createFixture({ 'job.md': AUDIT_JOB });
|
|
1883
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1884
|
+
|
|
1885
|
+
const newPhases = [
|
|
1886
|
+
{ number: '50.1', name: 'Fix auth' },
|
|
1887
|
+
];
|
|
1888
|
+
|
|
1889
|
+
insertGapFixSection(filePath, 3, 1, newPhases, 'v6.0');
|
|
1890
|
+
|
|
1891
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1892
|
+
const lines = content.split('\n');
|
|
1893
|
+
const stepLines = lines.filter(l => l.trim().startsWith('- ['));
|
|
1894
|
+
|
|
1895
|
+
// Original: 5 steps (plan, exec, verify, audit, complete)
|
|
1896
|
+
// Inserted: 3 steps (plan, exec, audit-milestone re-audit)
|
|
1897
|
+
// Total: 8 step lines
|
|
1898
|
+
assert.equal(stepLines.length, 8, 'Should have 5 original + 3 inserted = 8 steps');
|
|
1899
|
+
|
|
1900
|
+
// The complete-milestone step should still be the last step
|
|
1901
|
+
assert.ok(stepLines[stepLines.length - 1].includes('complete-milestone'), 'complete-milestone should remain last');
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
it('updates existing GapFixCycle header using updateJobHeader when field already present', () => {
|
|
1905
|
+
const jobWithExistingCycle = `# Milestone Job: v6.0
|
|
1906
|
+
|
|
1907
|
+
**Version:** v6.0
|
|
1908
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
1909
|
+
**Status:** in-progress
|
|
1910
|
+
**Check:** true
|
|
1911
|
+
**GapFixCycle:** 1
|
|
1912
|
+
|
|
1913
|
+
## Steps
|
|
1914
|
+
|
|
1915
|
+
- [x] \`/dgs:plan-phase 50\` \u2014 completed 2026-03-02T11:00:00Z
|
|
1916
|
+
- [x] \`/dgs:audit-milestone v6.0\` \u2014 completed 2026-03-02T14:00:00Z
|
|
1917
|
+
- [ ] \`/dgs:complete-milestone v6.0\`
|
|
1918
|
+
`;
|
|
1919
|
+
fixture = createFixture({ 'job.md': jobWithExistingCycle });
|
|
1920
|
+
const filePath = path.join(fixture.cwd, 'job.md');
|
|
1921
|
+
|
|
1922
|
+
const newPhases = [
|
|
1923
|
+
{ number: '50.2', name: 'Fix more gaps' },
|
|
1924
|
+
];
|
|
1925
|
+
|
|
1926
|
+
insertGapFixSection(filePath, 1, 2, newPhases, 'v6.0');
|
|
1927
|
+
|
|
1928
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1929
|
+
assert.ok(content.includes('**GapFixCycle:** 2'), 'GapFixCycle should be updated to 2');
|
|
1930
|
+
assert.ok(!content.includes('**GapFixCycle:** 1'), 'Old GapFixCycle value should be replaced');
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// ─── listJobs Tests ───────────────────────────────────────────────────
|
|
1935
|
+
|
|
1936
|
+
describe('listJobs', () => {
|
|
1937
|
+
let fixture;
|
|
1938
|
+
|
|
1939
|
+
afterEach(() => {
|
|
1940
|
+
if (fixture) fixture.cleanup();
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
it('returns empty groups when no job files exist in any directory', () => {
|
|
1944
|
+
fixture = createFixture({
|
|
1945
|
+
'.planning/jobs/pending/': null,
|
|
1946
|
+
'.planning/jobs/in-progress/': null,
|
|
1947
|
+
'.planning/jobs/completed/': null,
|
|
1948
|
+
});
|
|
1949
|
+
const result = listJobs(fixture.cwd);
|
|
1950
|
+
|
|
1951
|
+
assert.deepEqual(result.pending, []);
|
|
1952
|
+
assert.deepEqual(result.in_progress, []);
|
|
1953
|
+
assert.deepEqual(result.completed, []);
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
it('returns jobs grouped by status with correct fields', () => {
|
|
1957
|
+
fixture = createFixture({
|
|
1958
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
1959
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
1960
|
+
});
|
|
1961
|
+
const result = listJobs(fixture.cwd);
|
|
1962
|
+
|
|
1963
|
+
assert.equal(result.in_progress.length, 1);
|
|
1964
|
+
assert.equal(result.completed.length, 1);
|
|
1965
|
+
assert.equal(result.pending.length, 0);
|
|
1966
|
+
|
|
1967
|
+
const inProg = result.in_progress[0];
|
|
1968
|
+
assert.equal(inProg.version, 'v6.0');
|
|
1969
|
+
assert.equal(inProg.status, 'in-progress');
|
|
1970
|
+
assert.equal(inProg.check, true);
|
|
1971
|
+
assert.equal(inProg.progress, '1/4');
|
|
1972
|
+
assert.ok(inProg.file.includes('milestone-v6.0.md'));
|
|
1973
|
+
|
|
1974
|
+
const comp = result.completed[0];
|
|
1975
|
+
assert.equal(comp.version, 'v5.0');
|
|
1976
|
+
assert.equal(comp.status, 'completed');
|
|
1977
|
+
assert.equal(comp.check, false);
|
|
1978
|
+
assert.equal(comp.progress, '2/2');
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
it('shows check flag false when --no-check was used', () => {
|
|
1982
|
+
fixture = createFixture({
|
|
1983
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
1984
|
+
});
|
|
1985
|
+
const result = listJobs(fixture.cwd);
|
|
1986
|
+
|
|
1987
|
+
const comp = result.completed[0];
|
|
1988
|
+
assert.equal(comp.check, false);
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
it('handles missing directories gracefully with empty groups', () => {
|
|
1992
|
+
fixture = createFixture({
|
|
1993
|
+
'.planning/': null,
|
|
1994
|
+
});
|
|
1995
|
+
const result = listJobs(fixture.cwd);
|
|
1996
|
+
|
|
1997
|
+
assert.deepEqual(result.pending, []);
|
|
1998
|
+
assert.deepEqual(result.in_progress, []);
|
|
1999
|
+
assert.deepEqual(result.completed, []);
|
|
2000
|
+
});
|
|
2001
|
+
|
|
2002
|
+
it('each entry includes progress as fraction string like "4/12"', () => {
|
|
2003
|
+
fixture = createFixture({
|
|
2004
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2005
|
+
});
|
|
2006
|
+
const result = listJobs(fixture.cwd);
|
|
2007
|
+
const entry = result.in_progress[0];
|
|
2008
|
+
assert.match(entry.progress, /^\d+\/\d+$/, 'Progress should be fraction string');
|
|
2009
|
+
});
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
// ─── cancelJob Tests ──────────────────────────────────────────────────
|
|
2013
|
+
|
|
2014
|
+
describe('cancelJob', () => {
|
|
2015
|
+
let fixture;
|
|
2016
|
+
|
|
2017
|
+
afterEach(() => {
|
|
2018
|
+
if (fixture) fixture.cleanup();
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
it('cancels in-progress job: resets [>] steps to [ ], updates Status, moves to pending/', () => {
|
|
2022
|
+
fixture = createFixture({
|
|
2023
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2024
|
+
'.planning/jobs/pending/': null,
|
|
2025
|
+
});
|
|
2026
|
+
const result = cancelJob(fixture.cwd, 'v6.0');
|
|
2027
|
+
|
|
2028
|
+
assert.equal(result.cancelled, true);
|
|
2029
|
+
assert.equal(result.version, 'v6.0');
|
|
2030
|
+
assert.ok(result.path);
|
|
2031
|
+
assert.equal(result.steps_reset, 1); // one [>] step
|
|
2032
|
+
|
|
2033
|
+
// Verify file moved to pending
|
|
2034
|
+
assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending', 'milestone-v6.0.md')));
|
|
2035
|
+
assert.ok(!fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'in-progress', 'milestone-v6.0.md')));
|
|
2036
|
+
|
|
2037
|
+
// Verify content: [>] reset to [ ], [x] preserved
|
|
2038
|
+
const content = fs.readFileSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending', 'milestone-v6.0.md'), 'utf-8');
|
|
2039
|
+
assert.ok(content.includes('**Status:** pending'), 'Status should be pending');
|
|
2040
|
+
assert.ok(!content.includes('[>]'), 'No in-progress markers should remain');
|
|
2041
|
+
// completed steps preserved
|
|
2042
|
+
assert.ok(content.includes('[x]'), 'Completed steps should be preserved');
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
it('returns steps_reset count of reset steps', () => {
|
|
2046
|
+
const twoInProgress = `# Milestone Job: v6.0
|
|
2047
|
+
|
|
2048
|
+
**Version:** v6.0
|
|
2049
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
2050
|
+
**Status:** in-progress
|
|
2051
|
+
**Check:** true
|
|
2052
|
+
|
|
2053
|
+
## Steps
|
|
2054
|
+
|
|
2055
|
+
- [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
|
|
2056
|
+
- [>] \`/dgs:execute-phase 41\` \u2014 started 2026-03-02T15:00:00Z
|
|
2057
|
+
- [>] \`/dgs:plan-phase 42\` \u2014 started 2026-03-02T15:30:00Z
|
|
2058
|
+
- [ ] \`/dgs:execute-phase 42\`
|
|
2059
|
+
`;
|
|
2060
|
+
fixture = createFixture({
|
|
2061
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': twoInProgress,
|
|
2062
|
+
'.planning/jobs/pending/': null,
|
|
2063
|
+
});
|
|
2064
|
+
const result = cancelJob(fixture.cwd, 'v6.0');
|
|
2065
|
+
|
|
2066
|
+
assert.equal(result.cancelled, true);
|
|
2067
|
+
assert.equal(result.steps_reset, 2);
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
it('keeps completed [x] steps marked done', () => {
|
|
2071
|
+
fixture = createFixture({
|
|
2072
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2073
|
+
'.planning/jobs/pending/': null,
|
|
2074
|
+
});
|
|
2075
|
+
cancelJob(fixture.cwd, 'v6.0');
|
|
2076
|
+
|
|
2077
|
+
const content = fs.readFileSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending', 'milestone-v6.0.md'), 'utf-8');
|
|
2078
|
+
const stepLines = content.split('\n').filter(l => l.trim().startsWith('- ['));
|
|
2079
|
+
const completedSteps = stepLines.filter(l => l.includes('[x]'));
|
|
2080
|
+
assert.ok(completedSteps.length > 0, 'Completed steps should be preserved');
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
it('returns not_found when no job exists for version', () => {
|
|
2084
|
+
fixture = createFixture({
|
|
2085
|
+
'.planning/jobs/pending/': null,
|
|
2086
|
+
'.planning/jobs/in-progress/': null,
|
|
2087
|
+
'.planning/jobs/completed/': null,
|
|
2088
|
+
});
|
|
2089
|
+
const result = cancelJob(fixture.cwd, 'v99.0');
|
|
2090
|
+
|
|
2091
|
+
assert.equal(result.cancelled, false);
|
|
2092
|
+
assert.equal(result.reason, 'not_found');
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
it('returns not_in_progress when job is in pending/', () => {
|
|
2096
|
+
fixture = createFixture({
|
|
2097
|
+
'.planning/jobs/pending/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2098
|
+
});
|
|
2099
|
+
const result = cancelJob(fixture.cwd, 'v6.0');
|
|
2100
|
+
|
|
2101
|
+
assert.equal(result.cancelled, false);
|
|
2102
|
+
assert.equal(result.reason, 'not_in_progress');
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
it('returns not_in_progress when job is in completed/', () => {
|
|
2106
|
+
fixture = createFixture({
|
|
2107
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2108
|
+
});
|
|
2109
|
+
const result = cancelJob(fixture.cwd, 'v5.0');
|
|
2110
|
+
|
|
2111
|
+
assert.equal(result.cancelled, false);
|
|
2112
|
+
assert.equal(result.reason, 'not_in_progress');
|
|
2113
|
+
});
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
// ─── healthCheck Tests ────────────────────────────────────────────────
|
|
2117
|
+
|
|
2118
|
+
describe('healthCheck', () => {
|
|
2119
|
+
let fixture;
|
|
2120
|
+
|
|
2121
|
+
afterEach(() => {
|
|
2122
|
+
if (fixture) fixture.cleanup();
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
it('returns healthy: true with valid directory structure', () => {
|
|
2126
|
+
fixture = createFixture({
|
|
2127
|
+
'.planning/jobs/pending/': null,
|
|
2128
|
+
'.planning/jobs/in-progress/': null,
|
|
2129
|
+
'.planning/jobs/completed/': null,
|
|
2130
|
+
});
|
|
2131
|
+
const result = healthCheck(fixture.cwd);
|
|
2132
|
+
|
|
2133
|
+
assert.equal(result.healthy, true);
|
|
2134
|
+
assert.ok(Array.isArray(result.directories));
|
|
2135
|
+
assert.equal(result.job_count, 0);
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
it('returns healthy: false listing issues for missing directories, then auto-creates them', () => {
|
|
2139
|
+
fixture = createFixture({
|
|
2140
|
+
'.planning/': null,
|
|
2141
|
+
});
|
|
2142
|
+
const result = healthCheck(fixture.cwd);
|
|
2143
|
+
|
|
2144
|
+
// Should report missing directories but auto-create them
|
|
2145
|
+
assert.ok(Array.isArray(result.directories));
|
|
2146
|
+
// After auto-create, directories should exist
|
|
2147
|
+
assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'pending')));
|
|
2148
|
+
assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'in-progress')));
|
|
2149
|
+
assert.ok(fs.existsSync(path.join(fixture.cwd, '.planning', 'jobs', 'completed')));
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
it('validates each job file parses successfully; reports parse failures as issues', () => {
|
|
2153
|
+
const malformedJob = `# Bad Job
|
|
2154
|
+
|
|
2155
|
+
No version or anything
|
|
2156
|
+
|
|
2157
|
+
- [ ] random line
|
|
2158
|
+
`;
|
|
2159
|
+
fixture = createFixture({
|
|
2160
|
+
'.planning/jobs/pending/milestone-v99.0.md': malformedJob,
|
|
2161
|
+
'.planning/jobs/in-progress/': null,
|
|
2162
|
+
'.planning/jobs/completed/': null,
|
|
2163
|
+
});
|
|
2164
|
+
const result = healthCheck(fixture.cwd);
|
|
2165
|
+
|
|
2166
|
+
assert.equal(result.healthy, false);
|
|
2167
|
+
assert.ok(result.issues.length > 0, 'Should report parse failure');
|
|
2168
|
+
assert.ok(result.issues.some(i => i.includes('milestone-v99.0.md') || i.includes('parse')), 'Issue should reference the file');
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
it('counts total job files across all directories', () => {
|
|
2172
|
+
fixture = createFixture({
|
|
2173
|
+
'.planning/jobs/pending/milestone-v7.0.md': EMPTY_STEPS_JOB,
|
|
2174
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2175
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2176
|
+
});
|
|
2177
|
+
const result = healthCheck(fixture.cwd);
|
|
2178
|
+
|
|
2179
|
+
assert.equal(result.job_count, 3);
|
|
2180
|
+
});
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
// ─── dryRunPreview Tests ──────────────────────────────────────────────
|
|
2184
|
+
|
|
2185
|
+
describe('dryRunPreview', () => {
|
|
2186
|
+
let fixture;
|
|
2187
|
+
|
|
2188
|
+
afterEach(() => {
|
|
2189
|
+
if (fixture) fixture.cleanup();
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
it('returns step list with status annotations', () => {
|
|
2193
|
+
fixture = createFixture({
|
|
2194
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2195
|
+
'.planning/ROADMAP.md': '# Roadmap',
|
|
2196
|
+
});
|
|
2197
|
+
const result = dryRunPreview(fixture.cwd, 'v6.0');
|
|
2198
|
+
|
|
2199
|
+
assert.equal(result.found, true);
|
|
2200
|
+
assert.equal(result.version, 'v6.0');
|
|
2201
|
+
assert.ok(Array.isArray(result.steps));
|
|
2202
|
+
assert.equal(result.steps.length, 4);
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
it('each step includes index, command, args, status, display', () => {
|
|
2206
|
+
fixture = createFixture({
|
|
2207
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2208
|
+
'.planning/ROADMAP.md': '# Roadmap',
|
|
2209
|
+
});
|
|
2210
|
+
const result = dryRunPreview(fixture.cwd, 'v6.0');
|
|
2211
|
+
|
|
2212
|
+
for (const step of result.steps) {
|
|
2213
|
+
assert.ok(typeof step.index === 'number', 'step should have index');
|
|
2214
|
+
assert.ok(typeof step.command === 'string', 'step should have command');
|
|
2215
|
+
assert.ok(typeof step.args === 'string', 'step should have args');
|
|
2216
|
+
assert.ok(typeof step.status === 'string', 'step should have status');
|
|
2217
|
+
assert.ok(typeof step.display === 'string', 'step should have display');
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
it('shows resume_from as first non-completed step', () => {
|
|
2222
|
+
fixture = createFixture({
|
|
2223
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2224
|
+
'.planning/ROADMAP.md': '# Roadmap',
|
|
2225
|
+
});
|
|
2226
|
+
const result = dryRunPreview(fixture.cwd, 'v6.0');
|
|
2227
|
+
|
|
2228
|
+
// Step 0 completed, step 1 in-progress, step 2 pending
|
|
2229
|
+
// Resume from first non-completed = step 1
|
|
2230
|
+
assert.equal(result.resume_from, 1);
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
it('returns found: false when job does not exist', () => {
|
|
2234
|
+
fixture = createFixture({
|
|
2235
|
+
'.planning/jobs/pending/': null,
|
|
2236
|
+
'.planning/jobs/in-progress/': null,
|
|
2237
|
+
'.planning/jobs/completed/': null,
|
|
2238
|
+
});
|
|
2239
|
+
const result = dryRunPreview(fixture.cwd, 'v99.0');
|
|
2240
|
+
|
|
2241
|
+
assert.equal(result.found, false);
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2244
|
+
it('validates preconditions: warns if .planning/ missing or ROADMAP.md missing', () => {
|
|
2245
|
+
fixture = createFixture({
|
|
2246
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': WELL_FORMED_JOB,
|
|
2247
|
+
});
|
|
2248
|
+
// No ROADMAP.md
|
|
2249
|
+
const result = dryRunPreview(fixture.cwd, 'v6.0');
|
|
2250
|
+
|
|
2251
|
+
assert.ok(result.found, true);
|
|
2252
|
+
assert.ok(Array.isArray(result.warnings));
|
|
2253
|
+
assert.ok(result.warnings.some(w => w.includes('ROADMAP.md')), 'Should warn about missing ROADMAP.md');
|
|
2254
|
+
});
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
// ─── generateJobSummary Tests ─────────────────────────────────────────
|
|
2258
|
+
|
|
2259
|
+
describe('generateJobSummary', () => {
|
|
2260
|
+
let fixture;
|
|
2261
|
+
|
|
2262
|
+
afterEach(() => {
|
|
2263
|
+
if (fixture) fixture.cleanup();
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
it('produces markdown string with correct header', () => {
|
|
2267
|
+
fixture = createFixture({
|
|
2268
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2269
|
+
});
|
|
2270
|
+
const result = generateJobSummary(fixture.cwd, 'v5.0');
|
|
2271
|
+
|
|
2272
|
+
assert.equal(result.found, true);
|
|
2273
|
+
assert.equal(result.version, 'v5.0');
|
|
2274
|
+
assert.ok(typeof result.content === 'string');
|
|
2275
|
+
assert.ok(result.content.includes('# Job Summary: milestone-v5.0'));
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2278
|
+
it('includes per-step timing table', () => {
|
|
2279
|
+
fixture = createFixture({
|
|
2280
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2281
|
+
});
|
|
2282
|
+
const result = generateJobSummary(fixture.cwd, 'v5.0');
|
|
2283
|
+
|
|
2284
|
+
assert.ok(result.content.includes('| #'));
|
|
2285
|
+
assert.ok(result.content.includes('Command'));
|
|
2286
|
+
assert.ok(result.content.includes('Status'));
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
it('includes error section for failed steps', () => {
|
|
2290
|
+
const failedJob = `# Milestone Job: v6.0
|
|
2291
|
+
|
|
2292
|
+
**Version:** v6.0
|
|
2293
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
2294
|
+
**Status:** failed
|
|
2295
|
+
**Check:** true
|
|
2296
|
+
|
|
2297
|
+
## Steps
|
|
2298
|
+
|
|
2299
|
+
- [x] \`/dgs:plan-phase 41\` \u2014 completed 2026-03-02T14:30:00Z
|
|
2300
|
+
- [!] \`/dgs:execute-phase 41\` \u2014 failed 2026-03-02T15:00:00Z: Tests failed in auth module
|
|
2301
|
+
- [ ] \`/dgs:verify-work 41\`
|
|
2302
|
+
`;
|
|
2303
|
+
fixture = createFixture({
|
|
2304
|
+
'.planning/jobs/in-progress/milestone-v6.0.md': failedJob,
|
|
2305
|
+
});
|
|
2306
|
+
const result = generateJobSummary(fixture.cwd, 'v6.0');
|
|
2307
|
+
|
|
2308
|
+
assert.ok(result.content.includes('Error'), 'Should include error section');
|
|
2309
|
+
assert.ok(result.content.includes('Tests failed in auth module'), 'Should include specific error message');
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
it('includes auto-resolve audit section listing auto-resolved steps', () => {
|
|
2313
|
+
const autoJob = `# Milestone Job: v6.0
|
|
2314
|
+
|
|
2315
|
+
**Version:** v6.0
|
|
2316
|
+
**Created:** 2026-03-02T10:00:00Z
|
|
2317
|
+
**Status:** completed
|
|
2318
|
+
**Check:** true
|
|
2319
|
+
|
|
2320
|
+
## Steps
|
|
2321
|
+
|
|
2322
|
+
- [x] \`/dgs:plan-phase 41 --auto\` \u2014 completed 2026-03-02T14:30:00Z
|
|
2323
|
+
- [x] \`/dgs:execute-phase 41 --auto\` \u2014 completed 2026-03-02T15:00:00Z
|
|
2324
|
+
- [x] \`/dgs:verify-work 41\` \u2014 completed 2026-03-02T15:30:00Z
|
|
2325
|
+
`;
|
|
2326
|
+
fixture = createFixture({
|
|
2327
|
+
'.planning/jobs/completed/milestone-v6.0.md': autoJob,
|
|
2328
|
+
});
|
|
2329
|
+
const result = generateJobSummary(fixture.cwd, 'v6.0');
|
|
2330
|
+
|
|
2331
|
+
assert.ok(result.content.includes('Auto'), 'Should include auto-resolve audit section');
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
it('handles completed, failed, and cancelled jobs', () => {
|
|
2335
|
+
fixture = createFixture({
|
|
2336
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2337
|
+
});
|
|
2338
|
+
const result = generateJobSummary(fixture.cwd, 'v5.0');
|
|
2339
|
+
assert.equal(result.found, true);
|
|
2340
|
+
assert.ok(result.content.includes('completed'));
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
it('returns found: false when job does not exist', () => {
|
|
2344
|
+
fixture = createFixture({
|
|
2345
|
+
'.planning/jobs/pending/': null,
|
|
2346
|
+
'.planning/jobs/in-progress/': null,
|
|
2347
|
+
'.planning/jobs/completed/': null,
|
|
2348
|
+
});
|
|
2349
|
+
const result = generateJobSummary(fixture.cwd, 'v99.0');
|
|
2350
|
+
|
|
2351
|
+
assert.equal(result.found, false);
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
it('includes human verifications section when UAT has human_needed entries', () => {
|
|
2355
|
+
fixture = createFixture({
|
|
2356
|
+
'.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
|
|
2357
|
+
'.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
|
|
2358
|
+
'.planning/phases/50-test-phase/50-UAT.md': UAT_WITH_HUMAN_NEEDED,
|
|
2359
|
+
});
|
|
2360
|
+
const result = generateJobSummary(fixture.cwd, 'v6.0');
|
|
2361
|
+
|
|
2362
|
+
assert.equal(result.found, true);
|
|
2363
|
+
assert.ok(result.content.includes('Outstanding Human Verifications'), 'Should include human verifications section');
|
|
2364
|
+
assert.ok(result.content.includes('Dashboard renders correctly'), 'Should include dashboard test');
|
|
2365
|
+
assert.ok(result.content.includes('Login form accessible'), 'Should include login form test');
|
|
2366
|
+
assert.equal(result.human_verification_count, 2);
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
it('omits human verifications section when no human_needed entries', () => {
|
|
2370
|
+
fixture = createFixture({
|
|
2371
|
+
'.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
|
|
2372
|
+
'.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
|
|
2373
|
+
'.planning/phases/50-test-phase/50-UAT.md': UAT_ALL_PASSED,
|
|
2374
|
+
});
|
|
2375
|
+
const result = generateJobSummary(fixture.cwd, 'v6.0');
|
|
2376
|
+
|
|
2377
|
+
assert.equal(result.found, true);
|
|
2378
|
+
assert.ok(!result.content.includes('Outstanding Human Verifications'), 'Should not include human verifications section');
|
|
2379
|
+
assert.equal(result.human_verification_count, 0);
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
it('omits human verifications when no UAT files exist', () => {
|
|
2383
|
+
fixture = createFixture({
|
|
2384
|
+
'.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
|
|
2385
|
+
'.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
|
|
2386
|
+
'.planning/phases/50-test-phase/': null,
|
|
2387
|
+
});
|
|
2388
|
+
const result = generateJobSummary(fixture.cwd, 'v6.0');
|
|
2389
|
+
|
|
2390
|
+
assert.equal(result.found, true);
|
|
2391
|
+
assert.ok(!result.content.includes('Outstanding Human Verifications'), 'Should not include human verifications section');
|
|
2392
|
+
assert.equal(result.human_verification_count, 0);
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
it('skips UAT files without mode: auto-test', () => {
|
|
2396
|
+
fixture = createFixture({
|
|
2397
|
+
'.planning/jobs/completed/milestone-v6.0.md': COMPLETED_JOB_V6,
|
|
2398
|
+
'.planning/ROADMAP.md': ROADMAP_WITH_PHASE_50,
|
|
2399
|
+
'.planning/phases/50-test-phase/50-UAT.md': UAT_MANUAL_WITH_HUMAN_NEEDED,
|
|
2400
|
+
});
|
|
2401
|
+
const result = generateJobSummary(fixture.cwd, 'v6.0');
|
|
2402
|
+
|
|
2403
|
+
assert.equal(result.found, true);
|
|
2404
|
+
assert.ok(!result.content.includes('Outstanding Human Verifications'), 'Should not include human verifications for manual UAT');
|
|
2405
|
+
assert.equal(result.human_verification_count, 0);
|
|
2406
|
+
});
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
// ─── Created_by field Tests ─────────────────────────────────────────────
|
|
2410
|
+
|
|
2411
|
+
describe('Created_by field', () => {
|
|
2412
|
+
let fixture;
|
|
2413
|
+
|
|
2414
|
+
afterEach(() => {
|
|
2415
|
+
if (fixture) fixture.cleanup();
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
it('parseJobFile returns created_by when present', () => {
|
|
2419
|
+
fixture = createFixture({
|
|
2420
|
+
'job.md': WELL_FORMED_JOB_WITH_CREATED_BY,
|
|
2421
|
+
});
|
|
2422
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job.md'));
|
|
2423
|
+
assert.equal(result.created_by, 'Adrian <adrian@example.com>');
|
|
2424
|
+
});
|
|
2425
|
+
|
|
2426
|
+
it('parseJobFile returns null created_by when absent', () => {
|
|
2427
|
+
fixture = createFixture({
|
|
2428
|
+
'job.md': WELL_FORMED_JOB,
|
|
2429
|
+
});
|
|
2430
|
+
const result = parseJobFile(path.join(fixture.cwd, 'job.md'));
|
|
2431
|
+
assert.equal(result.created_by, null);
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
it('buildJobFileContent includes Created_by when provided', () => {
|
|
2435
|
+
const content = buildJobFileContent('v99.0', true, [{command:'plan-phase', args:'99'}], 'Test <test@t.com>');
|
|
2436
|
+
assert.ok(content.includes('**Created_by:** Test <test@t.com>'));
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
it('buildJobFileContent omits Created_by when not provided', () => {
|
|
2440
|
+
const content = buildJobFileContent('v99.0', true, [{command:'plan-phase', args:'99'}]);
|
|
2441
|
+
assert.ok(!content.includes('Created_by'));
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
it('buildJobFileContent Created_by appears between Created and Status', () => {
|
|
2445
|
+
const content = buildJobFileContent('v99.0', true, [{command:'plan-phase', args:'99'}], 'Test <test@t.com>');
|
|
2446
|
+
const lines = content.split('\n');
|
|
2447
|
+
const createdIdx = lines.findIndex(l => l.startsWith('**Created:**'));
|
|
2448
|
+
const createdByIdx = lines.findIndex(l => l.startsWith('**Created_by:**'));
|
|
2449
|
+
const statusIdx = lines.findIndex(l => l.startsWith('**Status:**'));
|
|
2450
|
+
assert.ok(createdByIdx > createdIdx, 'Created_by should come after Created');
|
|
2451
|
+
assert.ok(createdByIdx < statusIdx, 'Created_by should come before Status');
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2454
|
+
it('generateJobSummary includes Created_by in overview when present', () => {
|
|
2455
|
+
const jobWithCreatedBy = WELL_FORMED_JOB_WITH_CREATED_BY.replace('in-progress', 'completed');
|
|
2456
|
+
fixture = createFixture({
|
|
2457
|
+
'.planning/jobs/completed/milestone-v6.0.md': jobWithCreatedBy,
|
|
2458
|
+
});
|
|
2459
|
+
const result = generateJobSummary(fixture.cwd, 'v6.0');
|
|
2460
|
+
assert.equal(result.found, true);
|
|
2461
|
+
assert.ok(result.content.includes('**Created_by:** Adrian <adrian@example.com>'));
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
it('generateJobSummary omits Created_by in overview when absent', () => {
|
|
2465
|
+
fixture = createFixture({
|
|
2466
|
+
'.planning/jobs/completed/milestone-v5.0.md': ALL_COMPLETED_JOB,
|
|
2467
|
+
});
|
|
2468
|
+
const result = generateJobSummary(fixture.cwd, 'v5.0');
|
|
2469
|
+
assert.equal(result.found, true);
|
|
2470
|
+
assert.ok(!result.content.includes('Created_by'));
|
|
2471
|
+
});
|
|
2472
|
+
});
|
|
2473
|
+
});
|
|
2474
|
+
|
|
2475
|
+
// ─── Root Layout Tests ───────────────────────────────────────────────────────
|
|
2476
|
+
|
|
2477
|
+
describe('jobs root-layout', () => {
|
|
2478
|
+
let fixture;
|
|
2479
|
+
|
|
2480
|
+
afterEach(() => {
|
|
2481
|
+
if (fixture) fixture.cleanup();
|
|
2482
|
+
fixture = null;
|
|
2483
|
+
resetPaths();
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
it('findJobFile resolves jobs dir at root layout', () => {
|
|
2487
|
+
fixture = createTempProject({ layout: 'root' });
|
|
2488
|
+
// Create a job file at root/jobs/pending/ (not .planning/jobs/pending/)
|
|
2489
|
+
const jobsDir = path.join(fixture.cwd, 'jobs', 'pending');
|
|
2490
|
+
fs.mkdirSync(jobsDir, { recursive: true });
|
|
2491
|
+
const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
|
|
2492
|
+
fs.writeFileSync(path.join(jobsDir, 'milestone-v1.0.md'), jobContent);
|
|
2493
|
+
|
|
2494
|
+
const result = findJobFile(fixture.cwd, 'v1.0');
|
|
2495
|
+
assert.equal(result.found, true);
|
|
2496
|
+
assert.equal(result.directory, 'pending');
|
|
2497
|
+
// Should NOT be at .planning/jobs/
|
|
2498
|
+
assert.ok(result.path.includes(path.join(fixture.cwd, 'jobs')));
|
|
2499
|
+
assert.ok(!result.path.includes('.planning'));
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
it('healthCheck creates jobs dirs at root layout', () => {
|
|
2503
|
+
fixture = createTempProject({ layout: 'root' });
|
|
2504
|
+
const result = healthCheck(fixture.cwd);
|
|
2505
|
+
assert.ok(result.directories.length >= 3);
|
|
2506
|
+
// Verify dirs were created at root, not at .planning/
|
|
2507
|
+
assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'pending')));
|
|
2508
|
+
assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'in-progress')));
|
|
2509
|
+
assert.ok(fs.existsSync(path.join(fixture.cwd, 'jobs', 'completed')));
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
it('listJobs works in root layout', () => {
|
|
2513
|
+
fixture = createTempProject({ layout: 'root' });
|
|
2514
|
+
// Create jobs directory structure at root
|
|
2515
|
+
const pendingDir = path.join(fixture.cwd, 'jobs', 'pending');
|
|
2516
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
2517
|
+
fs.mkdirSync(path.join(fixture.cwd, 'jobs', 'in-progress'), { recursive: true });
|
|
2518
|
+
fs.mkdirSync(path.join(fixture.cwd, 'jobs', 'completed'), { recursive: true });
|
|
2519
|
+
|
|
2520
|
+
const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
|
|
2521
|
+
fs.writeFileSync(path.join(pendingDir, 'milestone-v1.0.md'), jobContent);
|
|
2522
|
+
|
|
2523
|
+
const result = listJobs(fixture.cwd);
|
|
2524
|
+
assert.equal(result.pending.length, 1);
|
|
2525
|
+
assert.equal(result.pending[0].version, 'v1.0');
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
// ─── SHA Recording & Rollback Tests ─────────────────────────────────────────
|
|
2530
|
+
|
|
2531
|
+
describe('recordStartShas', () => {
|
|
2532
|
+
let fixture;
|
|
2533
|
+
afterEach(() => { if (fixture) fixture.cleanup(); });
|
|
2534
|
+
|
|
2535
|
+
it('records planning repo SHA when no REPOS.md exists', () => {
|
|
2536
|
+
fixture = createTempProject({ withGit: true });
|
|
2537
|
+
const pendingDir = path.join(fixture.planningDir, 'jobs', 'pending');
|
|
2538
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
2539
|
+
const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** in-progress\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
|
|
2540
|
+
const jobPath = path.join(pendingDir, 'milestone-v1.0.md');
|
|
2541
|
+
fs.writeFileSync(jobPath, jobContent);
|
|
2542
|
+
|
|
2543
|
+
const result = recordStartShas(fixture.cwd, jobPath);
|
|
2544
|
+
assert.equal(result.recorded, true);
|
|
2545
|
+
assert.ok(result.shas._planning, 'Should have _planning SHA');
|
|
2546
|
+
assert.ok(result.shas._planning.length >= 7, 'SHA should be at least 7 chars');
|
|
2547
|
+
|
|
2548
|
+
// Verify it was written into the file
|
|
2549
|
+
const updatedContent = fs.readFileSync(jobPath, 'utf-8');
|
|
2550
|
+
assert.ok(updatedContent.includes('**StartShas:**'), 'Job file should contain StartShas header');
|
|
2551
|
+
});
|
|
2552
|
+
|
|
2553
|
+
it('skips recording if StartShas already exists', () => {
|
|
2554
|
+
fixture = createTempProject({ withGit: true });
|
|
2555
|
+
const pendingDir = path.join(fixture.planningDir, 'jobs', 'pending');
|
|
2556
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
2557
|
+
const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** in-progress\n**Check:** true\n**StartShas:** {"_planning":"abc123"}\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
|
|
2558
|
+
const jobPath = path.join(pendingDir, 'milestone-v1.0.md');
|
|
2559
|
+
fs.writeFileSync(jobPath, jobContent);
|
|
2560
|
+
|
|
2561
|
+
const result = recordStartShas(fixture.cwd, jobPath);
|
|
2562
|
+
assert.equal(result.recorded, true);
|
|
2563
|
+
assert.equal(result.already_existed, true);
|
|
2564
|
+
});
|
|
2565
|
+
});
|
|
2566
|
+
|
|
2567
|
+
describe('parseJobFile with StartShas', () => {
|
|
2568
|
+
let fixture;
|
|
2569
|
+
afterEach(() => { if (fixture) fixture.cleanup(); });
|
|
2570
|
+
|
|
2571
|
+
it('returns startShas as parsed JSON when present', () => {
|
|
2572
|
+
const shaObj = { _planning: 'abc123', myrepo: 'def456' };
|
|
2573
|
+
const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** in-progress\n**Check:** true\n**StartShas:** ${JSON.stringify(shaObj)}\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
|
|
2574
|
+
|
|
2575
|
+
fixture = createFixture({
|
|
2576
|
+
'.planning/config.json': '{}',
|
|
2577
|
+
'.planning/job.md': jobContent,
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2580
|
+
const parsed = parseJobFile(path.join(fixture.cwd, '.planning', 'job.md'));
|
|
2581
|
+
assert.deepEqual(parsed.startShas, shaObj);
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
it('returns startShas as null when absent', () => {
|
|
2585
|
+
const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** pending\n**Check:** true\n\n## Steps\n\n- [ ] \`/dgs:plan-phase 1\`\n`;
|
|
2586
|
+
|
|
2587
|
+
fixture = createFixture({
|
|
2588
|
+
'.planning/config.json': '{}',
|
|
2589
|
+
'.planning/job.md': jobContent,
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
const parsed = parseJobFile(path.join(fixture.cwd, '.planning', 'job.md'));
|
|
2593
|
+
assert.equal(parsed.startShas, null);
|
|
2594
|
+
});
|
|
2595
|
+
});
|
|
2596
|
+
|
|
2597
|
+
describe('rollbackJob', () => {
|
|
2598
|
+
let fixture;
|
|
2599
|
+
afterEach(() => { if (fixture) fixture.cleanup(); });
|
|
2600
|
+
|
|
2601
|
+
it('returns not_found for missing job', () => {
|
|
2602
|
+
fixture = createTempProject({ withGit: true });
|
|
2603
|
+
const result = rollbackJob(fixture.cwd, 'v99.0');
|
|
2604
|
+
assert.equal(result.rolledBack, false);
|
|
2605
|
+
assert.equal(result.reason, 'not_found');
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
it('returns no_start_shas for job without SHAs', () => {
|
|
2609
|
+
fixture = createTempProject({ withGit: true });
|
|
2610
|
+
const pendingDir = path.join(fixture.planningDir, 'jobs', 'pending');
|
|
2611
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
2612
|
+
const jobContent = `# Milestone Job: v1.0\n\n**Version:** v1.0\n**Created:** 2026-01-01T00:00:00Z\n**Status:** completed\n**Check:** true\n\n## Steps\n\n- [x] \`/dgs:plan-phase 1\` \u2014 completed 2026-01-01T01:00:00Z\n`;
|
|
2613
|
+
fs.writeFileSync(path.join(pendingDir, 'milestone-v1.0.md'), jobContent);
|
|
2614
|
+
|
|
2615
|
+
const result = rollbackJob(fixture.cwd, 'v1.0');
|
|
2616
|
+
assert.equal(result.rolledBack, false);
|
|
2617
|
+
assert.equal(result.reason, 'no_start_shas');
|
|
2618
|
+
});
|
|
2619
|
+
});
|