@ktpartners/dgs-platform 2.9.0 → 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 +197 -0
- package/README.md +34 -2
- package/agents/dgs-executor.md +124 -3
- package/agents/dgs-idea-researcher.md +447 -0
- package/agents/dgs-plan-checker.md +61 -3
- package/agents/dgs-planner.md +51 -8
- package/bin/install.js +44 -0
- package/commands/dgs/abandon-quick.md +28 -0
- package/commands/dgs/add-tests.md +2 -2
- package/commands/dgs/audit-milestone.md +4 -3
- package/commands/dgs/capture-principle.md +11 -11
- package/commands/dgs/cleanup.md +2 -2
- package/commands/dgs/complete-milestone.md +11 -11
- package/commands/dgs/complete-quick.md +28 -0
- package/commands/dgs/create-milestone-job.md +2 -2
- package/commands/dgs/debug.md +3 -3
- package/commands/dgs/develop-idea.md +1 -1
- package/commands/dgs/diff-report.md +124 -0
- package/commands/dgs/fast.md +3 -1
- package/commands/dgs/health.md +1 -1
- package/commands/dgs/map-codebase.md +6 -6
- package/commands/dgs/new-milestone.md +5 -5
- package/commands/dgs/new-project.md +8 -21
- package/commands/dgs/package-scan.md +43 -0
- package/commands/dgs/plan-milestone-gaps.md +1 -1
- package/commands/dgs/progress.md +3 -3
- package/commands/dgs/quick-abandon.md +8 -0
- package/commands/dgs/quick-complete.md +8 -0
- package/commands/dgs/quick.md +10 -3
- package/commands/dgs/research-idea.md +3 -2
- package/commands/dgs/research-phase.md +3 -3
- package/commands/dgs/switch-project.md +14 -1
- package/commands/dgs/write-spec.md +3 -3
- package/deliver-great-systems/bin/dgs-tools.cjs +401 -32
- 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 +626 -46
- package/deliver-great-systems/bin/lib/commands.test.cjs +451 -0
- package/deliver-great-systems/bin/lib/commit-verify.test.cjs +236 -0
- package/deliver-great-systems/bin/lib/config.cjs +80 -6
- package/deliver-great-systems/bin/lib/config.test.cjs +309 -0
- package/deliver-great-systems/bin/lib/context.cjs +120 -0
- package/deliver-great-systems/bin/lib/core.cjs +35 -14
- package/deliver-great-systems/bin/lib/core.test.cjs +79 -1
- package/deliver-great-systems/bin/lib/execution.cjs +49 -17
- 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/flat-migration.test.cjs +396 -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/ideas.cjs +206 -91
- package/deliver-great-systems/bin/lib/ideas.test.cjs +244 -1
- package/deliver-great-systems/bin/lib/init.cjs +357 -61
- package/deliver-great-systems/bin/lib/init.test.cjs +625 -8
- package/deliver-great-systems/bin/lib/jobs.cjs +131 -25
- package/deliver-great-systems/bin/lib/jobs.test.cjs +193 -74
- package/deliver-great-systems/bin/lib/migration.cjs +409 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +158 -1
- package/deliver-great-systems/bin/lib/milestone.cjs +154 -31
- 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 +146 -3
- package/deliver-great-systems/bin/lib/phase.test.cjs +420 -0
- package/deliver-great-systems/bin/lib/plan-number-validity.test.cjs +48 -0
- package/deliver-great-systems/bin/lib/projects.cjs +65 -10
- package/deliver-great-systems/bin/lib/projects.test.cjs +198 -2
- package/deliver-great-systems/bin/lib/quick.cjs +739 -0
- package/deliver-great-systems/bin/lib/quick.test.cjs +730 -0
- package/deliver-great-systems/bin/lib/repos.cjs +37 -13
- package/deliver-great-systems/bin/lib/review.cjs +1821 -0
- package/deliver-great-systems/bin/lib/roadmap.cjs +34 -13
- package/deliver-great-systems/bin/lib/specs.cjs +3 -81
- package/deliver-great-systems/bin/lib/state-transition-gate.test.cjs +160 -0
- package/deliver-great-systems/bin/lib/state.cjs +147 -55
- 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/sync.cjs +75 -0
- package/deliver-great-systems/bin/lib/verify.cjs +198 -7
- 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 +790 -0
- package/deliver-great-systems/bin/lib/worktrees.test.cjs +963 -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 +27 -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/abandon-quick.md +89 -0
- package/deliver-great-systems/workflows/add-idea.md +3 -3
- package/deliver-great-systems/workflows/add-phase.md +5 -0
- package/deliver-great-systems/workflows/add-tests.md +14 -0
- package/deliver-great-systems/workflows/add-todo.md +1 -0
- package/deliver-great-systems/workflows/approve-spec.md +25 -4
- package/deliver-great-systems/workflows/audit-milestone.md +66 -10
- package/deliver-great-systems/workflows/audit-phase.md +15 -5
- package/deliver-great-systems/workflows/cancel-job.md +2 -2
- package/deliver-great-systems/workflows/check-todos.md +2 -3
- package/deliver-great-systems/workflows/codereview.md +103 -9
- package/deliver-great-systems/workflows/complete-milestone.md +218 -24
- package/deliver-great-systems/workflows/complete-quick.md +106 -0
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +4 -4
- package/deliver-great-systems/workflows/develop-idea.md +11 -11
- package/deliver-great-systems/workflows/diagnose-issues.md +14 -0
- package/deliver-great-systems/workflows/discuss-idea.md +1 -1
- package/deliver-great-systems/workflows/discuss-phase.md +3 -2
- package/deliver-great-systems/workflows/execute-phase.md +209 -33
- package/deliver-great-systems/workflows/execute-plan.md +22 -22
- package/deliver-great-systems/workflows/help.md +53 -20
- package/deliver-great-systems/workflows/import-spec.md +65 -7
- package/deliver-great-systems/workflows/init-product.md +45 -167
- package/deliver-great-systems/workflows/new-milestone.md +140 -33
- 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/progress-all.md +133 -0
- package/deliver-great-systems/workflows/quick-abandon.md +89 -0
- package/deliver-great-systems/workflows/quick-complete.md +106 -0
- package/deliver-great-systems/workflows/quick.md +328 -26
- package/deliver-great-systems/workflows/refine-spec.md +1 -1
- package/deliver-great-systems/workflows/research-idea.md +77 -139
- package/deliver-great-systems/workflows/resume-project.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +29 -43
- package/deliver-great-systems/workflows/settings.md +13 -77
- package/deliver-great-systems/workflows/validate-phase.md +39 -1
- package/deliver-great-systems/workflows/verify-work.md +14 -0
- package/deliver-great-systems/workflows/write-spec.md +11 -13
- package/hooks/dist/dgs-enforce-discipline.js +196 -0
- package/package.json +1 -1
- package/scripts/build-hooks.js +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RED test scaffold for REL-01 (Phase 156 plan 02).
|
|
3
|
+
*
|
|
4
|
+
* The `verifyPlanCommit` helper is implemented in plan 02; running this
|
|
5
|
+
* file before plan 02 lands MUST produce 6 failed tests with
|
|
6
|
+
* 'not yet implemented — REL-01' style messages.
|
|
7
|
+
*
|
|
8
|
+
* Behaviour under test:
|
|
9
|
+
* - happy path: planner-reported created_files match HEAD; returns
|
|
10
|
+
* { ok: true, hash: '<short>', missing: [] }
|
|
11
|
+
* - missing-file: planner reports a file not on disk → returns
|
|
12
|
+
* { ok: false, exitLabel: 'plan-commit-incomplete', missing: [...] }
|
|
13
|
+
* - commit_docs:false: silently treated as success
|
|
14
|
+
* ({ ok: true, hash: null, reason: 'skipped_commit_docs_false' })
|
|
15
|
+
* - empty createdFiles: returns plan-commit-incomplete WITHOUT calling
|
|
16
|
+
* cmdCommit (defends against the cmdCommit `['.']` fallback at
|
|
17
|
+
* bin/lib/commands.cjs:321 — Hypothesis C from 156-Q1-FINDINGS.md)
|
|
18
|
+
* - commit failure (cmdCommit returns committed:false with
|
|
19
|
+
* non-skipped reason): returns plan-commit-incomplete
|
|
20
|
+
* - working tree unchanged on plan-commit-incomplete: snapshot of
|
|
21
|
+
* git status --porcelain before/after the failed call MUST match
|
|
22
|
+
*
|
|
23
|
+
* Conventions:
|
|
24
|
+
* - Uses node:test runner + node:assert (matches state-transition-gate.test.cjs)
|
|
25
|
+
* - Each test creates and tears down its own temp git repo via os.tmpdir()
|
|
26
|
+
* - Until plan 02 lands, every test fails with 'not yet implemented'
|
|
27
|
+
* so the file is RED in a controlled way (no parse errors).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const test = require('node:test');
|
|
31
|
+
const assert = require('node:assert');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const os = require('os');
|
|
34
|
+
const path = require('path');
|
|
35
|
+
const { execSync } = require('child_process');
|
|
36
|
+
|
|
37
|
+
const NOT_IMPL = 'verifyPlanCommit not yet implemented — REL-01';
|
|
38
|
+
|
|
39
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function tryRequireCommands() {
|
|
42
|
+
try {
|
|
43
|
+
// Reset the planning-root cache between tests so each makeTempRepo()
|
|
44
|
+
// sees a fresh root; getPlanningRoot caches per-process and would
|
|
45
|
+
// otherwise pin the first temp dir for the whole test run.
|
|
46
|
+
try { require('./paths.cjs').resetPaths(); } catch { /* paths module may not load if commands fails */ }
|
|
47
|
+
return require('./commands.cjs');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeTempRepo() {
|
|
54
|
+
// Resolve real path to handle symlink wrap on macOS (/tmp -> /private/tmp).
|
|
55
|
+
// getPlanningRoot returns the symlink-resolved path; loadConfig later
|
|
56
|
+
// composes it with config.json — they MUST match.
|
|
57
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'commit-verify-test-')));
|
|
58
|
+
execSync('git init --quiet', { cwd: dir });
|
|
59
|
+
execSync('git config user.email test@example.com', { cwd: dir });
|
|
60
|
+
execSync('git config user.name "Test User"', { cwd: dir });
|
|
61
|
+
// Create a config.json with commit_docs: true for verifyPlanCommit's loadConfig path.
|
|
62
|
+
// Stage and commit it FIRST so the seed commit exists, then any later
|
|
63
|
+
// overwrite of config.json (test 3) is just an unstaged edit on top.
|
|
64
|
+
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({ commit_docs: true }, null, 2));
|
|
65
|
+
fs.writeFileSync(path.join(dir, 'README.md'), '# seed\n');
|
|
66
|
+
execSync('git add README.md config.json', { cwd: dir });
|
|
67
|
+
execSync('git commit --quiet -m "seed"', { cwd: dir });
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cleanupRepo(dir) {
|
|
72
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function porcelain(cwd) {
|
|
76
|
+
return execSync('git status --porcelain', { cwd }).toString();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Test 1: happy path ────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
test('REL-01 happy path: orchestrator commits planner-reported created_files and verification passes', () => {
|
|
82
|
+
const cmds = tryRequireCommands();
|
|
83
|
+
if (!cmds || typeof cmds.verifyPlanCommit !== 'function') {
|
|
84
|
+
assert.fail(NOT_IMPL);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const repo = makeTempRepo();
|
|
88
|
+
try {
|
|
89
|
+
const created = ['plan-01.md', 'plan-02.md', 'plan-03.md'];
|
|
90
|
+
for (const f of created) fs.writeFileSync(path.join(repo, f), '# ' + f + '\n');
|
|
91
|
+
const result = cmds.verifyPlanCommit(repo, {
|
|
92
|
+
message: 'docs(99): create phase plan',
|
|
93
|
+
createdFiles: created,
|
|
94
|
+
}, true);
|
|
95
|
+
assert.strictEqual(result.ok, true, 'expected ok:true');
|
|
96
|
+
assert.match(result.hash || '', /^[0-9a-f]{6,}/, 'expected short hash');
|
|
97
|
+
assert.deepStrictEqual(result.missing || [], [], 'no missing files expected');
|
|
98
|
+
} finally {
|
|
99
|
+
cleanupRepo(repo);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── Test 2: missing-file ──────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
test('REL-01 missing-file: planner reported a path that is not in the commit returns plan-commit-incomplete', () => {
|
|
106
|
+
const cmds = tryRequireCommands();
|
|
107
|
+
if (!cmds || typeof cmds.verifyPlanCommit !== 'function') {
|
|
108
|
+
assert.fail(NOT_IMPL);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const repo = makeTempRepo();
|
|
112
|
+
try {
|
|
113
|
+
// Only 2 of 3 reported files exist on disk
|
|
114
|
+
fs.writeFileSync(path.join(repo, 'plan-01.md'), 'a\n');
|
|
115
|
+
fs.writeFileSync(path.join(repo, 'plan-02.md'), 'b\n');
|
|
116
|
+
const created = ['plan-01.md', 'plan-02.md', 'plan-03.md'];
|
|
117
|
+
const result = cmds.verifyPlanCommit(repo, {
|
|
118
|
+
message: 'docs(99): create phase plan',
|
|
119
|
+
createdFiles: created,
|
|
120
|
+
}, true);
|
|
121
|
+
assert.strictEqual(result.ok, false, 'expected ok:false');
|
|
122
|
+
assert.strictEqual(result.exitLabel, 'plan-commit-incomplete');
|
|
123
|
+
assert.ok(Array.isArray(result.missing) && result.missing.length > 0, 'expected missing array');
|
|
124
|
+
assert.ok(result.missing.includes('plan-03.md'), 'missing should list plan-03.md');
|
|
125
|
+
} finally {
|
|
126
|
+
cleanupRepo(repo);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─── Test 3: commit_docs:false silent success ──────────────────────────────
|
|
131
|
+
|
|
132
|
+
test('REL-01 commit_docs:false skipped is treated as success silently (NOT plan-commit-incomplete)', () => {
|
|
133
|
+
const cmds = tryRequireCommands();
|
|
134
|
+
if (!cmds || typeof cmds.verifyPlanCommit !== 'function') {
|
|
135
|
+
assert.fail(NOT_IMPL);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const repo = makeTempRepo();
|
|
139
|
+
try {
|
|
140
|
+
// Override config to disable commit_docs
|
|
141
|
+
fs.writeFileSync(path.join(repo, 'config.json'), JSON.stringify({ commit_docs: false }));
|
|
142
|
+
fs.writeFileSync(path.join(repo, 'plan-01.md'), 'a\n');
|
|
143
|
+
const result = cmds.verifyPlanCommit(repo, {
|
|
144
|
+
message: 'docs(99): create phase plan',
|
|
145
|
+
createdFiles: ['plan-01.md'],
|
|
146
|
+
}, true);
|
|
147
|
+
assert.strictEqual(result.ok, true, 'commit_docs:false MUST NOT be treated as plan-commit-incomplete');
|
|
148
|
+
assert.strictEqual(result.hash, null);
|
|
149
|
+
assert.strictEqual(result.reason, 'skipped_commit_docs_false');
|
|
150
|
+
} finally {
|
|
151
|
+
cleanupRepo(repo);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── Test 4: empty createdFiles list ───────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
test('REL-01 empty created_files list exits plan-commit-incomplete (no [.] fallback)', () => {
|
|
158
|
+
const cmds = tryRequireCommands();
|
|
159
|
+
if (!cmds || typeof cmds.verifyPlanCommit !== 'function') {
|
|
160
|
+
assert.fail(NOT_IMPL);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const repo = makeTempRepo();
|
|
164
|
+
try {
|
|
165
|
+
// Add an extra dirty file in the working tree to detect a `['.']` sweep
|
|
166
|
+
// — if the helper falls back to `['.']` the commit would include this
|
|
167
|
+
// unrelated file. The helper must reject empty createdFiles BEFORE
|
|
168
|
+
// any cmdCommit call.
|
|
169
|
+
fs.writeFileSync(path.join(repo, 'unrelated-edit.md'), 'should not be committed\n');
|
|
170
|
+
const before = porcelain(repo);
|
|
171
|
+
const result = cmds.verifyPlanCommit(repo, {
|
|
172
|
+
message: 'docs(99): create phase plan',
|
|
173
|
+
createdFiles: [],
|
|
174
|
+
}, true);
|
|
175
|
+
assert.strictEqual(result.ok, false);
|
|
176
|
+
assert.strictEqual(result.exitLabel, 'plan-commit-incomplete');
|
|
177
|
+
assert.strictEqual(result.reason, 'empty_created_files');
|
|
178
|
+
// Working tree must be unchanged — no commit should have happened.
|
|
179
|
+
const after = porcelain(repo);
|
|
180
|
+
assert.strictEqual(after, before, 'working tree must be unchanged after empty-list rejection');
|
|
181
|
+
} finally {
|
|
182
|
+
cleanupRepo(repo);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─── Test 5: cmdCommit returns committed:false with non-skipped reason ────
|
|
187
|
+
|
|
188
|
+
test('REL-01 cmdCommit returns committed:false with non-skipped reason exits plan-commit-incomplete', () => {
|
|
189
|
+
const cmds = tryRequireCommands();
|
|
190
|
+
if (!cmds || typeof cmds.verifyPlanCommit !== 'function') {
|
|
191
|
+
assert.fail(NOT_IMPL);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const repo = makeTempRepo();
|
|
195
|
+
try {
|
|
196
|
+
// The reported file does not exist on disk, so cmdCommit's `git add`
|
|
197
|
+
// will fail and the commit will resolve as nothing_to_commit /
|
|
198
|
+
// commit_failed. verifyPlanCommit must surface this as
|
|
199
|
+
// plan-commit-incomplete.
|
|
200
|
+
const result = cmds.verifyPlanCommit(repo, {
|
|
201
|
+
message: 'docs(99): create phase plan',
|
|
202
|
+
createdFiles: ['nonexistent-plan.md'],
|
|
203
|
+
}, true);
|
|
204
|
+
assert.strictEqual(result.ok, false);
|
|
205
|
+
assert.strictEqual(result.exitLabel, 'plan-commit-incomplete');
|
|
206
|
+
} finally {
|
|
207
|
+
cleanupRepo(repo);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ─── Test 6: working tree unchanged on plan-commit-incomplete ─────────────
|
|
212
|
+
|
|
213
|
+
test('REL-01 working tree unchanged on plan-commit-incomplete', () => {
|
|
214
|
+
const cmds = tryRequireCommands();
|
|
215
|
+
if (!cmds || typeof cmds.verifyPlanCommit !== 'function') {
|
|
216
|
+
assert.fail(NOT_IMPL);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const repo = makeTempRepo();
|
|
220
|
+
try {
|
|
221
|
+
// One reported file that does not exist on disk
|
|
222
|
+
const before = porcelain(repo);
|
|
223
|
+
const result = cmds.verifyPlanCommit(repo, {
|
|
224
|
+
message: 'docs(99): create phase plan',
|
|
225
|
+
createdFiles: ['missing.md'],
|
|
226
|
+
}, true);
|
|
227
|
+
assert.strictEqual(result.ok, false);
|
|
228
|
+
const after = porcelain(repo);
|
|
229
|
+
assert.strictEqual(after, before, 'porcelain output must match before/after');
|
|
230
|
+
// No new commit was created (HEAD still points at seed)
|
|
231
|
+
const log = execSync('git log --oneline', { cwd: repo }).toString().trim().split('\n');
|
|
232
|
+
assert.strictEqual(log.length, 1, 'no new commit expected after plan-commit-incomplete');
|
|
233
|
+
} finally {
|
|
234
|
+
cleanupRepo(repo);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
@@ -26,6 +26,7 @@ const LOCAL_KEYS = new Set([
|
|
|
26
26
|
'planningRoot',
|
|
27
27
|
'v2_hint_shown',
|
|
28
28
|
'sync_hint_shown',
|
|
29
|
+
'execution',
|
|
29
30
|
]);
|
|
30
31
|
|
|
31
32
|
const VALID_CONFIG_KEYS = new Set([
|
|
@@ -34,11 +35,28 @@ const VALID_CONFIG_KEYS = new Set([
|
|
|
34
35
|
'workflow.research', 'workflow.plan_check', 'workflow.verifier',
|
|
35
36
|
'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate',
|
|
36
37
|
'workflow._auto_chain_active', 'workflow.discipline', 'workflow.codereview',
|
|
37
|
-
'
|
|
38
|
+
'workflow.four_eyes',
|
|
39
|
+
'git.base_branch',
|
|
38
40
|
'git.sync', 'git.sync_push', 'git.sync_pull',
|
|
39
41
|
'planning.commit_docs', 'planning.search_gitignored',
|
|
42
|
+
'testing.packages.tool',
|
|
43
|
+
'testing.packages.severity_threshold',
|
|
44
|
+
'testing.packages.include_dev_dependencies',
|
|
45
|
+
'testing.packages.timeout_seconds',
|
|
46
|
+
// UAT Bug 2: Snyk org UUID for multi-org accounts. Free-form string or null.
|
|
47
|
+
// Not local-only — shareable setting; config.local.json still wins via _readPackagesConfig merge.
|
|
48
|
+
'testing.packages.snyk_org',
|
|
40
49
|
]);
|
|
41
50
|
|
|
51
|
+
/** Enum: valid values for testing.packages.tool. Pinned to Phase 150 cascade. */
|
|
52
|
+
const VALID_PACKAGES_TOOL = new Set(['auto', 'snyk', 'osv', 'native']);
|
|
53
|
+
|
|
54
|
+
/** Enum: valid values for testing.packages.severity_threshold. */
|
|
55
|
+
const VALID_PACKAGES_SEVERITY = new Set(['critical', 'high', 'medium', 'low']);
|
|
56
|
+
|
|
57
|
+
/** Keys rejected by cmdConfigSet -- require cmdConfigLocalSet (secrets / machine-specific). */
|
|
58
|
+
const LOCAL_ONLY_KEYS = new Set(['testing.packages.snyk_token']);
|
|
59
|
+
|
|
42
60
|
/**
|
|
43
61
|
* Get the path to the shared (tracked) config file.
|
|
44
62
|
*
|
|
@@ -161,9 +179,6 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
161
179
|
model_profile: 'balanced',
|
|
162
180
|
commit_docs: true,
|
|
163
181
|
search_gitignored: false,
|
|
164
|
-
branching_strategy: 'none',
|
|
165
|
-
phase_branch_template: 'dgs/{project}/phase-{phase}-{slug}',
|
|
166
|
-
milestone_branch_template: 'dgs/{project}/{milestone}-{slug}',
|
|
167
182
|
base_branch: 'main',
|
|
168
183
|
workflow: {
|
|
169
184
|
research: true,
|
|
@@ -173,8 +188,8 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
173
188
|
discipline: true,
|
|
174
189
|
},
|
|
175
190
|
git: {
|
|
176
|
-
sync_push: '
|
|
177
|
-
sync_pull: '
|
|
191
|
+
sync_push: 'auto',
|
|
192
|
+
sync_pull: 'auto',
|
|
178
193
|
},
|
|
179
194
|
parallelization: true,
|
|
180
195
|
brave_search: hasBraveSearch,
|
|
@@ -205,6 +220,12 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
205
220
|
error('Usage: config-set <key.path> <value>');
|
|
206
221
|
}
|
|
207
222
|
|
|
223
|
+
// LOCAL_ONLY guard -- runs BEFORE the unknown-key check so the error is precise
|
|
224
|
+
// (PKG-19: testing.packages.snyk_token is a secret; must route through config-local-set)
|
|
225
|
+
if (LOCAL_ONLY_KEYS.has(keyPath)) {
|
|
226
|
+
error(`"${keyPath}" is a local-only key. Use: dgs-tools config-local-set ${keyPath} <value>`);
|
|
227
|
+
}
|
|
228
|
+
|
|
208
229
|
if (!VALID_CONFIG_KEYS.has(keyPath)) {
|
|
209
230
|
error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}`);
|
|
210
231
|
}
|
|
@@ -215,6 +236,24 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
215
236
|
error(`Invalid sync mode: "${value}". Valid values: off, prompt, auto`);
|
|
216
237
|
}
|
|
217
238
|
|
|
239
|
+
// PKG-19: testing.packages.tool enum validator
|
|
240
|
+
if (keyPath === 'testing.packages.tool' && !VALID_PACKAGES_TOOL.has(value)) {
|
|
241
|
+
error(`Invalid packages tool: "${value}". Valid values: auto, snyk, osv, native`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// PKG-19: testing.packages.severity_threshold enum validator
|
|
245
|
+
if (keyPath === 'testing.packages.severity_threshold' && !VALID_PACKAGES_SEVERITY.has(value)) {
|
|
246
|
+
error(`Invalid packages severity: "${value}". Valid values: critical, high, medium, low`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// PKG-19: testing.packages.timeout_seconds integer range validator [10, 3600]
|
|
250
|
+
if (keyPath === 'testing.packages.timeout_seconds') {
|
|
251
|
+
const n = Number(value);
|
|
252
|
+
if (!Number.isInteger(n) || n < 10 || n > 3600) {
|
|
253
|
+
error(`Invalid packages timeout: "${value}". Must be integer between 10 and 3600 seconds`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
218
257
|
// Parse value (handle booleans and numbers)
|
|
219
258
|
let parsedValue = value;
|
|
220
259
|
if (value === 'true') parsedValue = true;
|
|
@@ -265,6 +304,40 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
265
304
|
}
|
|
266
305
|
}
|
|
267
306
|
|
|
307
|
+
/**
|
|
308
|
+
* CLI: Set a key in config.local.json (no VALID_CONFIG_KEYS gate).
|
|
309
|
+
* Used by workflows to write local-only fields like execution.active_context.
|
|
310
|
+
*/
|
|
311
|
+
function cmdConfigLocalSet(cwd, keyPath, value, raw) {
|
|
312
|
+
if (!keyPath) {
|
|
313
|
+
error('Usage: config-local-set <key.path> <value>');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let parsedValue = value;
|
|
317
|
+
if (value === 'true') parsedValue = true;
|
|
318
|
+
else if (value === 'false') parsedValue = false;
|
|
319
|
+
else if (value === 'null') parsedValue = null;
|
|
320
|
+
else if (!isNaN(value) && value !== '') parsedValue = Number(value);
|
|
321
|
+
|
|
322
|
+
const localPath = getLocalConfigPath(cwd);
|
|
323
|
+
let config = _readJsonSafe(localPath);
|
|
324
|
+
|
|
325
|
+
const keys = keyPath.split('.');
|
|
326
|
+
let current = config;
|
|
327
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
328
|
+
const key = keys[i];
|
|
329
|
+
if (current[key] === undefined || typeof current[key] !== 'object') {
|
|
330
|
+
current[key] = {};
|
|
331
|
+
}
|
|
332
|
+
current = current[key];
|
|
333
|
+
}
|
|
334
|
+
current[keys[keys.length - 1]] = parsedValue;
|
|
335
|
+
|
|
336
|
+
_writeJson(localPath, config);
|
|
337
|
+
const result = { updated: true, key: keyPath, value: parsedValue, file: 'config.local.json' };
|
|
338
|
+
output(result, raw, `${keyPath}=${parsedValue}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
268
341
|
function cmdConfigGet(cwd, keyPath, raw) {
|
|
269
342
|
if (!keyPath) {
|
|
270
343
|
error('Usage: config-get <key.path>');
|
|
@@ -520,6 +593,7 @@ module.exports = {
|
|
|
520
593
|
getReviewKeysPath,
|
|
521
594
|
cmdConfigEnsureSection,
|
|
522
595
|
cmdConfigSet,
|
|
596
|
+
cmdConfigLocalSet,
|
|
523
597
|
cmdConfigGet,
|
|
524
598
|
writeConfigField,
|
|
525
599
|
loadReviewConfig,
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.test.cjs -- Unit tests for config validators, including Phase 150
|
|
3
|
+
* testing.packages.* extensions (PKG-19).
|
|
4
|
+
*
|
|
5
|
+
* Phase 150 Plan 01 -- testing.packages.* config key validators + LOCAL_ONLY guard.
|
|
6
|
+
* Verifies:
|
|
7
|
+
* - four testing.packages.* keys accepted by VALID_CONFIG_KEYS
|
|
8
|
+
* - VALID_PACKAGES_TOOL enum validation (auto/snyk/osv/native)
|
|
9
|
+
* - VALID_PACKAGES_SEVERITY enum validation (critical/high/medium/low)
|
|
10
|
+
* - include_dev_dependencies boolean coercion
|
|
11
|
+
* - timeout_seconds integer range [10, 3600]
|
|
12
|
+
* - LOCAL_ONLY_KEYS guard redirects snyk_token to config-local-set
|
|
13
|
+
* - no regression on pre-existing keys (workflow.research, git.sync_push, unknown keys)
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
17
|
+
const assert = require('node:assert');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
const { cmdConfigSet, cmdConfigLocalSet } = require('./config.cjs');
|
|
24
|
+
const { loadConfig } = require('./core.cjs');
|
|
25
|
+
const { resetPaths } = require('./paths.cjs');
|
|
26
|
+
|
|
27
|
+
// ─── Harness ──────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Intercept process.exit and stderr writes during a cmdConfig* call.
|
|
31
|
+
* Returns { exitCode, stderr, stdout, threw }.
|
|
32
|
+
*
|
|
33
|
+
* process.exit only throws for non-zero codes (to escape error()); for exit(0),
|
|
34
|
+
* we throw a distinct __EXIT0__ marker that cmdConfigSet's try/catch would
|
|
35
|
+
* otherwise intercept. To prevent that, we only throw on non-zero; exit(0) sets
|
|
36
|
+
* the flag and the output() call naturally returns to the caller via the thrown
|
|
37
|
+
* marker which we then swallow at the outermost try.
|
|
38
|
+
*/
|
|
39
|
+
function interceptExit(fn) {
|
|
40
|
+
let exitCode = null;
|
|
41
|
+
let stderr = '';
|
|
42
|
+
let stdout = '';
|
|
43
|
+
let threw = null;
|
|
44
|
+
const origExit = process.exit;
|
|
45
|
+
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
46
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
47
|
+
process.exit = (code) => {
|
|
48
|
+
exitCode = code;
|
|
49
|
+
// For success exits (code 0) we return silently so cmdConfigSet's outer
|
|
50
|
+
// try/catch around _writeJson + output() doesn't mistake our throw for a
|
|
51
|
+
// write failure. For non-zero exits (triggered by error()), we throw a
|
|
52
|
+
// sentinel so the error call does not proceed further.
|
|
53
|
+
if (code === 0) return;
|
|
54
|
+
const err = new Error('__EXIT__' + code);
|
|
55
|
+
err.__isExitSentinel = true;
|
|
56
|
+
throw err;
|
|
57
|
+
};
|
|
58
|
+
process.stderr.write = (data) => { stderr += String(data); return true; };
|
|
59
|
+
process.stdout.write = (data) => { stdout += String(data); return true; };
|
|
60
|
+
try {
|
|
61
|
+
fn();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (!e || !e.__isExitSentinel) {
|
|
64
|
+
threw = e;
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
process.exit = origExit;
|
|
68
|
+
process.stderr.write = origStderrWrite;
|
|
69
|
+
process.stdout.write = origStdoutWrite;
|
|
70
|
+
}
|
|
71
|
+
return { exitCode, stderr, stdout, threw };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeTmpDir() {
|
|
75
|
+
// mkdtempSync + realpath resolves macOS /private/var -> /var symlinks so
|
|
76
|
+
// getPlanningRoot's git output matches the dir we're operating in.
|
|
77
|
+
const d = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-config-test-')));
|
|
78
|
+
// getPlanningRoot requires a git repo
|
|
79
|
+
execSync('git init -q', { cwd: d });
|
|
80
|
+
execSync('git config user.email "t@t" && git config user.name "t"', { cwd: d });
|
|
81
|
+
// Reset cached planning root so each test's fresh tmpDir is honoured
|
|
82
|
+
resetPaths();
|
|
83
|
+
return d;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeMinimalConfig(tmpDir) {
|
|
87
|
+
fs.writeFileSync(path.join(tmpDir, 'config.json'), JSON.stringify({
|
|
88
|
+
current_project: 'testproj',
|
|
89
|
+
}, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readSharedConfig(tmpDir) {
|
|
93
|
+
const p = path.join(tmpDir, 'config.json');
|
|
94
|
+
if (!fs.existsSync(p)) return {};
|
|
95
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readLocalConfig(tmpDir) {
|
|
99
|
+
const p = path.join(tmpDir, 'config.local.json');
|
|
100
|
+
if (!fs.existsSync(p)) return {};
|
|
101
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
describe('cmdConfigSet - testing.packages.tool validator (PKG-19)', () => {
|
|
107
|
+
let tmpDir;
|
|
108
|
+
beforeEach(() => { tmpDir = makeTmpDir(); writeMinimalConfig(tmpDir); });
|
|
109
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
110
|
+
|
|
111
|
+
test('accepts "auto"', () => {
|
|
112
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.tool', 'auto', true));
|
|
113
|
+
assert.strictEqual(r.exitCode, 0);
|
|
114
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.tool, 'auto');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('accepts "snyk"', () => {
|
|
118
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.tool', 'snyk', true));
|
|
119
|
+
assert.strictEqual(r.exitCode, 0);
|
|
120
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.tool, 'snyk');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('accepts "osv"', () => {
|
|
124
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.tool', 'osv', true));
|
|
125
|
+
assert.strictEqual(r.exitCode, 0);
|
|
126
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.tool, 'osv');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('accepts "native"', () => {
|
|
130
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.tool', 'native', true));
|
|
131
|
+
assert.strictEqual(r.exitCode, 0);
|
|
132
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.tool, 'native');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('rejects "bogus"', () => {
|
|
136
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.tool', 'bogus', true));
|
|
137
|
+
assert.strictEqual(r.exitCode, 1);
|
|
138
|
+
assert.match(r.stderr, /Invalid packages tool/);
|
|
139
|
+
assert.match(r.stderr, /auto, snyk, osv, native/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('rejects empty string', () => {
|
|
143
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.tool', '', true));
|
|
144
|
+
assert.strictEqual(r.exitCode, 1);
|
|
145
|
+
assert.match(r.stderr, /Invalid packages tool/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('cmdConfigSet - testing.packages.severity_threshold validator (PKG-19)', () => {
|
|
150
|
+
let tmpDir;
|
|
151
|
+
beforeEach(() => { tmpDir = makeTmpDir(); writeMinimalConfig(tmpDir); });
|
|
152
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
153
|
+
|
|
154
|
+
test('accepts "critical"', () => {
|
|
155
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.severity_threshold', 'critical', true));
|
|
156
|
+
assert.strictEqual(r.exitCode, 0);
|
|
157
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.severity_threshold, 'critical');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('accepts "high"', () => {
|
|
161
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.severity_threshold', 'high', true));
|
|
162
|
+
assert.strictEqual(r.exitCode, 0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('accepts "medium"', () => {
|
|
166
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.severity_threshold', 'medium', true));
|
|
167
|
+
assert.strictEqual(r.exitCode, 0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('accepts "low"', () => {
|
|
171
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.severity_threshold', 'low', true));
|
|
172
|
+
assert.strictEqual(r.exitCode, 0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('rejects "wat"', () => {
|
|
176
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.severity_threshold', 'wat', true));
|
|
177
|
+
assert.strictEqual(r.exitCode, 1);
|
|
178
|
+
assert.match(r.stderr, /Invalid packages severity/);
|
|
179
|
+
assert.match(r.stderr, /critical, high, medium, low/);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('cmdConfigSet - testing.packages.include_dev_dependencies (PKG-19)', () => {
|
|
184
|
+
let tmpDir;
|
|
185
|
+
beforeEach(() => { tmpDir = makeTmpDir(); writeMinimalConfig(tmpDir); });
|
|
186
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
187
|
+
|
|
188
|
+
test('accepts "true" and stores as boolean true', () => {
|
|
189
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.include_dev_dependencies', 'true', true));
|
|
190
|
+
assert.strictEqual(r.exitCode, 0);
|
|
191
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.include_dev_dependencies, true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('accepts "false" and stores as boolean false', () => {
|
|
195
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.include_dev_dependencies', 'false', true));
|
|
196
|
+
assert.strictEqual(r.exitCode, 0);
|
|
197
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.include_dev_dependencies, false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('cmdConfigSet - testing.packages.timeout_seconds range validator (PKG-19)', () => {
|
|
202
|
+
let tmpDir;
|
|
203
|
+
beforeEach(() => { tmpDir = makeTmpDir(); writeMinimalConfig(tmpDir); });
|
|
204
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
205
|
+
|
|
206
|
+
test('accepts 300 (default)', () => {
|
|
207
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.timeout_seconds', '300', true));
|
|
208
|
+
assert.strictEqual(r.exitCode, 0);
|
|
209
|
+
assert.strictEqual(readSharedConfig(tmpDir).testing.packages.timeout_seconds, 300);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('accepts 10 (min)', () => {
|
|
213
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.timeout_seconds', '10', true));
|
|
214
|
+
assert.strictEqual(r.exitCode, 0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('accepts 3600 (max)', () => {
|
|
218
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.timeout_seconds', '3600', true));
|
|
219
|
+
assert.strictEqual(r.exitCode, 0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('rejects 5 (below min)', () => {
|
|
223
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.timeout_seconds', '5', true));
|
|
224
|
+
assert.strictEqual(r.exitCode, 1);
|
|
225
|
+
assert.match(r.stderr, /Invalid packages timeout/);
|
|
226
|
+
assert.match(r.stderr, /between 10 and 3600/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('rejects 7200 (above max)', () => {
|
|
230
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.timeout_seconds', '7200', true));
|
|
231
|
+
assert.strictEqual(r.exitCode, 1);
|
|
232
|
+
assert.match(r.stderr, /Invalid packages timeout/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('rejects non-integer string (e.g., "abc")', () => {
|
|
236
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.timeout_seconds', 'abc', true));
|
|
237
|
+
assert.strictEqual(r.exitCode, 1);
|
|
238
|
+
assert.match(r.stderr, /Invalid packages timeout/);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('cmdConfigSet - LOCAL_ONLY_KEYS guard for snyk_token (PKG-19, success criterion 5)', () => {
|
|
243
|
+
let tmpDir;
|
|
244
|
+
beforeEach(() => { tmpDir = makeTmpDir(); writeMinimalConfig(tmpDir); });
|
|
245
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
246
|
+
|
|
247
|
+
test('rejects testing.packages.snyk_token with local-only error message', () => {
|
|
248
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.snyk_token', 'abc123', true));
|
|
249
|
+
assert.strictEqual(r.exitCode, 1);
|
|
250
|
+
assert.match(r.stderr, /local-only/);
|
|
251
|
+
assert.match(r.stderr, /config-local-set/);
|
|
252
|
+
assert.match(r.stderr, /testing\.packages\.snyk_token/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('rejection message does NOT contain "Unknown config key" (guard fires BEFORE membership check)', () => {
|
|
256
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.snyk_token', 'abc123', true));
|
|
257
|
+
assert.strictEqual(r.exitCode, 1);
|
|
258
|
+
assert.doesNotMatch(r.stderr, /Unknown config key/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('rejection happens even when value is empty string', () => {
|
|
262
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.packages.snyk_token', '', true));
|
|
263
|
+
assert.strictEqual(r.exitCode, 1);
|
|
264
|
+
assert.match(r.stderr, /local-only/);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('cmdConfigLocalSet - accepts snyk_token unchanged', () => {
|
|
269
|
+
let tmpDir;
|
|
270
|
+
beforeEach(() => { tmpDir = makeTmpDir(); writeMinimalConfig(tmpDir); });
|
|
271
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
272
|
+
|
|
273
|
+
test('cmdConfigLocalSet writes snyk_token to config.local.json', () => {
|
|
274
|
+
const r = interceptExit(() => cmdConfigLocalSet(tmpDir, 'testing.packages.snyk_token', 'abc123', true));
|
|
275
|
+
assert.strictEqual(r.exitCode, 0);
|
|
276
|
+
const local = readLocalConfig(tmpDir);
|
|
277
|
+
assert.strictEqual(local.testing.packages.snyk_token, 'abc123');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('value is readable back from config.local.json on disk', () => {
|
|
281
|
+
interceptExit(() => cmdConfigLocalSet(tmpDir, 'testing.packages.snyk_token', 'abc123', true));
|
|
282
|
+
const local = readLocalConfig(tmpDir);
|
|
283
|
+
assert.strictEqual(local.testing.packages.snyk_token, 'abc123');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('cmdConfigSet - no regression on existing keys', () => {
|
|
288
|
+
let tmpDir;
|
|
289
|
+
beforeEach(() => { tmpDir = makeTmpDir(); writeMinimalConfig(tmpDir); });
|
|
290
|
+
afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
|
|
291
|
+
|
|
292
|
+
test('workflow.research still accepts true', () => {
|
|
293
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'workflow.research', 'true', true));
|
|
294
|
+
assert.strictEqual(r.exitCode, 0);
|
|
295
|
+
assert.strictEqual(readSharedConfig(tmpDir).workflow.research, true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('git.sync_push still rejects invalid sync mode', () => {
|
|
299
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'git.sync_push', 'bogus', true));
|
|
300
|
+
assert.strictEqual(r.exitCode, 1);
|
|
301
|
+
assert.match(r.stderr, /Invalid sync mode/);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('Unknown key (e.g., testing.unknown) still rejects with "Unknown config key"', () => {
|
|
305
|
+
const r = interceptExit(() => cmdConfigSet(tmpDir, 'testing.unknown', 'x', true));
|
|
306
|
+
assert.strictEqual(r.exitCode, 1);
|
|
307
|
+
assert.match(r.stderr, /Unknown config key/);
|
|
308
|
+
});
|
|
309
|
+
});
|