@ktpartners/dgs-platform 3.0.4 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +8 -1
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +32 -0
- package/agents/dgs-planner.md +41 -8
- package/bin/install.js +44 -0
- package/commands/dgs/audit-milestone.md +2 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/research-idea.md +1 -0
- package/commands/dgs/switch-project.md +13 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +120 -5
- package/deliver-great-systems/bin/lib/audit-tolerance.cjs +77 -0
- package/deliver-great-systems/bin/lib/audit-tolerance.test.cjs +101 -0
- package/deliver-great-systems/bin/lib/commands.cjs +311 -16
- package/deliver-great-systems/bin/lib/commands.test.cjs +115 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +41 -0
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/core.cjs +7 -3
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/fast-routing.cjs +199 -0
- package/deliver-great-systems/bin/lib/fast-routing.test.cjs +108 -0
- package/deliver-great-systems/bin/lib/final-commit-precondition.test.cjs +87 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/bundler-audit-gemfile.json +21 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-expected.md +186 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/gate-parity-runresult.json +235 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/govulncheck-import.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/npm-audit-v10.json +37 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-clean.json +3 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/osv-vulns.json +77 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/pip-audit-requirements.json +28 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-lodash.json +30 -0
- package/deliver-great-systems/bin/lib/fixtures/package-scan/snyk-workspaces.json +55 -0
- package/deliver-great-systems/bin/lib/frontmatter.cjs +1 -1
- package/deliver-great-systems/bin/lib/governance.cjs +211 -0
- package/deliver-great-systems/bin/lib/governance.test.cjs +339 -0
- package/deliver-great-systems/bin/lib/health-untracked-phase.test.cjs +269 -0
- package/deliver-great-systems/bin/lib/init.cjs +56 -27
- package/deliver-great-systems/bin/lib/init.test.cjs +212 -5
- package/deliver-great-systems/bin/lib/jobs.cjs +7 -4
- package/deliver-great-systems/bin/lib/milestone.cjs +101 -3
- package/deliver-great-systems/bin/lib/milestone.test.cjs +203 -0
- package/deliver-great-systems/bin/lib/package-adapters.cjs +530 -0
- package/deliver-great-systems/bin/lib/package-adapters.test.cjs +618 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.cjs +350 -0
- package/deliver-great-systems/bin/lib/package-ecosystems.test.cjs +348 -0
- package/deliver-great-systems/bin/lib/package-runner.cjs +199 -0
- package/deliver-great-systems/bin/lib/package-runner.test.cjs +198 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.cjs +56 -0
- package/deliver-great-systems/bin/lib/package-scan-provenance.test.cjs +103 -0
- package/deliver-great-systems/bin/lib/package-scan-report.cjs +1140 -0
- package/deliver-great-systems/bin/lib/package-scan-report.test.cjs +1963 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.cjs +96 -0
- package/deliver-great-systems/bin/lib/package-scan-skill.test.cjs +136 -0
- package/deliver-great-systems/bin/lib/package-scan.cjs +919 -0
- package/deliver-great-systems/bin/lib/package-scan.test.cjs +2147 -0
- package/deliver-great-systems/bin/lib/phase.cjs +18 -1
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +38 -3
- package/deliver-great-systems/bin/lib/projects.test.cjs +112 -2
- package/deliver-great-systems/bin/lib/quick.cjs +178 -23
- package/deliver-great-systems/bin/lib/quick.test.cjs +138 -4
- package/deliver-great-systems/bin/lib/repos.cjs +12 -12
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/state.cjs +7 -3
- package/deliver-great-systems/bin/lib/summary-frontmatter.cjs +54 -0
- package/deliver-great-systems/bin/lib/summary-frontmatter.test.cjs +78 -0
- package/deliver-great-systems/bin/lib/sweep-scope.test.cjs +263 -0
- package/deliver-great-systems/bin/lib/verify.cjs +118 -6
- package/deliver-great-systems/bin/lib/verify.test.cjs +82 -0
- package/deliver-great-systems/bin/lib/wave-0-template-rename.test.cjs +40 -0
- package/deliver-great-systems/bin/lib/worktrees.cjs +27 -1
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +76 -0
- package/deliver-great-systems/references/agent-step-reliability.md +60 -0
- package/deliver-great-systems/references/conflict-resolution.md +4 -0
- package/deliver-great-systems/references/context-tiers.md +4 -0
- package/deliver-great-systems/references/package-scan-config.md +151 -0
- package/deliver-great-systems/references/questioning.md +0 -30
- package/deliver-great-systems/references/spec-review-loop.md +1 -2
- package/deliver-great-systems/references/workflow-conventions.md +29 -0
- package/deliver-great-systems/skills/dgs-tests/package-scan.md +44 -0
- package/deliver-great-systems/templates/REVIEW.md +35 -0
- package/deliver-great-systems/templates/VALIDATION.md +1 -1
- package/deliver-great-systems/templates/claude-md.md +11 -0
- package/deliver-great-systems/templates/package-scan-report.md +108 -0
- package/deliver-great-systems/templates/project.md +6 -170
- package/deliver-great-systems/templates/summary.md +3 -1
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/cancel-job.md +1 -1
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +26 -7
- package/deliver-great-systems/workflows/complete-quick.md +40 -2
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +89 -2
- package/deliver-great-systems/workflows/execute-plan.md +10 -1
- package/deliver-great-systems/workflows/help.md +51 -18
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +46 -152
- package/deliver-great-systems/workflows/new-milestone.md +115 -14
- package/deliver-great-systems/workflows/new-project.md +60 -331
- package/deliver-great-systems/workflows/package-scan.md +59 -0
- package/deliver-great-systems/workflows/plan-phase.md +79 -1
- package/deliver-great-systems/workflows/quick-complete.md +40 -2
- package/deliver-great-systems/workflows/quick.md +183 -10
- package/deliver-great-systems/workflows/research-idea.md +80 -142
- package/deliver-great-systems/workflows/run-job.md +21 -35
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/write-spec.md +9 -11
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -0
|
@@ -60,6 +60,21 @@ function v2FixtureWithProject() {
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// ─── v2 fixture for multi-project case (existing active + new slug) ──────────
|
|
64
|
+
|
|
65
|
+
function v2FixtureMultiProject() {
|
|
66
|
+
// Active project exists and is populated; we are about to create a second, distinct project.
|
|
67
|
+
return createFixture({
|
|
68
|
+
'config.json': JSON.stringify({ current_project: 'test-project' }),
|
|
69
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
|
|
70
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
|
|
71
|
+
'projects/test-project/STATE.md': '# State',
|
|
72
|
+
'projects/test-project/ROADMAP.md': '# Roadmap',
|
|
73
|
+
'projects/test-project/REQUIREMENTS.md': '# Requirements',
|
|
74
|
+
'projects/test-project/PROJECT.md': '# Project',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
// ─── v2 fixture without current_project (for guard tests) ────────────────────
|
|
64
79
|
|
|
65
80
|
function v2FixtureOneProject() {
|
|
@@ -67,6 +82,7 @@ function v2FixtureOneProject() {
|
|
|
67
82
|
'config.json': JSON.stringify({}),
|
|
68
83
|
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
69
84
|
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
85
|
+
'projects/test-project/PROJECT.md': '# Project\n',
|
|
70
86
|
'projects/test-project/STATE.md': '# State',
|
|
71
87
|
});
|
|
72
88
|
}
|
|
@@ -84,7 +100,9 @@ function v2FixtureMultipleProjects() {
|
|
|
84
100
|
'config.json': JSON.stringify({}),
|
|
85
101
|
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
86
102
|
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
103
|
+
'projects/alpha-project/PROJECT.md': '# Project\n',
|
|
87
104
|
'projects/alpha-project/STATE.md': '# State',
|
|
105
|
+
'projects/beta-project/PROJECT.md': '# Project\n',
|
|
88
106
|
'projects/beta-project/STATE.md': '# State',
|
|
89
107
|
});
|
|
90
108
|
}
|
|
@@ -278,7 +296,8 @@ describe('v1 mode: init todos', () => {
|
|
|
278
296
|
});
|
|
279
297
|
|
|
280
298
|
it('returns v1 pending_dir path', () => {
|
|
281
|
-
|
|
299
|
+
// Flat layout: pending_dir points to todos/ (not todos/pending/)
|
|
300
|
+
assert.equal(result.pending_dir, 'todos');
|
|
282
301
|
});
|
|
283
302
|
|
|
284
303
|
it('returns v1 completed_dir path', () => {
|
|
@@ -518,7 +537,8 @@ describe('v2 mode with project: init todos', () => {
|
|
|
518
537
|
});
|
|
519
538
|
|
|
520
539
|
it('returns product-level pending_dir', () => {
|
|
521
|
-
|
|
540
|
+
// Flat layout: pending_dir points to todos/ (not todos/pending/)
|
|
541
|
+
assert.equal(result.pending_dir, 'todos');
|
|
522
542
|
});
|
|
523
543
|
|
|
524
544
|
it('returns product-level completed_dir', () => {
|
|
@@ -1661,6 +1681,71 @@ describe('cmdInitQuick quick_dir resolution', () => {
|
|
|
1661
1681
|
assert.equal(result.description, 'test task');
|
|
1662
1682
|
});
|
|
1663
1683
|
});
|
|
1684
|
+
|
|
1685
|
+
describe('product mode with active quick worktree (parity)', () => {
|
|
1686
|
+
let fixture;
|
|
1687
|
+
|
|
1688
|
+
beforeEach(() => {
|
|
1689
|
+
// Hand-rolled fixture (not v2FixtureWithProject) so we can seed
|
|
1690
|
+
// config.local.json with an active quick worktree whose slug has
|
|
1691
|
+
// already been truncated by worktrees.cjs::_sanitizeSlug to 50 chars.
|
|
1692
|
+
fixture = createFixture({
|
|
1693
|
+
'config.json': JSON.stringify({ current_project: 'test-project' }),
|
|
1694
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n|---------|--------|\n| test-project | Active |\n',
|
|
1695
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n|------|------|\n',
|
|
1696
|
+
'projects/test-project/PROJECT.md': '# Project',
|
|
1697
|
+
'projects/test-project/STATE.md': '# State',
|
|
1698
|
+
'projects/test-project/ROADMAP.md': '# Roadmap\n\n## Phases\n\n- [ ] **Phase 1: Test Phase** - A test\n',
|
|
1699
|
+
'projects/test-project/REQUIREMENTS.md': '# Requirements',
|
|
1700
|
+
});
|
|
1701
|
+
// Seed the active quick worktree AFTER createFixture so we can point
|
|
1702
|
+
// entry.repos at fixture.cwd (which is a real on-disk tmp dir).
|
|
1703
|
+
// Slug is the SHORT (50-char) form that worktrees.cjs would produce.
|
|
1704
|
+
const shortSlug = '260428-k7f-this-is-a-very-long-description-that-w';
|
|
1705
|
+
const localConfig = {
|
|
1706
|
+
execution: { active_context: shortSlug },
|
|
1707
|
+
projects: {
|
|
1708
|
+
'test-project': {
|
|
1709
|
+
worktrees: {
|
|
1710
|
+
[shortSlug]: {
|
|
1711
|
+
type: 'quick',
|
|
1712
|
+
repos: { 'deliver-great-systems': fixture.cwd },
|
|
1713
|
+
},
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1716
|
+
},
|
|
1717
|
+
};
|
|
1718
|
+
fs.writeFileSync(path.join(fixture.cwd, 'config.local.json'),
|
|
1719
|
+
JSON.stringify(localConfig), 'utf-8');
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
afterEach(() => {
|
|
1723
|
+
fixture.cleanup();
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
it('task_dir basename matches active worktree slug (parity with getActiveQuick)', () => {
|
|
1727
|
+
// Description deliberately long enough that init.cjs's own 40-char
|
|
1728
|
+
// sanitiser would produce a descSlug differing from the worktree's
|
|
1729
|
+
// 39-char descSlug by exactly one char (pre-fix). Post-fix, init
|
|
1730
|
+
// defers to the worktree slug, so the two align.
|
|
1731
|
+
const result = runInit(fixture.cwd,
|
|
1732
|
+
'quick "this is a very long description that would otherwise"');
|
|
1733
|
+
|
|
1734
|
+
assert.ok(result.task_dir, 'task_dir should be set');
|
|
1735
|
+
assert.equal(
|
|
1736
|
+
path.basename(result.task_dir),
|
|
1737
|
+
'260428-k7f-this-is-a-very-long-description-that-w',
|
|
1738
|
+
'task_dir basename must equal active worktree slug (no second sanitiser pass)'
|
|
1739
|
+
);
|
|
1740
|
+
assert.equal(
|
|
1741
|
+
result.slug,
|
|
1742
|
+
'this-is-a-very-long-description-that-w',
|
|
1743
|
+
'slug must be derived from worktree slug (39 chars), not recomputed from description (40 chars)'
|
|
1744
|
+
);
|
|
1745
|
+
assert.equal(result.quick_id, '260428-k7f',
|
|
1746
|
+
'quick_id should still be extracted from worktree slug prefix (regression guard)');
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1664
1749
|
});
|
|
1665
1750
|
|
|
1666
1751
|
// ─── init progress-all ───────────────────────────────────────────────────────
|
|
@@ -1756,7 +1841,9 @@ describe('init progress-all: excludes completed projects', () => {
|
|
|
1756
1841
|
'config.json': JSON.stringify({}),
|
|
1757
1842
|
'PROJECTS.md': '# Projects\n',
|
|
1758
1843
|
'REPOS.md': '# Repos\n',
|
|
1844
|
+
'projects/active-x/PROJECT.md': '# Project\n',
|
|
1759
1845
|
'projects/active-x/STATE.md': '# Project State\n\nPhase: 1\nStatus: In progress\nProgress: [#---------] 10%\n',
|
|
1846
|
+
'projects/done-y/PROJECT.md': '# Project\n',
|
|
1760
1847
|
'projects/done-y/STATE.md': '# Project State\n\nPhase: 5\nStatus: completed\nProgress: [##########] 100%\nCompleted: 2026-01-20\n',
|
|
1761
1848
|
});
|
|
1762
1849
|
result = runInit(fixture.cwd, 'progress-all --raw');
|
|
@@ -1898,9 +1985,9 @@ describe('init progress-all: counts backlog files', () => {
|
|
|
1898
1985
|
'config.json': JSON.stringify({}),
|
|
1899
1986
|
'PROJECTS.md': '# Projects\n',
|
|
1900
1987
|
'REPOS.md': '# Repos\n',
|
|
1901
|
-
'todos/
|
|
1902
|
-
'todos/
|
|
1903
|
-
'ideas/
|
|
1988
|
+
'todos/todo-1.md': '---\nstatus: pending\n---\n# todo 1',
|
|
1989
|
+
'todos/todo-2.md': '---\nstatus: pending\n---\n# todo 2',
|
|
1990
|
+
'ideas/idea-1.md': '---\nstatus: pending\n---\n# idea 1',
|
|
1904
1991
|
'specs/spec-1.md': '# spec 1',
|
|
1905
1992
|
'specs/spec-2.md': '# spec 2',
|
|
1906
1993
|
'specs/spec-3.md': '# spec 3',
|
|
@@ -1931,3 +2018,123 @@ describe('init progress-all: counts backlog files', () => {
|
|
|
1931
2018
|
assert.equal(result.product.backlog.debug_active, 1);
|
|
1932
2019
|
});
|
|
1933
2020
|
});
|
|
2021
|
+
|
|
2022
|
+
// ─── v2 mode multi-project: init new-project <slug> ──────────────────────────
|
|
2023
|
+
|
|
2024
|
+
describe('v2 mode multi-project: init new-project <slug>', () => {
|
|
2025
|
+
it('returns project_exists false for a new, non-existing slug', () => {
|
|
2026
|
+
const fixture = v2FixtureMultiProject();
|
|
2027
|
+
try {
|
|
2028
|
+
const result = runInit(fixture.cwd, 'new-project second-project');
|
|
2029
|
+
assert.equal(result.project_exists, false);
|
|
2030
|
+
} finally {
|
|
2031
|
+
fixture.cleanup();
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
it('returns project_path under the requested slug, not current_project', () => {
|
|
2036
|
+
const fixture = v2FixtureMultiProject();
|
|
2037
|
+
try {
|
|
2038
|
+
const result = runInit(fixture.cwd, 'new-project second-project');
|
|
2039
|
+
assert.equal(result.project_path, path.join('projects', 'second-project', 'PROJECT.md'));
|
|
2040
|
+
} finally {
|
|
2041
|
+
fixture.cleanup();
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
it('returns state/roadmap/requirements/research paths under the requested slug', () => {
|
|
2046
|
+
const fixture = v2FixtureMultiProject();
|
|
2047
|
+
try {
|
|
2048
|
+
const result = runInit(fixture.cwd, 'new-project second-project');
|
|
2049
|
+
assert.equal(result.state_path, path.join('projects', 'second-project', 'STATE.md'));
|
|
2050
|
+
assert.equal(result.roadmap_path, path.join('projects', 'second-project', 'ROADMAP.md'));
|
|
2051
|
+
assert.equal(result.requirements_path, path.join('projects', 'second-project', 'REQUIREMENTS.md'));
|
|
2052
|
+
assert.equal(result.research_dir, path.join('projects', 'second-project', 'research'));
|
|
2053
|
+
} finally {
|
|
2054
|
+
fixture.cleanup();
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
it('returns project_exists true when the slug DOES already exist', () => {
|
|
2059
|
+
const fixture = v2FixtureWithProject();
|
|
2060
|
+
try {
|
|
2061
|
+
const result = runInit(fixture.cwd, 'new-project test-project');
|
|
2062
|
+
assert.equal(result.project_exists, true);
|
|
2063
|
+
assert.equal(result.project_path, path.join('projects', 'test-project', 'PROJECT.md'));
|
|
2064
|
+
} finally {
|
|
2065
|
+
fixture.cleanup();
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it('normalizes the slug consistently with generateSlugInternal', () => {
|
|
2070
|
+
// Lock in the contract that the resolver and `projects create` use the same normalizer.
|
|
2071
|
+
const { generateSlugInternal } = require('./core.cjs');
|
|
2072
|
+
assert.equal(generateSlugInternal('Second Project'), 'second-project');
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
it('exposes requested_slug on the result object', () => {
|
|
2076
|
+
const fixture = v2FixtureMultiProject();
|
|
2077
|
+
try {
|
|
2078
|
+
const result = runInit(fixture.cwd, 'new-project second-project');
|
|
2079
|
+
assert.equal(result.requested_slug, 'second-project');
|
|
2080
|
+
// current_project should still report the active project.
|
|
2081
|
+
assert.equal(result.current_project, 'test-project');
|
|
2082
|
+
} finally {
|
|
2083
|
+
fixture.cleanup();
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
// Regression: backwards-compatible no-slug behaviour
|
|
2088
|
+
it('v2 with current_project but no slug arg: behaviour unchanged', () => {
|
|
2089
|
+
const fixture = v2FixtureWithProject();
|
|
2090
|
+
try {
|
|
2091
|
+
const result = runInit(fixture.cwd, 'new-project');
|
|
2092
|
+
assert.equal(result.project_path, path.join('projects', 'test-project', 'PROJECT.md'));
|
|
2093
|
+
assert.equal(result.project_exists, true);
|
|
2094
|
+
} finally {
|
|
2095
|
+
fixture.cleanup();
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
it('v1 mode with no slug arg: behaviour unchanged', () => {
|
|
2100
|
+
const fixture = v1Fixture();
|
|
2101
|
+
try {
|
|
2102
|
+
const result = runInit(fixture.cwd, 'new-project');
|
|
2103
|
+
assert.equal(result.project_path, 'PROJECT.md');
|
|
2104
|
+
} finally {
|
|
2105
|
+
fixture.cleanup();
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// ─── v2 mode multi-project: end-to-end ───────────────────────────────────────
|
|
2111
|
+
|
|
2112
|
+
describe('v2 mode multi-project: end-to-end', () => {
|
|
2113
|
+
it('init new-project <slug> agrees with projects create on the target directory', () => {
|
|
2114
|
+
const fixture = v2FixtureMultiProject();
|
|
2115
|
+
try {
|
|
2116
|
+
// Step 1: init new-project resolves paths under projects/second-project/
|
|
2117
|
+
const initResult = runInit(fixture.cwd, 'new-project second-project');
|
|
2118
|
+
assert.equal(initResult.project_exists, false);
|
|
2119
|
+
assert.equal(initResult.project_path, path.join('projects', 'second-project', 'PROJECT.md'));
|
|
2120
|
+
assert.equal(initResult.requested_slug, 'second-project');
|
|
2121
|
+
// current_project is still the active project — workflow uses requested_slug or
|
|
2122
|
+
// project_path, not current_project, when creating.
|
|
2123
|
+
assert.equal(initResult.current_project, 'test-project');
|
|
2124
|
+
|
|
2125
|
+
// Step 2: projects create creates the same folder init pointed at.
|
|
2126
|
+
execSync(`node "${CLI}" projects create "Second Project"`, {
|
|
2127
|
+
cwd: fixture.cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
2128
|
+
});
|
|
2129
|
+
const createdDir = path.join(fixture.cwd, 'projects', 'second-project');
|
|
2130
|
+
assert.ok(fs.existsSync(createdDir), 'projects create should create projects/second-project/');
|
|
2131
|
+
assert.ok(fs.existsSync(path.join(createdDir, 'PROJECT.md')), 'PROJECT.md should exist in created folder');
|
|
2132
|
+
|
|
2133
|
+
// Step 3: re-running init new-project for the same slug now reports it exists.
|
|
2134
|
+
const reinit = runInit(fixture.cwd, 'new-project second-project');
|
|
2135
|
+
assert.equal(reinit.project_exists, true);
|
|
2136
|
+
} finally {
|
|
2137
|
+
fixture.cleanup();
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
});
|
|
@@ -343,6 +343,9 @@ function updateJobStep(filePath, stepIndex, status, options) {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
/**
|
|
346
|
+
* @deprecated Use setJobStatus() for state transitions instead of physical file moves.
|
|
347
|
+
* Kept for backward compatibility with legacy directory-based job layouts.
|
|
348
|
+
*
|
|
346
349
|
* Move a job file from its current location to a target directory.
|
|
347
350
|
*
|
|
348
351
|
* @param {string} filePath - Absolute path to the job file
|
|
@@ -1461,7 +1464,7 @@ function listJobs(cwd) {
|
|
|
1461
1464
|
}
|
|
1462
1465
|
|
|
1463
1466
|
/**
|
|
1464
|
-
* Cancel an in-progress job: reset [>] steps to [ ], set Status to pending
|
|
1467
|
+
* Cancel an in-progress job: reset [>] steps to [ ], set Status to pending.
|
|
1465
1468
|
*
|
|
1466
1469
|
* @param {string} cwd - Working directory
|
|
1467
1470
|
* @param {string} version - Milestone version (e.g., "v6.0")
|
|
@@ -1472,7 +1475,7 @@ function cancelJob(cwd, version) {
|
|
|
1472
1475
|
if (!found.found) {
|
|
1473
1476
|
return { cancelled: false, reason: 'not_found' };
|
|
1474
1477
|
}
|
|
1475
|
-
if (found.
|
|
1478
|
+
if (found.status !== 'in-progress') {
|
|
1476
1479
|
return { cancelled: false, reason: 'not_in_progress' };
|
|
1477
1480
|
}
|
|
1478
1481
|
|
|
@@ -1814,8 +1817,8 @@ function recordStartShas(cwd, jobFilePath) {
|
|
|
1814
1817
|
if (name === 'Name' || name.startsWith('-')) continue;
|
|
1815
1818
|
if (!repoPath || repoPath.startsWith('-')) continue;
|
|
1816
1819
|
|
|
1817
|
-
// Resolve absolute path: paths in REPOS.md are relative to planning root
|
|
1818
|
-
const absRepoPath = path.resolve(
|
|
1820
|
+
// Resolve absolute path: paths in REPOS.md are relative to the planning root
|
|
1821
|
+
const absRepoPath = path.resolve(planningRoot, repoPath);
|
|
1819
1822
|
if (!fs.existsSync(absRepoPath)) continue;
|
|
1820
1823
|
|
|
1821
1824
|
try {
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, getMilestonePhaseFilter, getProjectRoot, output, error } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, getMilestonePhaseFilter, getProjectRoot, output, error, loadConfig } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
10
10
|
const { writeStateMd } = require('./state.cjs');
|
|
11
|
+
const { getContributors, checkFourEyes } = require('./governance.cjs');
|
|
12
|
+
const { requireGitIdentity, formatAuthorString } = require('./identity.cjs');
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Internal helper: marks requirement IDs complete in REQUIREMENTS.md.
|
|
@@ -159,6 +161,72 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
159
161
|
}
|
|
160
162
|
} catch {}
|
|
161
163
|
|
|
164
|
+
// ── Four-Eyes Gate (GATE-01) ───────────────────────────────────────────────
|
|
165
|
+
const config = loadConfig(cwd);
|
|
166
|
+
const rawConfig = (() => {
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(fs.readFileSync(path.join(planRoot, 'config.json'), 'utf-8'));
|
|
169
|
+
} catch { return {}; }
|
|
170
|
+
})();
|
|
171
|
+
const fourEyesMode = (rawConfig.workflow && rawConfig.workflow.four_eyes) || 'off';
|
|
172
|
+
let governanceNote = null;
|
|
173
|
+
|
|
174
|
+
if (fourEyesMode !== 'off') {
|
|
175
|
+
// Resolve current user identity
|
|
176
|
+
let currentUserStr = '';
|
|
177
|
+
try {
|
|
178
|
+
const identity = requireGitIdentity(cwd);
|
|
179
|
+
currentUserStr = formatAuthorString(identity);
|
|
180
|
+
} catch {
|
|
181
|
+
// Identity gate already ran — this is a safety fallback
|
|
182
|
+
currentUserStr = '';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Get contributors for this milestone
|
|
186
|
+
const contribResult = getContributors(cwd);
|
|
187
|
+
const contributors = contribResult.contributors || [];
|
|
188
|
+
|
|
189
|
+
// Run four-eyes check
|
|
190
|
+
const feResult = checkFourEyes(contributors, currentUserStr, fourEyesMode);
|
|
191
|
+
|
|
192
|
+
// Display contributor list (SHR-02 — always when not off)
|
|
193
|
+
const contribNames = contributors.length > 0
|
|
194
|
+
? contributors.join(', ')
|
|
195
|
+
: '(none detected)';
|
|
196
|
+
|
|
197
|
+
if (feResult.passed) {
|
|
198
|
+
// Four-eyes satisfied — display and continue
|
|
199
|
+
process.stderr.write('Contributors: ' + contribNames + ' \u2014 \u2714 Four-eyes satisfied\n');
|
|
200
|
+
} else {
|
|
201
|
+
// Four-eyes failed — display contributor list first
|
|
202
|
+
process.stderr.write('Contributors: ' + contribNames + '\n');
|
|
203
|
+
|
|
204
|
+
// Extract current user name for display
|
|
205
|
+
let displayName = currentUserStr;
|
|
206
|
+
try {
|
|
207
|
+
const identity = requireGitIdentity(cwd);
|
|
208
|
+
displayName = identity.name;
|
|
209
|
+
} catch { /* use full string */ }
|
|
210
|
+
|
|
211
|
+
if (fourEyesMode === 'warn') {
|
|
212
|
+
// GATE-04: warn mode — display warning, proceed, log
|
|
213
|
+
process.stderr.write('\u26A0 You (' + displayName + ') contributed to this milestone. Completing anyway (warn mode).\n');
|
|
214
|
+
governanceNote = 'Four-eyes warning: ' + displayName + ' completed while also a contributor. Contributors: ' + contribNames;
|
|
215
|
+
} else if (fourEyesMode === 'enforce') {
|
|
216
|
+
if (options.force) {
|
|
217
|
+
// GATE-06: --force bypasses enforce
|
|
218
|
+
process.stderr.write('\u26A0 Forced: you (' + displayName + ') are the only contributor. Override logged.\n');
|
|
219
|
+
governanceNote = 'Four-eyes override: ' + displayName + ' force-completed. Contributors: ' + contribNames;
|
|
220
|
+
} else {
|
|
221
|
+
// GATE-05: enforce mode — block completion
|
|
222
|
+
error('\u2718 Blocked: you (' + displayName + ') are the only contributor. Use --force to override.');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Off mode: no check, no output, no performance impact (GATE-03)
|
|
229
|
+
|
|
162
230
|
// Archive ROADMAP.md
|
|
163
231
|
if (fs.existsSync(roadmapPath)) {
|
|
164
232
|
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
@@ -180,7 +248,8 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
180
248
|
|
|
181
249
|
// Create/append MILESTONES.md entry
|
|
182
250
|
const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n');
|
|
183
|
-
const
|
|
251
|
+
const governanceLine = governanceNote ? `\n**Governance:** ${governanceNote}\n` : '';
|
|
252
|
+
const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}${governanceLine}\n\n---\n\n`;
|
|
184
253
|
|
|
185
254
|
if (fs.existsSync(milestonesPath)) {
|
|
186
255
|
const existing = fs.readFileSync(milestonesPath, 'utf-8');
|
|
@@ -214,9 +283,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
214
283
|
/(\*\*Last Activity:\*\*\s*).*/,
|
|
215
284
|
`$1${today}`
|
|
216
285
|
);
|
|
286
|
+
const activitySuffix = governanceNote
|
|
287
|
+
? ` (four-eyes: ${fourEyesMode === 'warn' ? 'warn' : 'force'} override by ${(() => { try { return requireGitIdentity(cwd).name; } catch { return 'unknown'; } })()})`
|
|
288
|
+
: '';
|
|
217
289
|
stateContent = stateContent.replace(
|
|
218
290
|
/(\*\*Last Activity Description:\*\*\s*).*/,
|
|
219
|
-
`$1${version} milestone completed and archived`
|
|
291
|
+
`$1${version} milestone completed and archived${activitySuffix}`
|
|
220
292
|
);
|
|
221
293
|
writeStateMd(statePath, stateContent, cwd);
|
|
222
294
|
}
|
|
@@ -240,6 +312,31 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
240
312
|
} catch {}
|
|
241
313
|
}
|
|
242
314
|
|
|
315
|
+
// Archive completed quick task directories
|
|
316
|
+
let quickArchived = false;
|
|
317
|
+
const quickDir = path.join(projectRoot, 'quick');
|
|
318
|
+
if (fs.existsSync(quickDir)) {
|
|
319
|
+
try {
|
|
320
|
+
const quickArchiveDir = path.join(archiveDir, `${version}-quick`);
|
|
321
|
+
const quickEntries = fs.readdirSync(quickDir, { withFileTypes: true });
|
|
322
|
+
const quickDirNames = quickEntries.filter(e => e.isDirectory()).map(e => e.name);
|
|
323
|
+
let archivedCount = 0;
|
|
324
|
+
for (const dir of quickDirNames) {
|
|
325
|
+
// Check if this quick dir has a SUMMARY file (completed)
|
|
326
|
+
const dirPath = path.join(quickDir, dir);
|
|
327
|
+
const dirFiles = fs.readdirSync(dirPath);
|
|
328
|
+
const hasSummary = dirFiles.some(f => f.endsWith('-SUMMARY.md'));
|
|
329
|
+
if (!hasSummary) continue;
|
|
330
|
+
if (!fs.existsSync(quickArchiveDir)) {
|
|
331
|
+
fs.mkdirSync(quickArchiveDir, { recursive: true });
|
|
332
|
+
}
|
|
333
|
+
fs.renameSync(dirPath, path.join(quickArchiveDir, dir));
|
|
334
|
+
archivedCount++;
|
|
335
|
+
}
|
|
336
|
+
quickArchived = archivedCount > 0;
|
|
337
|
+
} catch {}
|
|
338
|
+
}
|
|
339
|
+
|
|
243
340
|
const result = {
|
|
244
341
|
version,
|
|
245
342
|
name: milestoneName,
|
|
@@ -253,6 +350,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
253
350
|
requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)),
|
|
254
351
|
audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
|
|
255
352
|
phases: phasesArchived,
|
|
353
|
+
quick: quickArchived,
|
|
256
354
|
},
|
|
257
355
|
milestones_updated: true,
|
|
258
356
|
state_updated: fs.existsSync(statePath),
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for milestone.cjs — quick dir archival in cmdMilestoneComplete
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in test runner (node:test) and assert (node:assert).
|
|
5
|
+
* Each test creates an isolated temp directory fixture and cleans up after.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const { createFixture, writeFile } = require('./test-helpers.cjs');
|
|
14
|
+
const { cmdMilestoneComplete } = require('./milestone.cjs');
|
|
15
|
+
|
|
16
|
+
// Helper: capture stdout from cmdMilestoneComplete (which calls output())
|
|
17
|
+
function captureResult(fn) {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
const origWrite = process.stdout.write;
|
|
20
|
+
process.stdout.write = function (chunk) {
|
|
21
|
+
chunks.push(String(chunk));
|
|
22
|
+
};
|
|
23
|
+
// Also suppress stderr (governance / state messages)
|
|
24
|
+
const origStderrWrite = process.stderr.write;
|
|
25
|
+
process.stderr.write = function () {};
|
|
26
|
+
// Prevent process.exit from actually exiting during tests
|
|
27
|
+
const origExit = process.exit;
|
|
28
|
+
process.exit = function () {};
|
|
29
|
+
try {
|
|
30
|
+
fn();
|
|
31
|
+
} finally {
|
|
32
|
+
process.stdout.write = origWrite;
|
|
33
|
+
process.stderr.write = origStderrWrite;
|
|
34
|
+
process.exit = origExit;
|
|
35
|
+
}
|
|
36
|
+
const raw = chunks.join('');
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a root-layout fixture suitable for cmdMilestoneComplete.
|
|
46
|
+
* Root layout: no PROJECTS.md/REPOS.md (avoids v2 detection).
|
|
47
|
+
* Includes config.local.json with planningRoot, STATE.md, ROADMAP.md, phases/.
|
|
48
|
+
*/
|
|
49
|
+
function createMilestoneFixture(extras) {
|
|
50
|
+
const base = {
|
|
51
|
+
'config.json': JSON.stringify({}),
|
|
52
|
+
'config.local.json': JSON.stringify({ planningRoot: '.' }),
|
|
53
|
+
'STATE.md': '# Project State\n\nPhase: 1\nStatus: Ready\nProgress: [----------] 0%\n',
|
|
54
|
+
'ROADMAP.md': '# Roadmap\n\n## Phase 1: Test\n',
|
|
55
|
+
'phases/': null,
|
|
56
|
+
};
|
|
57
|
+
if (extras) {
|
|
58
|
+
Object.assign(base, extras);
|
|
59
|
+
}
|
|
60
|
+
return createFixture(base);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Quick Dir Archival Tests ────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe('cmdMilestoneComplete quick dir archival', () => {
|
|
66
|
+
let fixture;
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
if (fixture) fixture.cleanup();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('archives completed quick dirs (with SUMMARY.md) to milestones/v1.0-quick/', () => {
|
|
73
|
+
fixture = createMilestoneFixture({
|
|
74
|
+
'quick/260101-abc-some-task/260101-abc-PLAN.md': '# Plan',
|
|
75
|
+
'quick/260101-abc-some-task/260101-abc-SUMMARY.md': '# Summary',
|
|
76
|
+
});
|
|
77
|
+
const cwd = fixture.cwd;
|
|
78
|
+
|
|
79
|
+
const result = captureResult(() => {
|
|
80
|
+
cmdMilestoneComplete(cwd, 'v1.0', {}, false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Quick dir should be moved to milestones/v1.0-quick/
|
|
84
|
+
const archivedDir = path.join(cwd, 'milestones', 'v1.0-quick', '260101-abc-some-task');
|
|
85
|
+
assert.ok(fs.existsSync(archivedDir), 'Quick dir should be archived to milestones/v1.0-quick/');
|
|
86
|
+
assert.ok(!fs.existsSync(path.join(cwd, 'quick', '260101-abc-some-task')), 'Quick dir should be removed from quick/');
|
|
87
|
+
assert.ok(result, 'Should return result JSON');
|
|
88
|
+
assert.equal(result.archived.quick, true, 'archived.quick should be true');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('does not move HISTORY.md from quick/', () => {
|
|
92
|
+
fixture = createMilestoneFixture({
|
|
93
|
+
'quick/HISTORY.md': '# History\n',
|
|
94
|
+
'quick/260102-def-other/260102-def-SUMMARY.md': '# Summary',
|
|
95
|
+
});
|
|
96
|
+
const cwd = fixture.cwd;
|
|
97
|
+
|
|
98
|
+
const result = captureResult(() => {
|
|
99
|
+
cmdMilestoneComplete(cwd, 'v1.0', {}, false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// HISTORY.md should remain in quick/
|
|
103
|
+
assert.ok(fs.existsSync(path.join(cwd, 'quick', 'HISTORY.md')), 'HISTORY.md should stay in quick/');
|
|
104
|
+
// The completed dir should be archived
|
|
105
|
+
assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick', '260102-def-other')), 'Completed dir should be archived');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('skips plain files (non-directories) in quick/', () => {
|
|
109
|
+
fixture = createMilestoneFixture({
|
|
110
|
+
'quick/some-notes.txt': 'random notes',
|
|
111
|
+
'quick/260103-ghi-task/260103-ghi-SUMMARY.md': '# Summary',
|
|
112
|
+
});
|
|
113
|
+
const cwd = fixture.cwd;
|
|
114
|
+
|
|
115
|
+
const result = captureResult(() => {
|
|
116
|
+
cmdMilestoneComplete(cwd, 'v1.0', {}, false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// File should remain
|
|
120
|
+
assert.ok(fs.existsSync(path.join(cwd, 'quick', 'some-notes.txt')), 'Plain files should remain in quick/');
|
|
121
|
+
// Dir should be archived
|
|
122
|
+
assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick', '260103-ghi-task')), 'Completed dir should be archived');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does NOT move quick dirs without SUMMARY.md (incomplete)', () => {
|
|
126
|
+
fixture = createMilestoneFixture({
|
|
127
|
+
'quick/260104-jkl-wip/260104-jkl-PLAN.md': '# Plan',
|
|
128
|
+
});
|
|
129
|
+
const cwd = fixture.cwd;
|
|
130
|
+
|
|
131
|
+
const result = captureResult(() => {
|
|
132
|
+
cmdMilestoneComplete(cwd, 'v1.0', {}, false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Should NOT be moved
|
|
136
|
+
assert.ok(fs.existsSync(path.join(cwd, 'quick', '260104-jkl-wip')), 'Incomplete quick dir should stay in quick/');
|
|
137
|
+
assert.ok(!fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick')), 'No quick archive dir should be created');
|
|
138
|
+
assert.ok(result, 'Should return result JSON');
|
|
139
|
+
assert.equal(result.archived.quick, false, 'archived.quick should be false when nothing archived');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('gracefully handles when quick/ does not exist', () => {
|
|
143
|
+
fixture = createMilestoneFixture();
|
|
144
|
+
const cwd = fixture.cwd;
|
|
145
|
+
|
|
146
|
+
// No quick/ directory at all
|
|
147
|
+
const result = captureResult(() => {
|
|
148
|
+
cmdMilestoneComplete(cwd, 'v1.0', {}, false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
assert.ok(result, 'Should return result JSON without error');
|
|
152
|
+
assert.equal(result.archived.quick, false, 'archived.quick should be false when no quick/ dir');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns quickArchived=false when quick/ has no completed dirs', () => {
|
|
156
|
+
fixture = createMilestoneFixture({
|
|
157
|
+
'quick/HISTORY.md': '# History',
|
|
158
|
+
'quick/260105-mno-wip/260105-mno-PLAN.md': '# Plan',
|
|
159
|
+
});
|
|
160
|
+
const cwd = fixture.cwd;
|
|
161
|
+
|
|
162
|
+
const result = captureResult(() => {
|
|
163
|
+
cmdMilestoneComplete(cwd, 'v1.0', {}, false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assert.equal(result.archived.quick, false, 'archived.quick should be false');
|
|
167
|
+
assert.ok(!fs.existsSync(path.join(cwd, 'milestones', 'v1.0-quick')), 'No quick archive dir should be created');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('creates milestones/v{X.Y}-quick/ only when there are dirs to archive', () => {
|
|
171
|
+
fixture = createMilestoneFixture({
|
|
172
|
+
'quick/260106-pqr-first/260106-pqr-SUMMARY.md': '# Summary',
|
|
173
|
+
'quick/260107-stu-second/260107-stu-SUMMARY.md': '# Summary',
|
|
174
|
+
'quick/260108-vwx-wip/260108-vwx-PLAN.md': '# Plan',
|
|
175
|
+
});
|
|
176
|
+
const cwd = fixture.cwd;
|
|
177
|
+
|
|
178
|
+
const result = captureResult(() => {
|
|
179
|
+
cmdMilestoneComplete(cwd, 'v2.0', {}, false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Both completed dirs should be archived
|
|
183
|
+
assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v2.0-quick', '260106-pqr-first')), 'First completed dir archived');
|
|
184
|
+
assert.ok(fs.existsSync(path.join(cwd, 'milestones', 'v2.0-quick', '260107-stu-second')), 'Second completed dir archived');
|
|
185
|
+
// Incomplete dir should remain
|
|
186
|
+
assert.ok(fs.existsSync(path.join(cwd, 'quick', '260108-vwx-wip')), 'Incomplete dir should stay');
|
|
187
|
+
assert.equal(result.archived.quick, true, 'archived.quick should be true');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('result JSON includes archived.quick field', () => {
|
|
191
|
+
fixture = createMilestoneFixture();
|
|
192
|
+
const cwd = fixture.cwd;
|
|
193
|
+
|
|
194
|
+
const result = captureResult(() => {
|
|
195
|
+
cmdMilestoneComplete(cwd, 'v1.0', {}, false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
assert.ok(result, 'Should return result');
|
|
199
|
+
assert.ok('archived' in result, 'Result should have archived object');
|
|
200
|
+
assert.ok('quick' in result.archived, 'archived should have quick field');
|
|
201
|
+
assert.equal(typeof result.archived.quick, 'boolean', 'archived.quick should be boolean');
|
|
202
|
+
});
|
|
203
|
+
});
|