guardrail-core 1.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/dist/__tests__/autopilot.test.d.ts +7 -0
- package/dist/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/__tests__/autopilot.test.js +156 -0
- package/dist/__tests__/tier-config.test.d.ts +9 -0
- package/dist/__tests__/tier-config.test.d.ts.map +1 -0
- package/dist/__tests__/tier-config.test.js +230 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash-inline.test.js +62 -0
- package/dist/__tests__/utils/hash.test.d.ts +3 -0
- package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash.test.js +95 -0
- package/dist/__tests__/utils/simple.test.d.ts +1 -0
- package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/simple.test.js +10 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils-simple.test.js +6 -0
- package/dist/__tests__/utils/utils.test.d.ts +15 -0
- package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils.test.js +172 -0
- package/dist/autopilot/autopilot-runner.d.ts +33 -0
- package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
- package/dist/autopilot/autopilot-runner.js +479 -0
- package/dist/autopilot/index.d.ts +6 -0
- package/dist/autopilot/index.d.ts.map +1 -0
- package/dist/autopilot/index.js +25 -0
- package/dist/autopilot/types.d.ts +102 -0
- package/dist/autopilot/types.d.ts.map +1 -0
- package/dist/autopilot/types.js +18 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +22 -0
- package/dist/cache/redis-cache.d.ts +145 -0
- package/dist/cache/redis-cache.d.ts.map +1 -0
- package/dist/cache/redis-cache.js +459 -0
- package/dist/ci/github-actions.d.ts +77 -0
- package/dist/ci/github-actions.d.ts.map +1 -0
- package/dist/ci/github-actions.js +277 -0
- package/dist/ci/index.d.ts +12 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +27 -0
- package/dist/ci/pre-commit.d.ts +65 -0
- package/dist/ci/pre-commit.d.ts.map +1 -0
- package/dist/ci/pre-commit.js +286 -0
- package/dist/entitlements.d.ts +149 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +464 -0
- package/dist/env.d.ts +113 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +204 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
- package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
- package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
- package/dist/fix-packs/generate-fix-packs.js +505 -0
- package/dist/fix-packs/index.d.ts +8 -0
- package/dist/fix-packs/index.d.ts.map +1 -0
- package/dist/fix-packs/index.js +23 -0
- package/dist/fix-packs/types.d.ts +113 -0
- package/dist/fix-packs/types.d.ts.map +1 -0
- package/dist/fix-packs/types.js +71 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/metrics/prometheus.d.ts +99 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +306 -0
- package/dist/quota-ledger.d.ts +119 -0
- package/dist/quota-ledger.d.ts.map +1 -0
- package/dist/quota-ledger.js +462 -0
- package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
- package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
- package/dist/rbac/__tests__/permissions.test.js +350 -0
- package/dist/rbac/index.d.ts +9 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +32 -0
- package/dist/rbac/permissions.d.ts +71 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +247 -0
- package/dist/rbac/types.d.ts +69 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +213 -0
- package/dist/tier-config.d.ts +203 -0
- package/dist/tier-config.d.ts.map +1 -0
- package/dist/tier-config.js +675 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +127 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
- package/dist/verified-autofix/format-validator.d.ts +101 -0
- package/dist/verified-autofix/format-validator.d.ts.map +1 -0
- package/dist/verified-autofix/format-validator.js +446 -0
- package/dist/verified-autofix/index.d.ts +14 -0
- package/dist/verified-autofix/index.d.ts.map +1 -0
- package/dist/verified-autofix/index.js +39 -0
- package/dist/verified-autofix/pipeline.d.ts +68 -0
- package/dist/verified-autofix/pipeline.d.ts.map +1 -0
- package/dist/verified-autofix/pipeline.js +330 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
- package/dist/verified-autofix/repo-fingerprint.js +396 -0
- package/dist/verified-autofix/workspace.d.ts +83 -0
- package/dist/verified-autofix/workspace.d.ts.map +1 -0
- package/dist/verified-autofix/workspace.js +454 -0
- package/dist/verified-autofix.d.ts +182 -0
- package/dist/verified-autofix.d.ts.map +1 -0
- package/dist/verified-autofix.js +1021 -0
- package/dist/visualization/dependency-graph.d.ts +79 -0
- package/dist/visualization/dependency-graph.d.ts.map +1 -0
- package/dist/visualization/dependency-graph.js +399 -0
- package/dist/visualization/index.d.ts +5 -0
- package/dist/visualization/index.d.ts.map +1 -0
- package/dist/visualization/index.js +20 -0
- package/package.json +29 -0
- package/src/__tests__/autopilot.test.ts +196 -0
- package/src/__tests__/tier-config.test.ts +289 -0
- package/src/__tests__/utils/hash-inline.test.ts +76 -0
- package/src/__tests__/utils/hash.test.ts +119 -0
- package/src/__tests__/utils/simple.test.ts +10 -0
- package/src/__tests__/utils/utils-simple.test.ts +5 -0
- package/src/__tests__/utils/utils.test.ts +203 -0
- package/src/autopilot/autopilot-runner.ts +503 -0
- package/src/autopilot/index.ts +6 -0
- package/src/autopilot/types.ts +119 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/redis-cache.d.ts +155 -0
- package/src/cache/redis-cache.d.ts.map +1 -0
- package/src/cache/redis-cache.ts +517 -0
- package/src/ci/github-actions.ts +335 -0
- package/src/ci/index.ts +12 -0
- package/src/ci/pre-commit.ts +338 -0
- package/src/db/usage-schema.prisma +114 -0
- package/src/entitlements.ts +570 -0
- package/src/env.d.ts +68 -0
- package/src/env.d.ts.map +1 -0
- package/src/env.ts +247 -0
- package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
- package/src/fix-packs/generate-fix-packs.ts +577 -0
- package/src/fix-packs/index.ts +8 -0
- package/src/fix-packs/types.ts +206 -0
- package/src/index.d.ts +7 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +12 -0
- package/src/metrics/prometheus.d.ts +104 -0
- package/src/metrics/prometheus.d.ts.map +1 -0
- package/src/metrics/prometheus.ts +446 -0
- package/src/quota-ledger.ts +548 -0
- package/src/rbac/__tests__/permissions.test.ts +446 -0
- package/src/rbac/index.ts +46 -0
- package/src/rbac/permissions.ts +301 -0
- package/src/rbac/types.ts +298 -0
- package/src/tier-config.json +157 -0
- package/src/tier-config.ts +815 -0
- package/src/types.d.ts +365 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +441 -0
- package/src/utils.d.ts +36 -0
- package/src/utils.d.ts.map +1 -0
- package/src/utils.ts +140 -0
- package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
- package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
- package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
- package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
- package/src/verified-autofix/format-validator.ts +517 -0
- package/src/verified-autofix/index.ts +63 -0
- package/src/verified-autofix/pipeline.ts +403 -0
- package/src/verified-autofix/repo-fingerprint.ts +459 -0
- package/src/verified-autofix/workspace.ts +531 -0
- package/src/verified-autofix.ts +1187 -0
- package/src/visualization/dependency-graph.d.ts +85 -0
- package/src/visualization/dependency-graph.d.ts.map +1 -0
- package/src/visualization/dependency-graph.ts +495 -0
- package/src/visualization/index.ts +5 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Tests - Verified AutoFix Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Tests for the full verification pipeline:
|
|
5
|
+
* - Format validation stage
|
|
6
|
+
* - Diff application stage
|
|
7
|
+
* - Verification stage (typecheck, build, tests)
|
|
8
|
+
* - Top 3 failure context extraction
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import {
|
|
15
|
+
VerifiedAutofixPipeline,
|
|
16
|
+
formatPipelineResult,
|
|
17
|
+
formatPipelineResultJson,
|
|
18
|
+
type PipelineResult,
|
|
19
|
+
} from '../pipeline';
|
|
20
|
+
import { validateAgentOutput } from '../format-validator';
|
|
21
|
+
|
|
22
|
+
describe('VerifiedAutofixPipeline', () => {
|
|
23
|
+
let tempDir: string;
|
|
24
|
+
let pipeline: VerifiedAutofixPipeline;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pipeline-test-'));
|
|
28
|
+
pipeline = new VerifiedAutofixPipeline();
|
|
29
|
+
|
|
30
|
+
// Create minimal project structure
|
|
31
|
+
await fs.promises.writeFile(
|
|
32
|
+
path.join(tempDir, 'package.json'),
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
name: 'test-project',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
scripts: {
|
|
37
|
+
build: 'echo "build"',
|
|
38
|
+
test: 'echo "test"',
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('validation stage', () => {
|
|
49
|
+
it('rejects missing agent output file', async () => {
|
|
50
|
+
const result = await pipeline.run({
|
|
51
|
+
projectPath: tempDir,
|
|
52
|
+
agentOutputFile: '/nonexistent/file.json',
|
|
53
|
+
skipEntitlements: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.success).toBe(false);
|
|
57
|
+
expect(result.stage).toBe('validate');
|
|
58
|
+
expect(result.errors.some(e => e.includes('not found'))).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects invalid JSON in agent output', async () => {
|
|
62
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
63
|
+
await fs.promises.writeFile(outputFile, 'not valid json');
|
|
64
|
+
|
|
65
|
+
const result = await pipeline.run({
|
|
66
|
+
projectPath: tempDir,
|
|
67
|
+
agentOutputFile: outputFile,
|
|
68
|
+
skipEntitlements: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result.success).toBe(false);
|
|
72
|
+
expect(result.errors.some(e => e.includes('Invalid JSON'))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects wrong format field', async () => {
|
|
76
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
77
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
78
|
+
format: 'wrong-format',
|
|
79
|
+
diff: '',
|
|
80
|
+
commands: [],
|
|
81
|
+
tests: [],
|
|
82
|
+
notes: '',
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
const result = await pipeline.run({
|
|
86
|
+
projectPath: tempDir,
|
|
87
|
+
agentOutputFile: outputFile,
|
|
88
|
+
skipEntitlements: true,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.success).toBe(false);
|
|
92
|
+
expect(result.errors.some(e => e.includes('guardrail-v1'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('accepts valid guardrail-v1 output with empty diff', async () => {
|
|
96
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
97
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
98
|
+
format: 'guardrail-v1',
|
|
99
|
+
diff: '',
|
|
100
|
+
commands: [],
|
|
101
|
+
tests: [],
|
|
102
|
+
notes: 'No changes needed',
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
const result = await pipeline.run({
|
|
106
|
+
projectPath: tempDir,
|
|
107
|
+
agentOutputFile: outputFile,
|
|
108
|
+
skipEntitlements: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
expect(result.stage).toBe('done');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('strips markdown fences and warns', async () => {
|
|
116
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
117
|
+
await fs.promises.writeFile(outputFile, '```json\n' + JSON.stringify({
|
|
118
|
+
format: 'guardrail-v1',
|
|
119
|
+
diff: '',
|
|
120
|
+
commands: [],
|
|
121
|
+
tests: [],
|
|
122
|
+
notes: '',
|
|
123
|
+
}) + '\n```');
|
|
124
|
+
|
|
125
|
+
const result = await pipeline.run({
|
|
126
|
+
projectPath: tempDir,
|
|
127
|
+
agentOutputFile: outputFile,
|
|
128
|
+
skipEntitlements: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
expect(result.warnings.some(w => w.includes('markdown'))).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('stub detection', () => {
|
|
137
|
+
it('blocks TODO comments in diff', async () => {
|
|
138
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
139
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
140
|
+
format: 'guardrail-v1',
|
|
141
|
+
diff: `--- a/src/file.ts
|
|
142
|
+
+++ b/src/file.ts
|
|
143
|
+
@@ -1,1 +1,2 @@
|
|
144
|
+
const x = 1;
|
|
145
|
+
+// TODO: implement this`,
|
|
146
|
+
commands: [],
|
|
147
|
+
tests: [],
|
|
148
|
+
notes: '',
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const result = await pipeline.run({
|
|
152
|
+
projectPath: tempDir,
|
|
153
|
+
agentOutputFile: outputFile,
|
|
154
|
+
skipEntitlements: true,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(result.success).toBe(false);
|
|
158
|
+
expect(result.errors.some(e => e.includes('Stub'))).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('blocks placeholder text in diff', async () => {
|
|
162
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
163
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
164
|
+
format: 'guardrail-v1',
|
|
165
|
+
diff: `--- a/src/file.ts
|
|
166
|
+
+++ b/src/file.ts
|
|
167
|
+
@@ -1,1 +1,2 @@
|
|
168
|
+
const x = 1;
|
|
169
|
+
+const message = "placeholder content here";`,
|
|
170
|
+
commands: [],
|
|
171
|
+
tests: [],
|
|
172
|
+
notes: '',
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
const result = await pipeline.run({
|
|
176
|
+
projectPath: tempDir,
|
|
177
|
+
agentOutputFile: outputFile,
|
|
178
|
+
skipEntitlements: true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result.success).toBe(false);
|
|
182
|
+
expect(result.errors.some(e => e.includes('Stub') || e.includes('placeholder'))).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('allows clean code without stubs', async () => {
|
|
186
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
187
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
188
|
+
format: 'guardrail-v1',
|
|
189
|
+
diff: `--- a/src/file.ts
|
|
190
|
+
+++ b/src/file.ts
|
|
191
|
+
@@ -1,1 +1,2 @@
|
|
192
|
+
const x = 1;
|
|
193
|
+
+const y = x + 1;`,
|
|
194
|
+
commands: [],
|
|
195
|
+
tests: [],
|
|
196
|
+
notes: '',
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
// Create the source file
|
|
200
|
+
await fs.promises.mkdir(path.join(tempDir, 'src'), { recursive: true });
|
|
201
|
+
await fs.promises.writeFile(path.join(tempDir, 'src', 'file.ts'), 'const x = 1;\n');
|
|
202
|
+
|
|
203
|
+
const result = await pipeline.run({
|
|
204
|
+
projectPath: tempDir,
|
|
205
|
+
agentOutputFile: outputFile,
|
|
206
|
+
dryRun: true,
|
|
207
|
+
skipEntitlements: true,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Should pass validation (stub detection at least)
|
|
211
|
+
expect(result.validation?.stubDetection?.hasStubs).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('path safety', () => {
|
|
216
|
+
it('blocks parent directory traversal', async () => {
|
|
217
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
218
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
219
|
+
format: 'guardrail-v1',
|
|
220
|
+
diff: `--- a/../../../etc/passwd
|
|
221
|
+
+++ b/../../../etc/passwd
|
|
222
|
+
@@ -1,1 +1,1 @@
|
|
223
|
+
-root
|
|
224
|
+
+hacked`,
|
|
225
|
+
commands: [],
|
|
226
|
+
tests: [],
|
|
227
|
+
notes: '',
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
const result = await pipeline.run({
|
|
231
|
+
projectPath: tempDir,
|
|
232
|
+
agentOutputFile: outputFile,
|
|
233
|
+
skipEntitlements: true,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result.success).toBe(false);
|
|
237
|
+
expect(result.errors.some(e => e.includes('Unsafe') || e.includes('path'))).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('blocks node_modules modifications', async () => {
|
|
241
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
242
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
243
|
+
format: 'guardrail-v1',
|
|
244
|
+
diff: `--- a/node_modules/lodash/index.js
|
|
245
|
+
+++ b/node_modules/lodash/index.js
|
|
246
|
+
@@ -1,1 +1,1 @@
|
|
247
|
+
-module.exports = {};
|
|
248
|
+
+module.exports = { hacked: true };`,
|
|
249
|
+
commands: [],
|
|
250
|
+
tests: [],
|
|
251
|
+
notes: '',
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
const result = await pipeline.run({
|
|
255
|
+
projectPath: tempDir,
|
|
256
|
+
agentOutputFile: outputFile,
|
|
257
|
+
skipEntitlements: true,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(result.success).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('dry run mode', () => {
|
|
265
|
+
it('validates without applying changes', async () => {
|
|
266
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
267
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
268
|
+
format: 'guardrail-v1',
|
|
269
|
+
diff: `--- a/src/file.ts
|
|
270
|
+
+++ b/src/file.ts
|
|
271
|
+
@@ -1,1 +1,2 @@
|
|
272
|
+
const x = 1;
|
|
273
|
+
+const y = 2;`,
|
|
274
|
+
commands: ['npm test'],
|
|
275
|
+
tests: ['test/file.test.ts'],
|
|
276
|
+
notes: 'Added y constant',
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
// Create source file
|
|
280
|
+
await fs.promises.mkdir(path.join(tempDir, 'src'), { recursive: true });
|
|
281
|
+
await fs.promises.writeFile(path.join(tempDir, 'src', 'file.ts'), 'const x = 1;\n');
|
|
282
|
+
|
|
283
|
+
const result = await pipeline.run({
|
|
284
|
+
projectPath: tempDir,
|
|
285
|
+
agentOutputFile: outputFile,
|
|
286
|
+
dryRun: true,
|
|
287
|
+
skipEntitlements: true,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(result.success).toBe(true);
|
|
291
|
+
expect(result.stage).toBe('done');
|
|
292
|
+
|
|
293
|
+
// File should NOT be modified in dry run
|
|
294
|
+
const content = await fs.promises.readFile(path.join(tempDir, 'src', 'file.ts'), 'utf8');
|
|
295
|
+
expect(content).toBe('const x = 1;\n');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('progress callbacks', () => {
|
|
300
|
+
it('calls onProgress for each stage', async () => {
|
|
301
|
+
const outputFile = path.join(tempDir, 'output.json');
|
|
302
|
+
await fs.promises.writeFile(outputFile, JSON.stringify({
|
|
303
|
+
format: 'guardrail-v1',
|
|
304
|
+
diff: '',
|
|
305
|
+
commands: [],
|
|
306
|
+
tests: [],
|
|
307
|
+
notes: '',
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
const stages: string[] = [];
|
|
311
|
+
await pipeline.run({
|
|
312
|
+
projectPath: tempDir,
|
|
313
|
+
agentOutputFile: outputFile,
|
|
314
|
+
skipEntitlements: true,
|
|
315
|
+
onProgress: (stage) => {
|
|
316
|
+
stages.push(stage);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(stages).toContain('init');
|
|
321
|
+
expect(stages).toContain('validate');
|
|
322
|
+
expect(stages).toContain('done');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('validateAgentOutput strictMarkdown option', () => {
|
|
328
|
+
const projectRoot = '/tmp/project';
|
|
329
|
+
|
|
330
|
+
it('rejects markdown in strict mode', () => {
|
|
331
|
+
const raw = '```json\n{"format":"guardrail-v1","diff":"","commands":[],"tests":[],"notes":""}\n```';
|
|
332
|
+
const result = validateAgentOutput(raw, projectRoot, { strictMarkdown: true });
|
|
333
|
+
|
|
334
|
+
expect(result.valid).toBe(false);
|
|
335
|
+
expect(result.wasMarkdownWrapped).toBe(true);
|
|
336
|
+
expect(result.errors[0]).toContain('markdown');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('accepts and strips markdown in forgiving mode', () => {
|
|
340
|
+
const raw = '```json\n{"format":"guardrail-v1","diff":"","commands":[],"tests":[],"notes":""}\n```';
|
|
341
|
+
const result = validateAgentOutput(raw, projectRoot);
|
|
342
|
+
|
|
343
|
+
expect(result.valid).toBe(true);
|
|
344
|
+
expect(result.wasMarkdownWrapped).toBe(true);
|
|
345
|
+
expect(result.warnings.some(w => w.includes('markdown'))).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('accepts raw JSON without warnings', () => {
|
|
349
|
+
const raw = '{"format":"guardrail-v1","diff":"","commands":[],"tests":[],"notes":""}';
|
|
350
|
+
const result = validateAgentOutput(raw, projectRoot);
|
|
351
|
+
|
|
352
|
+
expect(result.valid).toBe(true);
|
|
353
|
+
expect(result.wasMarkdownWrapped).toBe(false);
|
|
354
|
+
expect(result.warnings.filter(w => w.includes('markdown'))).toHaveLength(0);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('formatPipelineResult', () => {
|
|
359
|
+
it('formats successful result', () => {
|
|
360
|
+
const result: PipelineResult = {
|
|
361
|
+
success: true,
|
|
362
|
+
stage: 'done',
|
|
363
|
+
duration: 1234,
|
|
364
|
+
filesModified: ['src/file.ts'],
|
|
365
|
+
errors: [],
|
|
366
|
+
warnings: [],
|
|
367
|
+
failureContext: [],
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const output = formatPipelineResult(result);
|
|
371
|
+
expect(output).toContain('SUCCESSFUL');
|
|
372
|
+
expect(output).toContain('src/file.ts');
|
|
373
|
+
expect(output).toContain('1234');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('formats failed result with top 3 failures', () => {
|
|
377
|
+
const result: PipelineResult = {
|
|
378
|
+
success: false,
|
|
379
|
+
stage: 'typecheck',
|
|
380
|
+
duration: 500,
|
|
381
|
+
filesModified: [],
|
|
382
|
+
errors: ['TypeScript error'],
|
|
383
|
+
warnings: [],
|
|
384
|
+
failureContext: [
|
|
385
|
+
'error TS2322: Type mismatch',
|
|
386
|
+
'error TS2345: Argument type',
|
|
387
|
+
'error TS2339: Property does not exist',
|
|
388
|
+
'error TS2304: Cannot find name', // This 4th one should be cut off
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const output = formatPipelineResult(result);
|
|
393
|
+
expect(output).toContain('FAILED');
|
|
394
|
+
expect(output).toContain('TS2322');
|
|
395
|
+
expect(output).toContain('TS2345');
|
|
396
|
+
expect(output).toContain('TS2339');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('formatPipelineResultJson', () => {
|
|
401
|
+
it('outputs valid JSON', () => {
|
|
402
|
+
const result: PipelineResult = {
|
|
403
|
+
success: true,
|
|
404
|
+
stage: 'done',
|
|
405
|
+
duration: 100,
|
|
406
|
+
filesModified: ['a.ts'],
|
|
407
|
+
errors: [],
|
|
408
|
+
warnings: ['warn1'],
|
|
409
|
+
failureContext: [],
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const json = formatPipelineResultJson(result);
|
|
413
|
+
const parsed = JSON.parse(json);
|
|
414
|
+
|
|
415
|
+
expect(parsed.success).toBe(true);
|
|
416
|
+
expect(parsed.filesModified).toEqual(['a.ts']);
|
|
417
|
+
expect(parsed.warnings).toEqual(['warn1']);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo Fingerprint Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for project detection and configuration:
|
|
5
|
+
* - Package manager detection
|
|
6
|
+
* - Build tool detection
|
|
7
|
+
* - Framework detection
|
|
8
|
+
* - Test runner detection
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import {
|
|
15
|
+
fingerprintRepo,
|
|
16
|
+
getInstallCommand,
|
|
17
|
+
getBuildCommand,
|
|
18
|
+
getTestCommand,
|
|
19
|
+
getTypecheckCommand,
|
|
20
|
+
} from '../repo-fingerprint';
|
|
21
|
+
|
|
22
|
+
describe('fingerprintRepo', () => {
|
|
23
|
+
let tempDir: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'fingerprint-test-'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('detects pnpm from lock file', async () => {
|
|
34
|
+
await fs.promises.writeFile(path.join(tempDir, 'pnpm-lock.yaml'), '');
|
|
35
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
36
|
+
name: 'test',
|
|
37
|
+
dependencies: {},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
41
|
+
expect(fingerprint.packageManager).toBe('pnpm');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('detects yarn from lock file', async () => {
|
|
45
|
+
await fs.promises.writeFile(path.join(tempDir, 'yarn.lock'), '');
|
|
46
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
47
|
+
name: 'test',
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
51
|
+
expect(fingerprint.packageManager).toBe('yarn');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('detects npm from lock file', async () => {
|
|
55
|
+
await fs.promises.writeFile(path.join(tempDir, 'package-lock.json'), '{}');
|
|
56
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
57
|
+
name: 'test',
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
61
|
+
expect(fingerprint.packageManager).toBe('npm');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('detects Next.js framework', async () => {
|
|
65
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
66
|
+
name: 'test',
|
|
67
|
+
dependencies: { next: '^14.0.0', react: '^18.0.0' },
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
71
|
+
expect(fingerprint.framework).toBe('next');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('detects Vite framework', async () => {
|
|
75
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
76
|
+
name: 'test',
|
|
77
|
+
devDependencies: { vite: '^5.0.0' },
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
81
|
+
expect(fingerprint.framework).toBe('vite');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('detects Jest test runner', async () => {
|
|
85
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
86
|
+
name: 'test',
|
|
87
|
+
devDependencies: { jest: '^29.0.0' },
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
91
|
+
expect(fingerprint.testRunner).toBe('jest');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('detects Vitest test runner', async () => {
|
|
95
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
96
|
+
name: 'test',
|
|
97
|
+
devDependencies: { vitest: '^1.0.0' },
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
101
|
+
expect(fingerprint.testRunner).toBe('vitest');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('detects TypeScript', async () => {
|
|
105
|
+
await fs.promises.writeFile(path.join(tempDir, 'tsconfig.json'), '{}');
|
|
106
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
107
|
+
name: 'test',
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
111
|
+
expect(fingerprint.hasTypeScript).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('detects Turbo build tool', async () => {
|
|
115
|
+
await fs.promises.writeFile(path.join(tempDir, 'turbo.json'), '{}');
|
|
116
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
117
|
+
name: 'test',
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
121
|
+
expect(fingerprint.buildTool).toBe('turbo');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('detects monorepo from workspaces', async () => {
|
|
125
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
126
|
+
name: 'monorepo',
|
|
127
|
+
workspaces: ['packages/*'],
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
131
|
+
expect(fingerprint.isMonorepo).toBe(true);
|
|
132
|
+
expect(fingerprint.workspaces).toContain('packages/*');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('detects build script', async () => {
|
|
136
|
+
await fs.promises.writeFile(path.join(tempDir, 'package.json'), JSON.stringify({
|
|
137
|
+
name: 'test',
|
|
138
|
+
scripts: { build: 'tsc' },
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
const { fingerprint } = fingerprintRepo(tempDir);
|
|
142
|
+
expect(fingerprint.hasBuildScript).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('handles missing package.json gracefully', () => {
|
|
146
|
+
const { fingerprint, confidence, detectionNotes } = fingerprintRepo(tempDir);
|
|
147
|
+
expect(fingerprint.packageManager).toBe('unknown');
|
|
148
|
+
expect(confidence).toBeLessThan(100);
|
|
149
|
+
expect(detectionNotes).toContain('No package.json found');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('command helpers', () => {
|
|
154
|
+
it('returns correct install command for pnpm', () => {
|
|
155
|
+
expect(getInstallCommand('pnpm')).toBe('pnpm install --frozen-lockfile');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns correct install command for yarn', () => {
|
|
159
|
+
expect(getInstallCommand('yarn')).toBe('yarn install --frozen-lockfile');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns correct install command for npm', () => {
|
|
163
|
+
expect(getInstallCommand('npm')).toBe('npm ci');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns turbo build command when turbo detected', () => {
|
|
167
|
+
const fingerprint = {
|
|
168
|
+
buildTool: 'turbo' as const,
|
|
169
|
+
hasBuildScript: true,
|
|
170
|
+
hasTypeScript: true,
|
|
171
|
+
packageManager: 'pnpm' as const,
|
|
172
|
+
framework: 'next' as const,
|
|
173
|
+
testRunner: 'jest' as const,
|
|
174
|
+
hasESLint: true,
|
|
175
|
+
hasPrettier: true,
|
|
176
|
+
hasTestScript: true,
|
|
177
|
+
isMonorepo: true,
|
|
178
|
+
workspaces: ['packages/*'],
|
|
179
|
+
dependencies: {},
|
|
180
|
+
devDependencies: {},
|
|
181
|
+
};
|
|
182
|
+
expect(getBuildCommand(fingerprint)).toBe('npx turbo run build');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('returns vitest command for vitest runner', () => {
|
|
186
|
+
const fingerprint = {
|
|
187
|
+
testRunner: 'vitest' as const,
|
|
188
|
+
hasTestScript: true,
|
|
189
|
+
buildTool: 'none' as const,
|
|
190
|
+
hasBuildScript: false,
|
|
191
|
+
hasTypeScript: false,
|
|
192
|
+
packageManager: 'npm' as const,
|
|
193
|
+
framework: 'none' as const,
|
|
194
|
+
hasESLint: false,
|
|
195
|
+
hasPrettier: false,
|
|
196
|
+
isMonorepo: false,
|
|
197
|
+
workspaces: [],
|
|
198
|
+
dependencies: {},
|
|
199
|
+
devDependencies: {},
|
|
200
|
+
};
|
|
201
|
+
expect(getTestCommand(fingerprint)).toBe('npx vitest run');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('returns typecheck command when TypeScript present', () => {
|
|
205
|
+
const fingerprint = {
|
|
206
|
+
hasTypeScript: true,
|
|
207
|
+
testRunner: 'none' as const,
|
|
208
|
+
hasTestScript: false,
|
|
209
|
+
buildTool: 'none' as const,
|
|
210
|
+
hasBuildScript: false,
|
|
211
|
+
packageManager: 'npm' as const,
|
|
212
|
+
framework: 'none' as const,
|
|
213
|
+
hasESLint: false,
|
|
214
|
+
hasPrettier: false,
|
|
215
|
+
isMonorepo: false,
|
|
216
|
+
workspaces: [],
|
|
217
|
+
dependencies: {},
|
|
218
|
+
devDependencies: {},
|
|
219
|
+
};
|
|
220
|
+
expect(getTypecheckCommand(fingerprint)).toBe('npx tsc --noEmit');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns null typecheck command when no TypeScript', () => {
|
|
224
|
+
const fingerprint = {
|
|
225
|
+
hasTypeScript: false,
|
|
226
|
+
testRunner: 'none' as const,
|
|
227
|
+
hasTestScript: false,
|
|
228
|
+
buildTool: 'none' as const,
|
|
229
|
+
hasBuildScript: false,
|
|
230
|
+
packageManager: 'npm' as const,
|
|
231
|
+
framework: 'none' as const,
|
|
232
|
+
hasESLint: false,
|
|
233
|
+
hasPrettier: false,
|
|
234
|
+
isMonorepo: false,
|
|
235
|
+
workspaces: [],
|
|
236
|
+
dependencies: {},
|
|
237
|
+
devDependencies: {},
|
|
238
|
+
};
|
|
239
|
+
expect(getTypecheckCommand(fingerprint)).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
});
|