delimit-cli 2.4.0 → 3.0.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/.dockerignore +7 -0
- package/.github/workflows/ci.yml +22 -0
- package/CODE_OF_CONDUCT.md +48 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +9 -0
- package/LICENSE +21 -0
- package/README.md +18 -69
- package/SECURITY.md +42 -0
- package/adapters/gemini-forge.js +11 -0
- package/adapters/gemini-jamsons.js +152 -0
- package/bin/delimit-cli.js +8 -0
- package/bin/delimit-setup.js +258 -0
- package/gateway/ai/backends/__init__.py +0 -0
- package/gateway/ai/backends/async_utils.py +21 -0
- package/gateway/ai/backends/deploy_bridge.py +150 -0
- package/gateway/ai/backends/gateway_core.py +261 -0
- package/gateway/ai/backends/generate_bridge.py +38 -0
- package/gateway/ai/backends/governance_bridge.py +196 -0
- package/gateway/ai/backends/intel_bridge.py +59 -0
- package/gateway/ai/backends/memory_bridge.py +93 -0
- package/gateway/ai/backends/ops_bridge.py +137 -0
- package/gateway/ai/backends/os_bridge.py +82 -0
- package/gateway/ai/backends/repo_bridge.py +117 -0
- package/gateway/ai/backends/ui_bridge.py +118 -0
- package/gateway/ai/backends/vault_bridge.py +129 -0
- package/gateway/ai/server.py +1182 -0
- package/gateway/core/__init__.py +3 -0
- package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
- package/gateway/core/auto_baseline.py +304 -0
- package/gateway/core/ci_formatter.py +283 -0
- package/gateway/core/complexity_analyzer.py +386 -0
- package/gateway/core/contract_ledger.py +345 -0
- package/gateway/core/dependency_graph.py +218 -0
- package/gateway/core/dependency_manifest.py +223 -0
- package/gateway/core/diff_engine_v2.py +477 -0
- package/gateway/core/diff_engine_v2.py.bak +426 -0
- package/gateway/core/event_backbone.py +268 -0
- package/gateway/core/event_schema.py +258 -0
- package/gateway/core/explainer.py +438 -0
- package/gateway/core/gateway.py +128 -0
- package/gateway/core/gateway_v2.py +154 -0
- package/gateway/core/gateway_v3.py +224 -0
- package/gateway/core/impact_analyzer.py +163 -0
- package/gateway/core/policies/default.yml +13 -0
- package/gateway/core/policies/relaxed.yml +48 -0
- package/gateway/core/policies/strict.yml +55 -0
- package/gateway/core/policy_engine.py +464 -0
- package/gateway/core/registry.py +52 -0
- package/gateway/core/registry_v2.py +132 -0
- package/gateway/core/registry_v3.py +134 -0
- package/gateway/core/semver_classifier.py +152 -0
- package/gateway/core/spec_detector.py +130 -0
- package/gateway/core/surface_bridge.py +307 -0
- package/gateway/core/zero_spec/__init__.py +4 -0
- package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/detector.py +353 -0
- package/gateway/core/zero_spec/express_extractor.py +483 -0
- package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
- package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
- package/gateway/tasks/__init__.py +1 -0
- package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/check_policy.py +177 -0
- package/gateway/tasks/check_policy_v2.py +255 -0
- package/gateway/tasks/check_policy_v3.py +255 -0
- package/gateway/tasks/explain_diff.py +305 -0
- package/gateway/tasks/explain_diff_v2.py +267 -0
- package/gateway/tasks/validate_api.py +131 -0
- package/gateway/tasks/validate_api_v2.py +208 -0
- package/gateway/tasks/validate_api_v3.py +163 -0
- package/package.json +2 -2
- package/adapters/codex-skill.js +0 -87
- package/adapters/cursor-extension.js +0 -190
- package/adapters/gemini-action.js +0 -93
- package/adapters/openai-function.js +0 -112
- package/adapters/xai-plugin.js +0 -151
- package/test-decision-engine.js +0 -181
- package/test-hook.js +0 -27
- package/tests/cli.test.js +0 -359
- package/tests/fixtures/openapi-changed.yaml +0 -56
- package/tests/fixtures/openapi.yaml +0 -87
package/test-decision-engine.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const DecisionEngine = require('./lib/decision-engine');
|
|
4
|
-
const axios = require('axios');
|
|
5
|
-
|
|
6
|
-
async function runTests() {
|
|
7
|
-
console.log('=== DECISION ENGINE HOSTILE VERIFICATION ===\n');
|
|
8
|
-
|
|
9
|
-
// First restore good policy and restart agent
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const goodPolicy = `defaultMode: advisory
|
|
12
|
-
|
|
13
|
-
rules:
|
|
14
|
-
- name: "Production Protection"
|
|
15
|
-
mode: enforce
|
|
16
|
-
triggers:
|
|
17
|
-
- gitBranch: [main, master, production]
|
|
18
|
-
|
|
19
|
-
- name: "Payment Code Security"
|
|
20
|
-
mode: enforce
|
|
21
|
-
triggers:
|
|
22
|
-
- path: "**/payment/**"
|
|
23
|
-
- content: ["stripe", "payment", "billing"]
|
|
24
|
-
|
|
25
|
-
- name: "Documentation Freedom"
|
|
26
|
-
mode: advisory
|
|
27
|
-
triggers:
|
|
28
|
-
- path: "**/*.md"
|
|
29
|
-
final: true`;
|
|
30
|
-
|
|
31
|
-
fs.writeFileSync('delimit.yml', goodPolicy);
|
|
32
|
-
|
|
33
|
-
// Kill existing agent
|
|
34
|
-
try {
|
|
35
|
-
require('child_process').execSync('pkill -f "node lib/agent.js"');
|
|
36
|
-
} catch(e) {}
|
|
37
|
-
|
|
38
|
-
// Start fresh agent
|
|
39
|
-
const agent = require('child_process').spawn('node', ['lib/agent.js'], {
|
|
40
|
-
detached: true,
|
|
41
|
-
stdio: 'ignore'
|
|
42
|
-
});
|
|
43
|
-
agent.unref();
|
|
44
|
-
|
|
45
|
-
// Wait for agent
|
|
46
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
47
|
-
|
|
48
|
-
const tests = [
|
|
49
|
-
{
|
|
50
|
-
name: 'TEST 1: Documentation file -> Advisory',
|
|
51
|
-
context: {
|
|
52
|
-
command: 'pre-commit',
|
|
53
|
-
pwd: '/test',
|
|
54
|
-
gitBranch: 'feature',
|
|
55
|
-
files: ['README.md', 'docs/api.md'],
|
|
56
|
-
diff: 'documentation changes'
|
|
57
|
-
},
|
|
58
|
-
expected: 'advisory'
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: 'TEST 2: Payment path -> Enforce',
|
|
62
|
-
context: {
|
|
63
|
-
command: 'pre-commit',
|
|
64
|
-
pwd: '/test',
|
|
65
|
-
gitBranch: 'feature',
|
|
66
|
-
files: ['lib/payment/stripe.js'],
|
|
67
|
-
diff: 'payment code changes'
|
|
68
|
-
},
|
|
69
|
-
expected: 'enforce'
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'TEST 3: Main branch -> Enforce',
|
|
73
|
-
context: {
|
|
74
|
-
command: 'pre-commit',
|
|
75
|
-
pwd: '/test',
|
|
76
|
-
gitBranch: 'main',
|
|
77
|
-
files: ['lib/utils.js'],
|
|
78
|
-
diff: 'utility changes'
|
|
79
|
-
},
|
|
80
|
-
expected: 'enforce'
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
name: 'TEST 4: No match -> Default advisory',
|
|
84
|
-
context: {
|
|
85
|
-
command: 'pre-commit',
|
|
86
|
-
pwd: '/test',
|
|
87
|
-
gitBranch: 'feature',
|
|
88
|
-
files: ['lib/utils.js'],
|
|
89
|
-
diff: 'regular code'
|
|
90
|
-
},
|
|
91
|
-
expected: 'advisory'
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: 'TEST 5: Conflicting rules -> Stronger wins',
|
|
95
|
-
context: {
|
|
96
|
-
command: 'pre-commit',
|
|
97
|
-
pwd: '/test',
|
|
98
|
-
gitBranch: 'main',
|
|
99
|
-
files: ['README.md'],
|
|
100
|
-
diff: 'readme on main branch'
|
|
101
|
-
},
|
|
102
|
-
expected: 'enforce' // Production Protection should win over Documentation Freedom
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: 'TEST 6: Determinism check (repeat test 2)',
|
|
106
|
-
context: {
|
|
107
|
-
command: 'pre-commit',
|
|
108
|
-
pwd: '/test',
|
|
109
|
-
gitBranch: 'feature',
|
|
110
|
-
files: ['lib/payment/stripe.js'],
|
|
111
|
-
diff: 'payment code changes'
|
|
112
|
-
},
|
|
113
|
-
expected: 'enforce'
|
|
114
|
-
}
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
const results = [];
|
|
118
|
-
for (const test of tests) {
|
|
119
|
-
try {
|
|
120
|
-
const response = await axios.post('http://127.0.0.1:7823/evaluate', test.context);
|
|
121
|
-
const decision = response.data;
|
|
122
|
-
|
|
123
|
-
const result = {
|
|
124
|
-
test: test.name,
|
|
125
|
-
expected: test.expected,
|
|
126
|
-
actual: decision.mode,
|
|
127
|
-
action: decision.action,
|
|
128
|
-
rule: decision.rule,
|
|
129
|
-
pass: decision.mode === test.expected
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
results.push(result);
|
|
133
|
-
console.log(`${test.name}`);
|
|
134
|
-
console.log(` Expected: ${test.expected}, Actual: ${decision.mode}`);
|
|
135
|
-
console.log(` Rule: ${decision.rule || 'none'}`);
|
|
136
|
-
console.log(` Status: ${result.pass ? '✅ PASS' : '❌ FAIL'}\n`);
|
|
137
|
-
|
|
138
|
-
// Get explanation for this decision
|
|
139
|
-
const explainResponse = await axios.get('http://127.0.0.1:7823/explain/last');
|
|
140
|
-
if (explainResponse.data.explanation) {
|
|
141
|
-
console.log(' Explanation quality check:');
|
|
142
|
-
const exp = explainResponse.data.explanation;
|
|
143
|
-
console.log(` - Has decision ID: ${exp.includes('Decision ID:') ? '✓' : '✗'}`);
|
|
144
|
-
console.log(` - Has effective mode: ${exp.includes('Effective:') ? '✓' : '✗'}`);
|
|
145
|
-
console.log(` - Has matched rules: ${exp.includes('MATCHED RULES') || exp.includes('No matching rules') ? '✓' : '✗'}`);
|
|
146
|
-
console.log(` - Has context: ${exp.includes('CONTEXT') ? '✓' : '✗'}\n`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
} catch (e) {
|
|
150
|
-
results.push({
|
|
151
|
-
test: test.name,
|
|
152
|
-
expected: test.expected,
|
|
153
|
-
actual: 'ERROR',
|
|
154
|
-
error: e.message,
|
|
155
|
-
pass: false
|
|
156
|
-
});
|
|
157
|
-
console.log(`${test.name}: ❌ ERROR - ${e.message}\n`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Summary
|
|
162
|
-
console.log('=== SUMMARY ===');
|
|
163
|
-
const passed = results.filter(r => r.pass).length;
|
|
164
|
-
console.log(`Passed: ${passed}/${results.length}`);
|
|
165
|
-
|
|
166
|
-
// Check determinism
|
|
167
|
-
if (results[1].actual === results[5].actual && results[1].rule === results[5].rule) {
|
|
168
|
-
console.log('✅ DETERMINISM CHECK: Same input produced same output');
|
|
169
|
-
} else {
|
|
170
|
-
console.log('❌ DETERMINISM CHECK: Same input produced different outputs!');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Kill agent
|
|
174
|
-
try {
|
|
175
|
-
require('child_process').execSync('pkill -f "node lib/agent.js"');
|
|
176
|
-
} catch(e) {}
|
|
177
|
-
|
|
178
|
-
process.exit(passed === results.length ? 0 : 1);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
runTests().catch(console.error);
|
package/test-hook.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Test script to simulate a Git pre-commit hook
|
|
4
|
-
const axios = require('axios');
|
|
5
|
-
|
|
6
|
-
async function testHook() {
|
|
7
|
-
const context = {
|
|
8
|
-
command: 'pre-commit',
|
|
9
|
-
pwd: '/home/delimit/npm-delimit',
|
|
10
|
-
gitBranch: 'main',
|
|
11
|
-
files: ['lib/payment/stripe.js', 'README.md'],
|
|
12
|
-
diff: 'diff --git a/lib/payment/stripe.js\n+const stripe = require("stripe");'
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const response = await axios.post('http://127.0.0.1:7823/evaluate', context);
|
|
17
|
-
console.log('Decision:', response.data);
|
|
18
|
-
|
|
19
|
-
// Now test the explain endpoint
|
|
20
|
-
const explainResponse = await axios.get('http://127.0.0.1:7823/explain/last');
|
|
21
|
-
console.log('\n' + explainResponse.data.explanation);
|
|
22
|
-
} catch (e) {
|
|
23
|
-
console.error('Error:', e.message);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
testHook();
|
package/tests/cli.test.js
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Delimit CLI Tests
|
|
3
|
-
*
|
|
4
|
-
* Uses Node.js built-in test runner (node:test).
|
|
5
|
-
* Run with: node --test tests/cli.test.js
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { describe, it, before, after } = require('node:test');
|
|
9
|
-
const assert = require('node:assert/strict');
|
|
10
|
-
const { execSync } = require('node:child_process');
|
|
11
|
-
const path = require('node:path');
|
|
12
|
-
const fs = require('node:fs');
|
|
13
|
-
const os = require('node:os');
|
|
14
|
-
|
|
15
|
-
const CLI = path.join(__dirname, '..', 'bin', 'delimit-cli.js');
|
|
16
|
-
const FIXTURES = path.join(__dirname, 'fixtures');
|
|
17
|
-
const SPEC_CLEAN = path.join(FIXTURES, 'openapi.yaml');
|
|
18
|
-
const SPEC_BREAKING = path.join(FIXTURES, 'openapi-changed.yaml');
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Run the CLI and return { stdout, stderr, exitCode }.
|
|
22
|
-
* Does not throw on non-zero exit.
|
|
23
|
-
*/
|
|
24
|
-
function run(args, opts = {}) {
|
|
25
|
-
const cwd = opts.cwd || os.tmpdir();
|
|
26
|
-
try {
|
|
27
|
-
const stdout = execSync(`node "${CLI}" ${args}`, {
|
|
28
|
-
cwd,
|
|
29
|
-
encoding: 'utf-8',
|
|
30
|
-
timeout: 30000,
|
|
31
|
-
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
32
|
-
});
|
|
33
|
-
return { stdout, stderr: '', exitCode: 0 };
|
|
34
|
-
} catch (err) {
|
|
35
|
-
return {
|
|
36
|
-
stdout: err.stdout || '',
|
|
37
|
-
stderr: err.stderr || '',
|
|
38
|
-
exitCode: err.status ?? 1,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// 1. CLI entry point loads without error
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
describe('CLI entry point', () => {
|
|
47
|
-
it('loads the module without throwing', () => {
|
|
48
|
-
assert.doesNotThrow(() => {
|
|
49
|
-
execSync(`node --check "${CLI}"`, { encoding: 'utf-8' });
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// 2. --version returns the version from package.json
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
describe('--version', () => {
|
|
58
|
-
it('prints the version from package.json', () => {
|
|
59
|
-
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
60
|
-
const { stdout } = run('--version');
|
|
61
|
-
assert.equal(stdout.trim(), pkg.version);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// 3. --help works at top level
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
describe('--help (top level)', () => {
|
|
69
|
-
it('shows usage and lists commands', () => {
|
|
70
|
-
const { stdout } = run('--help');
|
|
71
|
-
assert.match(stdout, /Usage:/);
|
|
72
|
-
assert.match(stdout, /init/);
|
|
73
|
-
assert.match(stdout, /lint/);
|
|
74
|
-
assert.match(stdout, /diff/);
|
|
75
|
-
assert.match(stdout, /explain/);
|
|
76
|
-
assert.match(stdout, /doctor/);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// 4. Each command has help text
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
describe('subcommand --help', () => {
|
|
84
|
-
for (const cmd of ['init', 'lint', 'diff', 'explain', 'doctor']) {
|
|
85
|
-
it(`"help ${cmd}" shows description`, () => {
|
|
86
|
-
const { stdout } = run(`help ${cmd}`);
|
|
87
|
-
assert.match(stdout, /Usage:/);
|
|
88
|
-
assert.match(stdout, new RegExp(cmd));
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// 5. init creates .delimit/policies.yml with default preset
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
describe('init command', () => {
|
|
97
|
-
let tmpDir;
|
|
98
|
-
|
|
99
|
-
before(() => {
|
|
100
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'delimit-test-init-'));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
after(() => {
|
|
104
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('creates .delimit/policies.yml with default preset', () => {
|
|
108
|
-
const { stdout, exitCode } = run('init', { cwd: tmpDir });
|
|
109
|
-
assert.equal(exitCode, 0);
|
|
110
|
-
const policyPath = path.join(tmpDir, '.delimit', 'policies.yml');
|
|
111
|
-
assert.ok(fs.existsSync(policyPath), 'policies.yml should exist');
|
|
112
|
-
const content = fs.readFileSync(policyPath, 'utf-8');
|
|
113
|
-
assert.match(content, /Delimit Policy Preset: default/);
|
|
114
|
-
assert.match(content, /override_defaults: false/);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('reports already initialized on second run', () => {
|
|
118
|
-
const { stdout, exitCode } = run('init', { cwd: tmpDir });
|
|
119
|
-
assert.equal(exitCode, 0);
|
|
120
|
-
assert.match(stdout, /Already initialized/);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('init --preset strict', () => {
|
|
125
|
-
let tmpDir;
|
|
126
|
-
|
|
127
|
-
before(() => {
|
|
128
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'delimit-test-init-strict-'));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
after(() => {
|
|
132
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('creates policies.yml with strict preset', () => {
|
|
136
|
-
const { stdout, exitCode } = run('init --preset strict', { cwd: tmpDir });
|
|
137
|
-
assert.equal(exitCode, 0);
|
|
138
|
-
const content = fs.readFileSync(
|
|
139
|
-
path.join(tmpDir, '.delimit', 'policies.yml'),
|
|
140
|
-
'utf-8'
|
|
141
|
-
);
|
|
142
|
-
assert.match(content, /Delimit Policy Preset: strict/);
|
|
143
|
-
assert.match(content, /no_endpoint_removal/);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('init --preset relaxed', () => {
|
|
148
|
-
let tmpDir;
|
|
149
|
-
|
|
150
|
-
before(() => {
|
|
151
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'delimit-test-init-relaxed-'));
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
after(() => {
|
|
155
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('creates policies.yml with relaxed preset', () => {
|
|
159
|
-
const { stdout, exitCode } = run('init --preset relaxed', { cwd: tmpDir });
|
|
160
|
-
assert.equal(exitCode, 0);
|
|
161
|
-
const content = fs.readFileSync(
|
|
162
|
-
path.join(tmpDir, '.delimit', 'policies.yml'),
|
|
163
|
-
'utf-8'
|
|
164
|
-
);
|
|
165
|
-
assert.match(content, /Delimit Policy Preset: relaxed/);
|
|
166
|
-
assert.match(content, /warn_endpoint_removal/);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
// 6. lint with identical specs returns clean (exit 0)
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
describe('lint (clean)', () => {
|
|
174
|
-
it('returns exit 0 with no violations for identical specs', () => {
|
|
175
|
-
const { stdout, exitCode } = run(
|
|
176
|
-
`lint "${SPEC_CLEAN}" "${SPEC_CLEAN}" --json`
|
|
177
|
-
);
|
|
178
|
-
assert.equal(exitCode, 0);
|
|
179
|
-
const result = JSON.parse(stdout);
|
|
180
|
-
assert.equal(result.decision, 'pass');
|
|
181
|
-
assert.equal(result.exit_code, 0);
|
|
182
|
-
assert.equal(result.summary.breaking_changes, 0);
|
|
183
|
-
assert.equal(result.violations.length, 0);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// 7. lint with breaking change returns exit 1
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
describe('lint (breaking)', () => {
|
|
191
|
-
it('returns exit 1 when breaking changes are detected', () => {
|
|
192
|
-
const { stdout, exitCode } = run(
|
|
193
|
-
`lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
|
|
194
|
-
);
|
|
195
|
-
assert.equal(exitCode, 1);
|
|
196
|
-
const result = JSON.parse(stdout);
|
|
197
|
-
assert.equal(result.decision, 'fail');
|
|
198
|
-
assert.equal(result.exit_code, 1);
|
|
199
|
-
assert.ok(result.summary.breaking_changes > 0, 'should have breaking changes');
|
|
200
|
-
assert.ok(result.violations.length > 0, 'should have violations');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('includes endpoint_removed in changes', () => {
|
|
204
|
-
const { stdout } = run(
|
|
205
|
-
`lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
|
|
206
|
-
);
|
|
207
|
-
const result = JSON.parse(stdout);
|
|
208
|
-
const types = result.all_changes.map(c => c.type);
|
|
209
|
-
assert.ok(types.includes('endpoint_removed'), 'should detect endpoint removal');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('includes semver bump classification', () => {
|
|
213
|
-
const { stdout } = run(
|
|
214
|
-
`lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
|
|
215
|
-
);
|
|
216
|
-
const result = JSON.parse(stdout);
|
|
217
|
-
assert.ok(result.semver, 'should have semver field');
|
|
218
|
-
assert.equal(result.semver.bump, 'major');
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
// 8. diff outputs change types
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
describe('diff command', () => {
|
|
226
|
-
it('outputs changes between two specs', () => {
|
|
227
|
-
const { stdout, exitCode } = run(
|
|
228
|
-
`diff "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
|
|
229
|
-
);
|
|
230
|
-
assert.equal(exitCode, 0);
|
|
231
|
-
const result = JSON.parse(stdout);
|
|
232
|
-
assert.ok(result.total_changes > 0, 'should have changes');
|
|
233
|
-
assert.ok(result.breaking_changes > 0, 'should have breaking changes');
|
|
234
|
-
assert.ok(Array.isArray(result.changes), 'changes should be an array');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('reports change types correctly', () => {
|
|
238
|
-
const { stdout } = run(
|
|
239
|
-
`diff "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
|
|
240
|
-
);
|
|
241
|
-
const result = JSON.parse(stdout);
|
|
242
|
-
const types = result.changes.map(c => c.type);
|
|
243
|
-
assert.ok(types.includes('endpoint_removed'));
|
|
244
|
-
assert.ok(types.includes('type_changed'));
|
|
245
|
-
assert.ok(types.includes('enum_value_removed'));
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('marks breaking changes with is_breaking flag', () => {
|
|
249
|
-
const { stdout } = run(
|
|
250
|
-
`diff "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
|
|
251
|
-
);
|
|
252
|
-
const result = JSON.parse(stdout);
|
|
253
|
-
const breakingChanges = result.changes.filter(c => c.is_breaking);
|
|
254
|
-
assert.equal(breakingChanges.length, result.breaking_changes);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('returns no changes for identical specs', () => {
|
|
258
|
-
const { stdout, exitCode } = run(
|
|
259
|
-
`diff "${SPEC_CLEAN}" "${SPEC_CLEAN}" --json`
|
|
260
|
-
);
|
|
261
|
-
assert.equal(exitCode, 0);
|
|
262
|
-
const result = JSON.parse(stdout);
|
|
263
|
-
assert.equal(result.total_changes, 0);
|
|
264
|
-
assert.equal(result.breaking_changes, 0);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
// 9. explain command
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
describe('explain command', () => {
|
|
272
|
-
it('generates human-readable explanation', () => {
|
|
273
|
-
const { stdout, exitCode } = run(
|
|
274
|
-
`explain "${SPEC_CLEAN}" "${SPEC_BREAKING}" --json`
|
|
275
|
-
);
|
|
276
|
-
assert.equal(exitCode, 0);
|
|
277
|
-
const result = JSON.parse(stdout);
|
|
278
|
-
assert.ok(result.output, 'should have output text');
|
|
279
|
-
assert.ok(result.output.length > 0, 'output should not be empty');
|
|
280
|
-
assert.ok(result.template, 'should report template used');
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('supports --template flag', () => {
|
|
284
|
-
const { stdout, exitCode } = run(
|
|
285
|
-
`explain "${SPEC_CLEAN}" "${SPEC_BREAKING}" --template migration --json`
|
|
286
|
-
);
|
|
287
|
-
assert.equal(exitCode, 0);
|
|
288
|
-
const result = JSON.parse(stdout);
|
|
289
|
-
assert.equal(result.template, 'migration');
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
// 10. lint with --policy preset
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
describe('lint --policy', () => {
|
|
297
|
-
it('accepts relaxed preset and does not fail on breaking changes', () => {
|
|
298
|
-
// relaxed preset uses action:warn, so decision is "warn" not "fail"
|
|
299
|
-
const { stdout, exitCode } = run(
|
|
300
|
-
`lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --policy relaxed --json`
|
|
301
|
-
);
|
|
302
|
-
assert.equal(exitCode, 0, 'relaxed preset should exit 0');
|
|
303
|
-
const result = JSON.parse(stdout);
|
|
304
|
-
assert.notEqual(result.decision, 'fail', 'relaxed should not produce fail decision');
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('accepts strict preset and fails on breaking changes', () => {
|
|
308
|
-
const { stdout, exitCode } = run(
|
|
309
|
-
`lint "${SPEC_CLEAN}" "${SPEC_BREAKING}" --policy strict --json`
|
|
310
|
-
);
|
|
311
|
-
assert.equal(exitCode, 1);
|
|
312
|
-
const result = JSON.parse(stdout);
|
|
313
|
-
assert.equal(result.decision, 'fail');
|
|
314
|
-
assert.ok(result.violations.length > 0);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
// 11. Error handling -- missing files
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
describe('error handling', () => {
|
|
322
|
-
it('lint with nonexistent spec file reports error', () => {
|
|
323
|
-
const { exitCode } = run('lint /nonexistent/old.yaml /nonexistent/new.yaml --json');
|
|
324
|
-
assert.notEqual(exitCode, 0);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('diff with nonexistent spec file reports error', () => {
|
|
328
|
-
const { exitCode } = run('diff /nonexistent/old.yaml /nonexistent/new.yaml --json');
|
|
329
|
-
assert.notEqual(exitCode, 0);
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// 12. api-engine module exports
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
describe('api-engine module', () => {
|
|
337
|
-
it('exports lint, diff, explain, semver, zeroSpec functions', () => {
|
|
338
|
-
const engine = require(path.join(__dirname, '..', 'lib', 'api-engine.js'));
|
|
339
|
-
assert.equal(typeof engine.lint, 'function');
|
|
340
|
-
assert.equal(typeof engine.diff, 'function');
|
|
341
|
-
assert.equal(typeof engine.explain, 'function');
|
|
342
|
-
assert.equal(typeof engine.semver, 'function');
|
|
343
|
-
assert.equal(typeof engine.zeroSpec, 'function');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('lint returns parsed JSON with decision field', () => {
|
|
347
|
-
const engine = require(path.join(__dirname, '..', 'lib', 'api-engine.js'));
|
|
348
|
-
const result = engine.lint(SPEC_CLEAN, SPEC_CLEAN);
|
|
349
|
-
assert.ok(result.decision, 'should have decision field');
|
|
350
|
-
assert.equal(result.decision, 'pass');
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('diff returns parsed JSON with changes array', () => {
|
|
354
|
-
const engine = require(path.join(__dirname, '..', 'lib', 'api-engine.js'));
|
|
355
|
-
const result = engine.diff(SPEC_CLEAN, SPEC_BREAKING);
|
|
356
|
-
assert.ok(Array.isArray(result.changes), 'should have changes array');
|
|
357
|
-
assert.ok(result.total_changes > 0);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
openapi: "3.0.3"
|
|
2
|
-
info:
|
|
3
|
-
title: Pet Store API
|
|
4
|
-
version: "2.0.0"
|
|
5
|
-
description: Sample API for Delimit quickstart
|
|
6
|
-
paths:
|
|
7
|
-
/pets:
|
|
8
|
-
get:
|
|
9
|
-
summary: List all pets
|
|
10
|
-
operationId: listPets
|
|
11
|
-
parameters:
|
|
12
|
-
- name: limit
|
|
13
|
-
in: query
|
|
14
|
-
required: false
|
|
15
|
-
schema:
|
|
16
|
-
type: integer
|
|
17
|
-
format: int32
|
|
18
|
-
responses:
|
|
19
|
-
"200":
|
|
20
|
-
description: A list of pets
|
|
21
|
-
content:
|
|
22
|
-
application/json:
|
|
23
|
-
schema:
|
|
24
|
-
type: array
|
|
25
|
-
items:
|
|
26
|
-
$ref: "#/components/schemas/Pet"
|
|
27
|
-
post:
|
|
28
|
-
summary: Create a pet
|
|
29
|
-
operationId: createPet
|
|
30
|
-
requestBody:
|
|
31
|
-
required: true
|
|
32
|
-
content:
|
|
33
|
-
application/json:
|
|
34
|
-
schema:
|
|
35
|
-
$ref: "#/components/schemas/Pet"
|
|
36
|
-
responses:
|
|
37
|
-
"201":
|
|
38
|
-
description: Pet created
|
|
39
|
-
# /pets/{petId} removed -- this is the breaking change
|
|
40
|
-
components:
|
|
41
|
-
schemas:
|
|
42
|
-
Pet:
|
|
43
|
-
type: object
|
|
44
|
-
required:
|
|
45
|
-
- id
|
|
46
|
-
- name
|
|
47
|
-
properties:
|
|
48
|
-
id:
|
|
49
|
-
type: integer
|
|
50
|
-
name:
|
|
51
|
-
type: string
|
|
52
|
-
status:
|
|
53
|
-
type: string
|
|
54
|
-
enum:
|
|
55
|
-
- available
|
|
56
|
-
- adopted
|