@saiteja1123/mcp-server 1.1.4 → 1.1.6
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/package.json +59 -55
- package/src/api-scan.mjs +362 -93
- package/src/cli.js +771 -322
- package/src/deep-scan/contracts.js +201 -0
- package/src/deep-scan/deterministic-scan.js +337 -0
- package/src/deep-scan/index.js +109 -0
- package/src/deep-scan/project-map.js +507 -0
- package/src/deep-scan/ralph-accept.js +510 -0
- package/src/deep-scan/ralph-compare.js +498 -0
- package/src/deep-scan/ralph-tasks.js +598 -0
- package/src/deep-scan/ralph-track.js +548 -0
- package/src/deep-scan/registry.js +159 -0
- package/src/deep-scan/runtime.js +275 -0
- package/src/deep-scan/sample-steppers.js +128 -0
- package/src/deep-scan/sourceSafe.js +73 -0
- package/src/deep-scan/status.js +70 -0
- package/src/deep-scan/store.js +57 -0
- package/src/deep-scan/test-plan.js +760 -0
- package/src/index.js +6 -5
- package/src/lock.mjs +55 -14
- package/src/mcp-config.mjs +161 -0
- package/src/middleware/governance.js +135 -0
- package/src/orchestrator/runScan.js +211 -0
- package/src/project-bindings.mjs +215 -0
- package/src/rule-engine/index.js +2 -1
- package/src/rule-engine/localScan.js +39 -12
- package/src/rule-engine/metadata.js +20 -0
- package/src/rule-engine/prompt.js +6 -5
- package/src/rule-engine/rules.js +71 -43
- package/src/rule-engine/score.js +5 -4
- package/src/security/pathGuard.js +170 -0
- package/src/selftest.js +2473 -0
- package/src/server.js +109 -150
- package/src/tools/deepScan.js +286 -0
- package/src/tools/localScan.js +85 -0
- package/src/tools/projects.js +124 -0
- package/src/tools/scanFile.js +131 -0
package/src/selftest.js
ADDED
|
@@ -0,0 +1,2473 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { pathToFileURL } from 'url';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createLock,
|
|
12
|
+
diagnosticLock,
|
|
13
|
+
validateScanPath,
|
|
14
|
+
isRuntimeCompatibleLock,
|
|
15
|
+
buildLockedRootHash as buildLockHashFromLockModule,
|
|
16
|
+
} from './lock.mjs';
|
|
17
|
+
import {
|
|
18
|
+
buildLockedRootHash as buildLockHashFromApiModule,
|
|
19
|
+
normalizeApiBase,
|
|
20
|
+
requestServerBind,
|
|
21
|
+
verifyInstallBinding,
|
|
22
|
+
} from './api-scan.mjs';
|
|
23
|
+
import { createBindingGuard } from './security/pathGuard.js';
|
|
24
|
+
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
|
|
27
|
+
async function withMockMcpBackend(fn) {
|
|
28
|
+
const installs = new Map();
|
|
29
|
+
const activeTokenByAuth = new Map();
|
|
30
|
+
const scanLogCalls = [];
|
|
31
|
+
let tokenCounter = 0;
|
|
32
|
+
const nextInstallToken = () => {
|
|
33
|
+
tokenCounter += 1;
|
|
34
|
+
return tokenCounter.toString(16).padStart(64, 'c');
|
|
35
|
+
};
|
|
36
|
+
const server = http.createServer(async (req, res) => {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
39
|
+
req.on('end', () => {
|
|
40
|
+
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
|
|
41
|
+
res.setHeader('Content-Type', 'application/json');
|
|
42
|
+
|
|
43
|
+
if (req.method === 'POST' && req.url === '/api/v1/mcp/bind') {
|
|
44
|
+
const auth = req.headers.authorization || '';
|
|
45
|
+
if (!auth.startsWith('Bearer ')) {
|
|
46
|
+
res.writeHead(401);
|
|
47
|
+
res.end(JSON.stringify({ success: false, error: 'auth required' }));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const lockedRootPath = body.lockedRootPath;
|
|
51
|
+
const lockedRootHash = buildLockHashFromApiModule(lockedRootPath);
|
|
52
|
+
const previousToken = activeTokenByAuth.get(auth);
|
|
53
|
+
if (previousToken && installs.has(previousToken)) {
|
|
54
|
+
installs.get(previousToken).revoked = true;
|
|
55
|
+
}
|
|
56
|
+
const installToken = nextInstallToken();
|
|
57
|
+
installs.set(installToken, { lockedRootHash, revoked: false });
|
|
58
|
+
activeTokenByAuth.set(auth, installToken);
|
|
59
|
+
res.writeHead(201);
|
|
60
|
+
res.end(JSON.stringify({
|
|
61
|
+
success: true,
|
|
62
|
+
data: { installToken, lockedRootHash, lockedRootPath },
|
|
63
|
+
}));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (req.method === 'POST' && req.url === '/api/v1/mcp/verify-install') {
|
|
68
|
+
const install = installs.get(body.installToken);
|
|
69
|
+
if (
|
|
70
|
+
!install
|
|
71
|
+
|| install.revoked
|
|
72
|
+
|| install.lockedRootHash !== String(body.lockedRootHash || '').toLowerCase()
|
|
73
|
+
) {
|
|
74
|
+
res.writeHead(403);
|
|
75
|
+
res.end(JSON.stringify({
|
|
76
|
+
success: false,
|
|
77
|
+
error: 'MCP install verification failed',
|
|
78
|
+
code: 'MCP_INSTALL_INVALID',
|
|
79
|
+
}));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
res.writeHead(200);
|
|
83
|
+
res.end(JSON.stringify({ success: true, data: { verifiedAt: new Date().toISOString() } }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (req.method === 'POST' && req.url === '/api/v1/scan/local') {
|
|
88
|
+
const install = installs.get(body.installToken);
|
|
89
|
+
if (
|
|
90
|
+
!install
|
|
91
|
+
|| install.revoked
|
|
92
|
+
|| install.lockedRootHash !== String(body.lockedRootHash || '').toLowerCase()
|
|
93
|
+
) {
|
|
94
|
+
res.writeHead(403);
|
|
95
|
+
res.end(JSON.stringify({
|
|
96
|
+
success: false,
|
|
97
|
+
error: 'MCP install verification failed',
|
|
98
|
+
code: 'MCP_INSTALL_INVALID',
|
|
99
|
+
}));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (body.code === 'quota-exhausted') {
|
|
103
|
+
res.writeHead(402);
|
|
104
|
+
res.end(JSON.stringify({
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'Project quota exhausted',
|
|
107
|
+
code: 'QUOTA_EXCEEDED',
|
|
108
|
+
}));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (String(body.code || '').includes('remote-ok-without-data')) {
|
|
112
|
+
res.writeHead(200);
|
|
113
|
+
res.end(JSON.stringify({ success: true }));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.writeHead(200);
|
|
117
|
+
res.end(JSON.stringify({
|
|
118
|
+
success: true,
|
|
119
|
+
data: {
|
|
120
|
+
findings: [],
|
|
121
|
+
checklist: [],
|
|
122
|
+
score: 100,
|
|
123
|
+
grade: 'A',
|
|
124
|
+
verdict: 'No benchmark issues found',
|
|
125
|
+
codeHash: 'a'.repeat(64),
|
|
126
|
+
receiptId: 'scan-receipt-1',
|
|
127
|
+
},
|
|
128
|
+
quota: { limit: 10, remaining: 9 },
|
|
129
|
+
}));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (req.method === 'POST' && req.url === '/api/v1/scan/log') {
|
|
134
|
+
const install = installs.get(body.installToken);
|
|
135
|
+
if (
|
|
136
|
+
!install
|
|
137
|
+
|| install.revoked
|
|
138
|
+
|| install.lockedRootHash !== String(body.lockedRootHash || '').toLowerCase()
|
|
139
|
+
) {
|
|
140
|
+
res.writeHead(403);
|
|
141
|
+
res.end(JSON.stringify({
|
|
142
|
+
success: false,
|
|
143
|
+
error: 'MCP install verification failed',
|
|
144
|
+
code: 'MCP_INSTALL_INVALID',
|
|
145
|
+
}));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
scanLogCalls.push(body);
|
|
149
|
+
res.writeHead(201);
|
|
150
|
+
res.end(JSON.stringify({ success: true, data: { scanId: 'scan-log-1' } }));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
res.writeHead(404);
|
|
155
|
+
res.end(JSON.stringify({ success: false, error: 'not found' }));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
160
|
+
const { port } = server.address();
|
|
161
|
+
try {
|
|
162
|
+
return await fn(`http://127.0.0.1:${port}`, { scanLogCalls });
|
|
163
|
+
} finally {
|
|
164
|
+
await new Promise((resolve) => server.close(resolve));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function run() {
|
|
169
|
+
const tmpBase = await fs.mkdtemp(path.join(os.tmpdir(), 'vibesecur-mcp-selftest-'));
|
|
170
|
+
try {
|
|
171
|
+
const projectRoot = path.join(tmpBase, 'project');
|
|
172
|
+
await fs.mkdir(projectRoot, { recursive: true });
|
|
173
|
+
|
|
174
|
+
const lock = await createLock({
|
|
175
|
+
rootPath: projectRoot,
|
|
176
|
+
account: 'selftest',
|
|
177
|
+
installToken: 'a'.repeat(64),
|
|
178
|
+
source: 'selftest',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const valid = await validateScanPath(projectRoot, lock.installToken);
|
|
182
|
+
assert.equal(valid.ok, true, 'validateScanPath should accept matching token in bound root');
|
|
183
|
+
|
|
184
|
+
const invalid = await validateScanPath(projectRoot, 'b'.repeat(64));
|
|
185
|
+
assert.equal(invalid.ok, false, 'validateScanPath should reject mismatched token');
|
|
186
|
+
assert.equal(invalid.code, 'TOKEN_MISMATCH', 'expected TOKEN_MISMATCH for wrong install token');
|
|
187
|
+
|
|
188
|
+
const hashFromLock = buildLockHashFromLockModule(projectRoot);
|
|
189
|
+
const hashFromApi = buildLockHashFromApiModule(projectRoot);
|
|
190
|
+
assert.equal(hashFromLock, hashFromApi, 'locked root hash must match across modules');
|
|
191
|
+
|
|
192
|
+
assert.equal(
|
|
193
|
+
normalizeApiBase('https://example.com'),
|
|
194
|
+
'https://example.com/api/v1',
|
|
195
|
+
'normalizeApiBase should append /api/v1',
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
assert.equal(
|
|
199
|
+
isRuntimeCompatibleLock(lock),
|
|
200
|
+
false,
|
|
201
|
+
'local/selftest locks must not be runtime-compatible server bindings',
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const serverLock = await createLock({
|
|
205
|
+
rootPath: projectRoot,
|
|
206
|
+
account: 'selftest',
|
|
207
|
+
installToken: 'd'.repeat(64),
|
|
208
|
+
lockedRootHash: buildLockHashFromLockModule(projectRoot),
|
|
209
|
+
source: 'server',
|
|
210
|
+
});
|
|
211
|
+
assert.equal(
|
|
212
|
+
isRuntimeCompatibleLock(serverLock),
|
|
213
|
+
true,
|
|
214
|
+
'server-issued locks must be runtime-compatible',
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const serverDiag = await validateScanPath(projectRoot, serverLock.installToken);
|
|
218
|
+
assert.equal(serverDiag.ok, true, 'server lock must pass local path/token diagnostics');
|
|
219
|
+
assert.equal(
|
|
220
|
+
isRuntimeCompatibleLock(serverDiag.lock),
|
|
221
|
+
true,
|
|
222
|
+
'validated server lock must preserve runtime-compatible source',
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
const {
|
|
227
|
+
DeepScanRuntime,
|
|
228
|
+
LocalDeepScanStore,
|
|
229
|
+
StepperRegistry,
|
|
230
|
+
DETERMINISTIC_SCAN_STEPPER_ID,
|
|
231
|
+
SECURITY_TEST_PLAN_SCHEMA_VERSION,
|
|
232
|
+
SECURITY_TEST_PLAN_STATUS_SEMANTICS,
|
|
233
|
+
SECURITY_TEST_PLAN_STEPPER_ID,
|
|
234
|
+
AGENT_FIX_TASK_SCHEMA_VERSION,
|
|
235
|
+
AGENT_FIX_TASK_STEPPER_ID,
|
|
236
|
+
buildAgentFixTaskArtifact,
|
|
237
|
+
buildRalphComparisonArtifact,
|
|
238
|
+
buildRalphFailureStateArtifact,
|
|
239
|
+
buildDeterministicScanArtifact,
|
|
240
|
+
buildProjectMapArtifact,
|
|
241
|
+
buildSecurityTestPlanArtifact,
|
|
242
|
+
buildDeepScanStatus,
|
|
243
|
+
createArtifactRef,
|
|
244
|
+
hashAgentFixTaskArtifact,
|
|
245
|
+
hashRalphComparisonArtifact,
|
|
246
|
+
hashRalphFailureStateArtifact,
|
|
247
|
+
hashProjectMapArtifact,
|
|
248
|
+
hashSecurityTestPlanArtifact,
|
|
249
|
+
RALPH_COMPARISON_SCHEMA_VERSION,
|
|
250
|
+
RALPH_COMPARISON_STEPPER_ID,
|
|
251
|
+
RALPH_FAILURE_STATE_SCHEMA_VERSION,
|
|
252
|
+
RALPH_FAILURE_STATE_STEPPER_ID,
|
|
253
|
+
DEFAULT_RALPH_LOOP_RETRY,
|
|
254
|
+
ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
|
|
255
|
+
ACCEPTED_RISK_STATE_SCHEMA_VERSION,
|
|
256
|
+
ACCEPTED_RISK_STEPPER_ID,
|
|
257
|
+
appendAcceptedRiskRecord,
|
|
258
|
+
buildAcceptedRiskStateArtifact,
|
|
259
|
+
hashAcceptedRiskStateArtifact,
|
|
260
|
+
readAcceptedRiskRegister,
|
|
261
|
+
validateAcceptedRiskRecord,
|
|
262
|
+
validateAcceptedRiskRegister,
|
|
263
|
+
validateAcceptedRiskStateArtifact,
|
|
264
|
+
ralphAcceptStepper,
|
|
265
|
+
ralphTaskStepper,
|
|
266
|
+
ralphComparisonStepper,
|
|
267
|
+
ralphFailureTrackStepper,
|
|
268
|
+
createSampleStepperRegistry,
|
|
269
|
+
deterministicScanStepper,
|
|
270
|
+
projectMapStepper,
|
|
271
|
+
sampleArtifactStepper,
|
|
272
|
+
sampleNeedsHumanStepper,
|
|
273
|
+
sampleNoopStepper,
|
|
274
|
+
securityTestPlanStepper,
|
|
275
|
+
validateAgentFixTaskArtifact,
|
|
276
|
+
validateRalphComparisonArtifact,
|
|
277
|
+
validateRalphFailureStateArtifact,
|
|
278
|
+
validateStepResult,
|
|
279
|
+
validateSecurityTestPlanArtifact,
|
|
280
|
+
writeAgentFixTaskArtifact,
|
|
281
|
+
writeRalphComparisonArtifact,
|
|
282
|
+
writeRalphFailureStateArtifact,
|
|
283
|
+
writeDeterministicScanArtifact,
|
|
284
|
+
writeProjectMapArtifact,
|
|
285
|
+
writeSecurityTestPlanArtifact,
|
|
286
|
+
} = await import(pathToFileURL(path.resolve('./src/deep-scan/index.js')).href);
|
|
287
|
+
|
|
288
|
+
const artifact = createArtifactRef({
|
|
289
|
+
id: 'artifact-project-map',
|
|
290
|
+
type: 'project_map',
|
|
291
|
+
storage: 'local',
|
|
292
|
+
uri: '.vibesecur/deep-scans/run-1/project-map.json',
|
|
293
|
+
hash: 'c'.repeat(64),
|
|
294
|
+
preview: 'frameworks and route file names only',
|
|
295
|
+
metadata: { sourceSafe: true },
|
|
296
|
+
});
|
|
297
|
+
const validResult = {
|
|
298
|
+
stepperId: 'sample.artifact',
|
|
299
|
+
version: '1.0.0',
|
|
300
|
+
status: 'passed',
|
|
301
|
+
startedAt: '2026-05-26T00:00:00.000Z',
|
|
302
|
+
finishedAt: '2026-05-26T00:00:01.000Z',
|
|
303
|
+
evidence: [{
|
|
304
|
+
type: 'hash',
|
|
305
|
+
label: 'Project map hash',
|
|
306
|
+
hash: artifact.hash,
|
|
307
|
+
preview: 'metadata-only artifact reference',
|
|
308
|
+
}],
|
|
309
|
+
findings: [],
|
|
310
|
+
artifacts: [artifact],
|
|
311
|
+
summary: 'Created metadata-only project map reference.',
|
|
312
|
+
nextActions: ['Inspect Deep Scan status.'],
|
|
313
|
+
};
|
|
314
|
+
assert.deepEqual(
|
|
315
|
+
validateStepResult(validResult, { stepperId: 'sample.artifact', version: '1.0.0' }).artifacts,
|
|
316
|
+
[artifact],
|
|
317
|
+
'Deep Scan step result should accept source-safe artifact refs',
|
|
318
|
+
);
|
|
319
|
+
assert.throws(
|
|
320
|
+
() => validateStepResult({
|
|
321
|
+
...validResult,
|
|
322
|
+
artifacts: [{ ...artifact, content: 'const secret = "sk_live_should_not_be_logged";' }],
|
|
323
|
+
}, { stepperId: 'sample.artifact', version: '1.0.0' }),
|
|
324
|
+
/raw source|source-safe/i,
|
|
325
|
+
'Deep Scan artifact contracts must reject raw source-like fields',
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const fixtureBase = path.join(tmpBase, 'project-map-fixtures');
|
|
329
|
+
const writeFixture = async (relativePath, value) => {
|
|
330
|
+
const target = path.join(fixtureBase, relativePath);
|
|
331
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
332
|
+
await fs.writeFile(target, value, 'utf8');
|
|
333
|
+
};
|
|
334
|
+
await writeFixture('static-app/package.json', JSON.stringify({
|
|
335
|
+
scripts: { test: 'node --test', build: 'vite build' },
|
|
336
|
+
dependencies: { '@vitejs/plugin-react': '^5.0.0', vite: '^7.0.0', react: '^19.0.0' },
|
|
337
|
+
devDependencies: {},
|
|
338
|
+
}, null, 2));
|
|
339
|
+
await writeFixture('static-app/src/App.jsx', 'export function App(){ return <main>safe fixture</main>; }');
|
|
340
|
+
await writeFixture('static-app/.env.local', 'VITE_TOKEN=VS_FIXTURE_SECRET_DO_NOT_EXPORT');
|
|
341
|
+
await writeFixture('express-api/package.json', JSON.stringify({
|
|
342
|
+
scripts: { test: 'node --test tests/*.test.js', start: 'node src/server.js' },
|
|
343
|
+
dependencies: { express: '^5.0.0', pg: '^8.0.0' },
|
|
344
|
+
}, null, 2));
|
|
345
|
+
await writeFixture('express-api/src/routes/users.js', 'function secretRoute(){ return "VS_ROUTE_SOURCE_DO_NOT_EXPORT"; }');
|
|
346
|
+
await writeFixture('express-api/render.yaml', 'services: []');
|
|
347
|
+
await writeFixture('scan-app/package.json', JSON.stringify({
|
|
348
|
+
scripts: { test: 'node --test' },
|
|
349
|
+
dependencies: { express: '^5.0.0', jsonwebtoken: '^9.0.0' },
|
|
350
|
+
}, null, 2));
|
|
351
|
+
await writeFixture(
|
|
352
|
+
'scan-app/src/routes/auth.js',
|
|
353
|
+
'const jwt = require("jsonwebtoken");\nconst leaked = "sk_live_PHASE3_SELFTEST_SECRET";\napp.post("/login", (req, res) => jwt.sign({ user: req.body.user }, leaked));\n',
|
|
354
|
+
);
|
|
355
|
+
await writeFixture(
|
|
356
|
+
'scan-app/src/config.js',
|
|
357
|
+
'password = "CorrectHorseBatteryStaple"\njwt_secret: "phase3-jwt-secret"\nconst databaseUrl = "postgres://user:superpass@localhost:5432/app";\n',
|
|
358
|
+
);
|
|
359
|
+
await writeFixture(
|
|
360
|
+
'scan-app/app.py',
|
|
361
|
+
'password = "PythonSecretValue"\napi_key = "python_api_key_secret"\n',
|
|
362
|
+
);
|
|
363
|
+
await writeFixture('workspace/package.json', JSON.stringify({
|
|
364
|
+
workspaces: ['apps/*', 'packages/*'],
|
|
365
|
+
scripts: { test: 'npm run test -w app' },
|
|
366
|
+
devDependencies: { turbo: '^2.0.0' },
|
|
367
|
+
}, null, 2));
|
|
368
|
+
await writeFixture('workspace/apps/app/package.json', JSON.stringify({ scripts: { test: 'node --test' } }, null, 2));
|
|
369
|
+
await writeFixture('mcp-tool/package.json', JSON.stringify({
|
|
370
|
+
bin: { fixture: './src/cli.js' },
|
|
371
|
+
dependencies: { '@modelcontextprotocol/sdk': '^1.29.0', zod: '^4.0.0' },
|
|
372
|
+
}, null, 2));
|
|
373
|
+
await writeFixture('sensitive-scripts/package.json', JSON.stringify({
|
|
374
|
+
scripts: {
|
|
375
|
+
verify: 'node test.js --token plain-token-123 --api-key=plain-api-key-456 --password "plain-password-789"',
|
|
376
|
+
lint: 'curl -H "Authorization: Bearer plain-bearer-token-123" https://user:pass@example.com/check',
|
|
377
|
+
},
|
|
378
|
+
dependencies: {},
|
|
379
|
+
}, null, 2));
|
|
380
|
+
await writeFixture('unknown/README.md', '# Unknown fixture\n');
|
|
381
|
+
|
|
382
|
+
const projectMapSnapshot = (artifact) => ({
|
|
383
|
+
packageManagers: artifact.packageManagers,
|
|
384
|
+
frameworkNames: artifact.frameworks.map((item) => item.name),
|
|
385
|
+
runtimes: artifact.runtimes,
|
|
386
|
+
languageNames: artifact.languages.map((item) => item.language),
|
|
387
|
+
routeFiles: artifact.routeFiles,
|
|
388
|
+
testCommands: artifact.testCommands,
|
|
389
|
+
deploymentFiles: artifact.deploymentFiles,
|
|
390
|
+
envFiles: artifact.envFiles,
|
|
391
|
+
manifestPaths: artifact.manifests.map((item) => item.path),
|
|
392
|
+
workspace: artifact.workspace,
|
|
393
|
+
privacy: {
|
|
394
|
+
envValuesCaptured: artifact.privacy.envValuesCaptured,
|
|
395
|
+
sourceBodiesCaptured: artifact.privacy.sourceBodiesCaptured,
|
|
396
|
+
rawSourceStored: artifact.privacy.rawSourceStored,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const staticMap = await buildProjectMapArtifact({
|
|
401
|
+
rootPath: path.join(fixtureBase, 'static-app'),
|
|
402
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
403
|
+
});
|
|
404
|
+
assert.deepEqual(projectMapSnapshot(staticMap), {
|
|
405
|
+
packageManagers: ['npm'],
|
|
406
|
+
frameworkNames: ['react', 'vite'],
|
|
407
|
+
runtimes: ['browser', 'node'],
|
|
408
|
+
languageNames: ['JavaScript/TypeScript'],
|
|
409
|
+
routeFiles: [],
|
|
410
|
+
testCommands: [{ source: 'package.json', name: 'test', command: 'node --test' }],
|
|
411
|
+
deploymentFiles: [],
|
|
412
|
+
envFiles: [{ path: '.env.local', kind: 'env', valuesCaptured: false }],
|
|
413
|
+
manifestPaths: ['package.json'],
|
|
414
|
+
workspace: { detected: false, manifests: [], patterns: [] },
|
|
415
|
+
privacy: {
|
|
416
|
+
envValuesCaptured: false,
|
|
417
|
+
sourceBodiesCaptured: false,
|
|
418
|
+
rawSourceStored: false,
|
|
419
|
+
},
|
|
420
|
+
}, 'Static app Project Map snapshot should stay stable');
|
|
421
|
+
assert.deepEqual(staticMap.packageManagers, ['npm'], 'Project Map should detect npm package manager');
|
|
422
|
+
assert.ok(staticMap.frameworks.some((item) => item.name === 'vite'), 'Project Map should detect Vite');
|
|
423
|
+
assert.ok(staticMap.frameworks.some((item) => item.name === 'react'), 'Project Map should detect React');
|
|
424
|
+
assert.ok(staticMap.languages.some((item) => item.language === 'JavaScript/TypeScript'), 'Project Map should summarize JS/TS files');
|
|
425
|
+
assert.ok(staticMap.testCommands.some((item) => item.command === 'node --test'), 'Project Map should capture package test scripts');
|
|
426
|
+
assert.ok(staticMap.envFiles.some((item) => item.path === '.env.local'), 'Project Map should list env filenames');
|
|
427
|
+
assert.equal(staticMap.privacy.envValuesCaptured, false, 'Project Map must not capture env values');
|
|
428
|
+
assert.doesNotMatch(JSON.stringify(staticMap), /VS_FIXTURE_SECRET_DO_NOT_EXPORT|export function App/i);
|
|
429
|
+
|
|
430
|
+
const expressMap = await buildProjectMapArtifact({
|
|
431
|
+
rootPath: path.join(fixtureBase, 'express-api'),
|
|
432
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
433
|
+
});
|
|
434
|
+
assert.deepEqual(projectMapSnapshot(expressMap), {
|
|
435
|
+
packageManagers: ['npm'],
|
|
436
|
+
frameworkNames: ['express'],
|
|
437
|
+
runtimes: ['node'],
|
|
438
|
+
languageNames: ['JavaScript/TypeScript', 'YAML'],
|
|
439
|
+
routeFiles: [{ path: 'src/routes/users.js', kind: 'route_file' }],
|
|
440
|
+
testCommands: [{ source: 'package.json', name: 'test', command: 'node --test tests/*.test.js' }],
|
|
441
|
+
deploymentFiles: [{ path: 'render.yaml', type: 'render' }],
|
|
442
|
+
envFiles: [],
|
|
443
|
+
manifestPaths: ['package.json'],
|
|
444
|
+
workspace: { detected: false, manifests: [], patterns: [] },
|
|
445
|
+
privacy: {
|
|
446
|
+
envValuesCaptured: false,
|
|
447
|
+
sourceBodiesCaptured: false,
|
|
448
|
+
rawSourceStored: false,
|
|
449
|
+
},
|
|
450
|
+
}, 'Express API Project Map snapshot should stay stable');
|
|
451
|
+
assert.ok(expressMap.frameworks.some((item) => item.name === 'express'), 'Project Map should detect Express');
|
|
452
|
+
assert.ok(expressMap.routeFiles.some((item) => item.path === 'src/routes/users.js'), 'Project Map should detect route-like files');
|
|
453
|
+
assert.ok(expressMap.deploymentFiles.some((item) => item.path === 'render.yaml'), 'Project Map should detect Render deployment files');
|
|
454
|
+
assert.doesNotMatch(JSON.stringify(expressMap), /VS_ROUTE_SOURCE_DO_NOT_EXPORT|function secretRoute/i);
|
|
455
|
+
|
|
456
|
+
const workspaceMap = await buildProjectMapArtifact({
|
|
457
|
+
rootPath: path.join(fixtureBase, 'workspace'),
|
|
458
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
459
|
+
});
|
|
460
|
+
assert.deepEqual(projectMapSnapshot(workspaceMap), {
|
|
461
|
+
packageManagers: ['npm'],
|
|
462
|
+
frameworkNames: [],
|
|
463
|
+
runtimes: ['node'],
|
|
464
|
+
languageNames: [],
|
|
465
|
+
routeFiles: [],
|
|
466
|
+
testCommands: [
|
|
467
|
+
{ source: 'apps/app/package.json', name: 'test', command: 'node --test' },
|
|
468
|
+
{ source: 'package.json', name: 'test', command: 'npm run test -w app' },
|
|
469
|
+
],
|
|
470
|
+
deploymentFiles: [],
|
|
471
|
+
envFiles: [],
|
|
472
|
+
manifestPaths: ['apps/app/package.json', 'package.json'],
|
|
473
|
+
workspace: { detected: true, manifests: ['package.json'], patterns: ['apps/*', 'packages/*'] },
|
|
474
|
+
privacy: {
|
|
475
|
+
envValuesCaptured: false,
|
|
476
|
+
sourceBodiesCaptured: false,
|
|
477
|
+
rawSourceStored: false,
|
|
478
|
+
},
|
|
479
|
+
}, 'Workspace Project Map snapshot should stay stable');
|
|
480
|
+
assert.equal(workspaceMap.workspace.detected, true, 'Project Map should detect package workspaces');
|
|
481
|
+
assert.ok(workspaceMap.manifests.some((item) => item.path === 'apps/app/package.json'), 'Project Map should include workspace manifests');
|
|
482
|
+
|
|
483
|
+
const mcpMap = await buildProjectMapArtifact({
|
|
484
|
+
rootPath: path.join(fixtureBase, 'mcp-tool'),
|
|
485
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
486
|
+
});
|
|
487
|
+
assert.deepEqual(projectMapSnapshot(mcpMap), {
|
|
488
|
+
packageManagers: ['npm'],
|
|
489
|
+
frameworkNames: ['mcp'],
|
|
490
|
+
runtimes: ['node'],
|
|
491
|
+
languageNames: [],
|
|
492
|
+
routeFiles: [],
|
|
493
|
+
testCommands: [],
|
|
494
|
+
deploymentFiles: [],
|
|
495
|
+
envFiles: [],
|
|
496
|
+
manifestPaths: ['package.json'],
|
|
497
|
+
workspace: { detected: false, manifests: [], patterns: [] },
|
|
498
|
+
privacy: {
|
|
499
|
+
envValuesCaptured: false,
|
|
500
|
+
sourceBodiesCaptured: false,
|
|
501
|
+
rawSourceStored: false,
|
|
502
|
+
},
|
|
503
|
+
}, 'MCP tool Project Map snapshot should stay stable');
|
|
504
|
+
assert.ok(mcpMap.frameworks.some((item) => item.name === 'mcp'), 'Project Map should detect MCP packages');
|
|
505
|
+
|
|
506
|
+
const sensitiveScriptsMap = await buildProjectMapArtifact({
|
|
507
|
+
rootPath: path.join(fixtureBase, 'sensitive-scripts'),
|
|
508
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
509
|
+
});
|
|
510
|
+
assert.deepEqual(sensitiveScriptsMap.testCommands, [
|
|
511
|
+
{
|
|
512
|
+
source: 'package.json',
|
|
513
|
+
name: 'lint',
|
|
514
|
+
command: 'curl -H "Authorization: Bearer [redacted]" https://[redacted]@example.com/check',
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
source: 'package.json',
|
|
518
|
+
name: 'verify',
|
|
519
|
+
command: 'node test.js --token [redacted] --api-key=[redacted] --password [redacted]',
|
|
520
|
+
},
|
|
521
|
+
], 'Project Map should redact credentials embedded in test/check command strings');
|
|
522
|
+
assert.doesNotMatch(
|
|
523
|
+
JSON.stringify(sensitiveScriptsMap),
|
|
524
|
+
/plain-token-123|plain-api-key-456|plain-password-789|plain-bearer-token-123|user:pass/i,
|
|
525
|
+
'Project Map command metadata must not retain script-embedded credential values',
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const unknownMap = await buildProjectMapArtifact({
|
|
529
|
+
rootPath: path.join(fixtureBase, 'unknown'),
|
|
530
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
531
|
+
});
|
|
532
|
+
assert.deepEqual(projectMapSnapshot(unknownMap), {
|
|
533
|
+
packageManagers: [],
|
|
534
|
+
frameworkNames: [],
|
|
535
|
+
runtimes: [],
|
|
536
|
+
languageNames: ['Markdown'],
|
|
537
|
+
routeFiles: [],
|
|
538
|
+
testCommands: [],
|
|
539
|
+
deploymentFiles: [],
|
|
540
|
+
envFiles: [],
|
|
541
|
+
manifestPaths: [],
|
|
542
|
+
workspace: { detected: false, manifests: [], patterns: [] },
|
|
543
|
+
privacy: {
|
|
544
|
+
envValuesCaptured: false,
|
|
545
|
+
sourceBodiesCaptured: false,
|
|
546
|
+
rawSourceStored: false,
|
|
547
|
+
},
|
|
548
|
+
}, 'Unknown project Project Map snapshot should stay stable');
|
|
549
|
+
assert.equal(unknownMap.frameworks.length, 0, 'Unknown projects should produce a valid map without fake frameworks');
|
|
550
|
+
|
|
551
|
+
const firstHash = hashProjectMapArtifact(staticMap);
|
|
552
|
+
const changedHash = hashProjectMapArtifact({
|
|
553
|
+
...staticMap,
|
|
554
|
+
testCommands: [...staticMap.testCommands, { source: 'package.json', name: 'lint', command: 'eslint .' }],
|
|
555
|
+
});
|
|
556
|
+
assert.match(firstHash, /^[a-f0-9]{64}$/i, 'Project Map hash should be SHA-256');
|
|
557
|
+
assert.notEqual(changedHash, firstHash, 'Project Map hash should change when safe artifact metadata changes');
|
|
558
|
+
|
|
559
|
+
const projectMapDefinition = projectMapStepper();
|
|
560
|
+
const projectMapResult = await projectMapDefinition.run({
|
|
561
|
+
runId: 'deep_scan_project_map_selftest',
|
|
562
|
+
projectId: 'fixture-static',
|
|
563
|
+
config: { rootPath: path.join(fixtureBase, 'static-app') },
|
|
564
|
+
state: {},
|
|
565
|
+
});
|
|
566
|
+
assert.equal(projectMapResult.stepperId, 'project.map', 'Project Map stepper id should be stable');
|
|
567
|
+
assert.equal(projectMapResult.artifacts[0].type, 'project_map', 'Project Map stepper should emit a project_map artifact ref');
|
|
568
|
+
assert.match(projectMapResult.artifacts[0].hash, /^[a-f0-9]{64}$/i, 'Project Map artifact ref should include a hash');
|
|
569
|
+
assert.doesNotMatch(JSON.stringify(projectMapResult), /VS_FIXTURE_SECRET_DO_NOT_EXPORT|export function App/i);
|
|
570
|
+
|
|
571
|
+
const deterministicDefinition = deterministicScanStepper();
|
|
572
|
+
const deterministicResult = await deterministicDefinition.run({
|
|
573
|
+
runId: 'deep_scan_deterministic_selftest',
|
|
574
|
+
projectId: 'fixture-scan',
|
|
575
|
+
config: {
|
|
576
|
+
rootPath: path.join(fixtureBase, 'scan-app'),
|
|
577
|
+
maxFiles: 25,
|
|
578
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
579
|
+
},
|
|
580
|
+
state: {},
|
|
581
|
+
});
|
|
582
|
+
assert.equal(
|
|
583
|
+
deterministicResult.stepperId,
|
|
584
|
+
DETERMINISTIC_SCAN_STEPPER_ID,
|
|
585
|
+
'Deterministic scan stepper id should be stable',
|
|
586
|
+
);
|
|
587
|
+
assert.equal(deterministicResult.status, 'passed', 'Deterministic scan stepper should pass when the scan runs');
|
|
588
|
+
assert.ok(deterministicResult.findings.length > 0, 'Deterministic scan stepper should surface scanner findings');
|
|
589
|
+
assert.ok(
|
|
590
|
+
deterministicResult.findings.every((finding) =>
|
|
591
|
+
finding.id
|
|
592
|
+
&& finding.ruleId
|
|
593
|
+
&& finding.ruleName
|
|
594
|
+
&& finding.status === 'open'
|
|
595
|
+
&& finding.source?.filePath
|
|
596
|
+
&& Number.isInteger(finding.source?.lineNumber)
|
|
597
|
+
&& finding.fingerprint),
|
|
598
|
+
'Deterministic scan findings should be normalized for downstream report/passport/Ralph consumers',
|
|
599
|
+
);
|
|
600
|
+
assert.ok(
|
|
601
|
+
deterministicResult.artifacts.some((ref) => ref.type === 'deterministic_scan'),
|
|
602
|
+
'Deterministic scan stepper should emit a deterministic_scan artifact ref',
|
|
603
|
+
);
|
|
604
|
+
assert.ok(
|
|
605
|
+
deterministicResult.receipts.some((receipt) =>
|
|
606
|
+
receipt.type === 'deterministic_scan'
|
|
607
|
+
&& receipt.engine?.counts?.deterministic > 0
|
|
608
|
+
&& receipt.summary?.totalIssues === deterministicResult.findings.length),
|
|
609
|
+
'Deterministic scan stepper should attach receipt metadata with rule counts and issue summary',
|
|
610
|
+
);
|
|
611
|
+
const deterministicArtifactRef = deterministicResult.artifacts.find((ref) => ref.type === 'deterministic_scan');
|
|
612
|
+
const deterministicArtifactText = await fs.readFile(
|
|
613
|
+
path.join(fixtureBase, 'scan-app', deterministicArtifactRef.uri),
|
|
614
|
+
'utf8',
|
|
615
|
+
);
|
|
616
|
+
assert.ok(
|
|
617
|
+
['S002', 'S003', 'S004', 'P004'].every((ruleId) =>
|
|
618
|
+
deterministicResult.findings.some((finding) => finding.ruleId === ruleId)),
|
|
619
|
+
'Deterministic scan should preserve finding metadata for JS and Python secret rules',
|
|
620
|
+
);
|
|
621
|
+
assert.doesNotMatch(
|
|
622
|
+
JSON.stringify(deterministicResult),
|
|
623
|
+
/CorrectHorseBatteryStaple|phase3-jwt-secret|postgres:\/\/user:superpass|PythonSecretValue|python_api_key_secret/i,
|
|
624
|
+
'Deterministic scan step result must not retain raw JS or Python secret values',
|
|
625
|
+
);
|
|
626
|
+
assert.doesNotMatch(
|
|
627
|
+
deterministicArtifactText,
|
|
628
|
+
/CorrectHorseBatteryStaple|phase3-jwt-secret|postgres:\/\/user:superpass|PythonSecretValue|python_api_key_secret/i,
|
|
629
|
+
'Written deterministic scan artifact must not retain raw JS or Python secret values',
|
|
630
|
+
);
|
|
631
|
+
assert.match(
|
|
632
|
+
deterministicArtifactText,
|
|
633
|
+
/Preview withheld/,
|
|
634
|
+
'Written deterministic scan artifact should use generic local-source previews instead of scanner snippets',
|
|
635
|
+
);
|
|
636
|
+
assert.doesNotMatch(
|
|
637
|
+
JSON.stringify(deterministicResult),
|
|
638
|
+
/sk_live_PHASE3_SELFTEST_SECRET|jwt\.sign|const leaked|require\("jsonwebtoken"\)/i,
|
|
639
|
+
'Deterministic scan step result must not retain raw source or secret values',
|
|
640
|
+
);
|
|
641
|
+
await assert.rejects(
|
|
642
|
+
() => writeProjectMapArtifact({ rootPath: projectRoot, runId: '..', artifact: staticMap }),
|
|
643
|
+
/runId/i,
|
|
644
|
+
'Project Map artifact writes should reject dot-only run ids',
|
|
645
|
+
);
|
|
646
|
+
const deterministicArtifact = await buildDeterministicScanArtifact({
|
|
647
|
+
rootPath: path.join(fixtureBase, 'scan-app'),
|
|
648
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
649
|
+
maxFiles: 25,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const scanProjectMap = await buildProjectMapArtifact({
|
|
653
|
+
rootPath: path.join(fixtureBase, 'scan-app'),
|
|
654
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
655
|
+
});
|
|
656
|
+
const testPlanArtifact = buildSecurityTestPlanArtifact({
|
|
657
|
+
projectMap: scanProjectMap,
|
|
658
|
+
deterministicScan: deterministicArtifact,
|
|
659
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
660
|
+
});
|
|
661
|
+
assert.equal(
|
|
662
|
+
testPlanArtifact.schemaVersion,
|
|
663
|
+
SECURITY_TEST_PLAN_SCHEMA_VERSION,
|
|
664
|
+
'Security Test Plan artifact schema version should be stable',
|
|
665
|
+
);
|
|
666
|
+
assert.ok(testPlanArtifact.testCases.length > 0, 'Security Test Plan should emit evidence-tied tests');
|
|
667
|
+
assert.deepEqual(
|
|
668
|
+
testPlanArtifact.statusSemantics,
|
|
669
|
+
SECURITY_TEST_PLAN_STATUS_SEMANTICS,
|
|
670
|
+
'Security Test Plan should embed stable status semantics for downstream consumers',
|
|
671
|
+
);
|
|
672
|
+
assert.ok(
|
|
673
|
+
testPlanArtifact.downstreamContract?.stableCandidateFields.includes('suggestedCommand')
|
|
674
|
+
&& testPlanArtifact.downstreamContract?.stableCandidateFields.includes('priority'),
|
|
675
|
+
'Security Test Plan should publish downstream-stable candidate fields',
|
|
676
|
+
);
|
|
677
|
+
assert.ok(
|
|
678
|
+
testPlanArtifact.testCases.every((testCase) =>
|
|
679
|
+
testCase.id
|
|
680
|
+
&& ['planned', 'runnable', 'blocked', 'manual'].includes(testCase.status)
|
|
681
|
+
&& testCase.priority
|
|
682
|
+
&& Object.prototype.hasOwnProperty.call(testCase, 'suggestedCommand')
|
|
683
|
+
&& Object.prototype.hasOwnProperty.call(testCase, 'blockedReason')
|
|
684
|
+
&& Object.prototype.hasOwnProperty.call(testCase, 'manualReason')
|
|
685
|
+
&& Array.isArray(testCase.evidence)
|
|
686
|
+
&& testCase.evidence.length > 0
|
|
687
|
+
&& Array.isArray(testCase.sourceArtifactRefs)
|
|
688
|
+
&& testCase.sourceArtifactRefs.length > 0),
|
|
689
|
+
'Every Security Test Plan item should have explicit status and source evidence',
|
|
690
|
+
);
|
|
691
|
+
assert.ok(
|
|
692
|
+
testPlanArtifact.testCases.some((testCase) =>
|
|
693
|
+
testCase.sourceFindingIds.length > 0
|
|
694
|
+
&& testCase.targetSurface.files.includes('src/routes/auth.js')
|
|
695
|
+
&& testCase.status === 'runnable'
|
|
696
|
+
&& testCase.commandHint === 'node --test'
|
|
697
|
+
&& testCase.suggestedCommand === 'node --test'),
|
|
698
|
+
'Security Test Plan should map deterministic findings to runnable evidence-tied regression tests',
|
|
699
|
+
);
|
|
700
|
+
assert.ok(
|
|
701
|
+
testPlanArtifact.testCases.some((testCase) =>
|
|
702
|
+
testCase.category === 'auth_session'
|
|
703
|
+
&& testCase.targetSurface.files.includes('src/routes/auth.js')
|
|
704
|
+
&& testCase.status === 'manual'
|
|
705
|
+
&& testCase.manualReason),
|
|
706
|
+
'Security Test Plan should create manual-only auth/session review checks without requiring credentials in artifacts',
|
|
707
|
+
);
|
|
708
|
+
assert.ok(
|
|
709
|
+
testPlanArtifact.testCases.some((testCase) =>
|
|
710
|
+
testCase.category === 'api_abuse'
|
|
711
|
+
&& testCase.targetSurface.files.includes('src/routes/auth.js')
|
|
712
|
+
&& testCase.status === 'runnable'),
|
|
713
|
+
'Security Test Plan should map Project Map route files to targeted API abuse tests',
|
|
714
|
+
);
|
|
715
|
+
assert.equal(
|
|
716
|
+
new Set(testPlanArtifact.testCases.map((testCase) => testCase.dedupeKey)).size,
|
|
717
|
+
testPlanArtifact.testCases.length,
|
|
718
|
+
'Security Test Plan should dedupe candidate tests by stable target/risk keys',
|
|
719
|
+
);
|
|
720
|
+
assert.deepEqual(
|
|
721
|
+
testPlanArtifact.testCases.map((testCase) => testCase.rank),
|
|
722
|
+
[...testPlanArtifact.testCases.keys()].map((index) => index + 1),
|
|
723
|
+
'Security Test Plan ranks should be sequential after risk sorting',
|
|
724
|
+
);
|
|
725
|
+
assert.equal(
|
|
726
|
+
testPlanArtifact.summary.byStatus.runnable > 0,
|
|
727
|
+
true,
|
|
728
|
+
'Security Test Plan summary should count runnable tests when local test commands exist',
|
|
729
|
+
);
|
|
730
|
+
const plannedFindingPlan = buildSecurityTestPlanArtifact({
|
|
731
|
+
projectMap: {
|
|
732
|
+
...scanProjectMap,
|
|
733
|
+
testCommands: [],
|
|
734
|
+
routeFiles: [],
|
|
735
|
+
envFiles: [],
|
|
736
|
+
deploymentFiles: [],
|
|
737
|
+
},
|
|
738
|
+
deterministicScan: deterministicArtifact,
|
|
739
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
740
|
+
sourceRefs: {
|
|
741
|
+
projectMap: 'artifact-project-map-planned',
|
|
742
|
+
deterministicScan: 'artifact-rules-scan-planned',
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
assert.ok(
|
|
746
|
+
plannedFindingPlan.testCases.every((testCase) =>
|
|
747
|
+
testCase.status === 'planned'
|
|
748
|
+
&& testCase.runner === 'agent_written_test'
|
|
749
|
+
&& testCase.suggestedCommand === null
|
|
750
|
+
&& testCase.sourceArtifactRefs.includes('artifact-rules-scan-planned')),
|
|
751
|
+
'Security Test Plan should mark finding-backed candidates planned when no local harness signal exists',
|
|
752
|
+
);
|
|
753
|
+
const unknownCommandPlan = buildSecurityTestPlanArtifact({
|
|
754
|
+
projectMap: {
|
|
755
|
+
...scanProjectMap,
|
|
756
|
+
testCommands: [{
|
|
757
|
+
source: 'package.json',
|
|
758
|
+
name: 'verify',
|
|
759
|
+
command: 'curl -H "Authorization: Bearer [redacted]" https://example.com/check',
|
|
760
|
+
}],
|
|
761
|
+
routeFiles: [{ path: 'src/routes/payments.js', kind: 'route_file' }],
|
|
762
|
+
envFiles: [],
|
|
763
|
+
deploymentFiles: [],
|
|
764
|
+
},
|
|
765
|
+
deterministicScan: {
|
|
766
|
+
...deterministicArtifact,
|
|
767
|
+
findings: [],
|
|
768
|
+
summary: { ...deterministicArtifact.summary, totalIssues: 0, bySeverity: {} },
|
|
769
|
+
},
|
|
770
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
771
|
+
});
|
|
772
|
+
assert.equal(
|
|
773
|
+
unknownCommandPlan.summary.runnableCount,
|
|
774
|
+
0,
|
|
775
|
+
'Unknown or redacted commands should not make Security Test Plan candidates runnable',
|
|
776
|
+
);
|
|
777
|
+
assert.ok(
|
|
778
|
+
unknownCommandPlan.testCases.some((testCase) =>
|
|
779
|
+
testCase.status === 'blocked'
|
|
780
|
+
&& testCase.runner === 'blocked_no_harness'
|
|
781
|
+
&& /No local test command/.test(testCase.blockedReason)),
|
|
782
|
+
'Security Test Plan should block route/dependency candidates when no recognized local test harness exists',
|
|
783
|
+
);
|
|
784
|
+
const noFindingsPlan = buildSecurityTestPlanArtifact({
|
|
785
|
+
projectMap: expressMap,
|
|
786
|
+
deterministicScan: {
|
|
787
|
+
...deterministicArtifact,
|
|
788
|
+
findings: [],
|
|
789
|
+
summary: { ...deterministicArtifact.summary, totalIssues: 0, bySeverity: {} },
|
|
790
|
+
},
|
|
791
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
792
|
+
});
|
|
793
|
+
assert.ok(
|
|
794
|
+
noFindingsPlan.testCases.length > 0
|
|
795
|
+
&& noFindingsPlan.testCases.every((testCase) => testCase.sourceFindingIds.length === 0),
|
|
796
|
+
'Security Test Plan should still emit Project Map evidence candidates when deterministic findings are absent',
|
|
797
|
+
);
|
|
798
|
+
const deterministicSnapshot = (plan) => plan.testCases.map((testCase) => ({
|
|
799
|
+
id: testCase.id,
|
|
800
|
+
dedupeKey: testCase.dedupeKey,
|
|
801
|
+
rank: testCase.rank,
|
|
802
|
+
sourceArtifactRefs: testCase.sourceArtifactRefs,
|
|
803
|
+
sourceFindingIds: testCase.sourceFindingIds,
|
|
804
|
+
targetSurface: testCase.targetSurface,
|
|
805
|
+
}));
|
|
806
|
+
assert.deepEqual(
|
|
807
|
+
deterministicSnapshot(testPlanArtifact),
|
|
808
|
+
deterministicSnapshot(buildSecurityTestPlanArtifact({
|
|
809
|
+
projectMap: scanProjectMap,
|
|
810
|
+
deterministicScan: deterministicArtifact,
|
|
811
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
812
|
+
})),
|
|
813
|
+
'Security Test Plan candidate ids, refs, finding links, dedupe keys, and ranks should be deterministic',
|
|
814
|
+
);
|
|
815
|
+
assert.match(
|
|
816
|
+
testPlanArtifact.sources.projectMap.rootPathHash,
|
|
817
|
+
/^[a-f0-9]{64}$/i,
|
|
818
|
+
'Security Test Plan should preserve Project Map source hash metadata for downstream evidence',
|
|
819
|
+
);
|
|
820
|
+
assert.doesNotMatch(
|
|
821
|
+
JSON.stringify(testPlanArtifact),
|
|
822
|
+
/CorrectHorseBatteryStaple|phase3-jwt-secret|postgres:\/\/user:superpass|PythonSecretValue|python_api_key_secret|sk_live_PHASE3_SELFTEST_SECRET|jwt\.sign|const leaked/i,
|
|
823
|
+
'Security Test Plan artifact must not retain raw source or secret values',
|
|
824
|
+
);
|
|
825
|
+
assert.deepEqual(
|
|
826
|
+
validateSecurityTestPlanArtifact(testPlanArtifact).testCases.map((testCase) => testCase.status),
|
|
827
|
+
testPlanArtifact.testCases.map((testCase) => testCase.status),
|
|
828
|
+
'Security Test Plan validator should preserve explicit statuses',
|
|
829
|
+
);
|
|
830
|
+
assert.throws(
|
|
831
|
+
() => validateSecurityTestPlanArtifact({
|
|
832
|
+
...testPlanArtifact,
|
|
833
|
+
testCases: [{ ...testPlanArtifact.testCases[0], evidence: [] }],
|
|
834
|
+
}),
|
|
835
|
+
/evidence/i,
|
|
836
|
+
'Security Test Plan validator should reject generic tests without evidence',
|
|
837
|
+
);
|
|
838
|
+
assert.throws(
|
|
839
|
+
() => validateSecurityTestPlanArtifact({
|
|
840
|
+
...testPlanArtifact,
|
|
841
|
+
testCases: [{
|
|
842
|
+
...testPlanArtifact.testCases[0],
|
|
843
|
+
suggestedCommand: 'node --test',
|
|
844
|
+
metadata: { providerKey: 'sk_live_PHASE4_PROVIDER_SECRET' },
|
|
845
|
+
}],
|
|
846
|
+
}),
|
|
847
|
+
/source-safe|secret/i,
|
|
848
|
+
'Security Test Plan validator should reject provider keys, install tokens, JWT secrets, and generated test bodies',
|
|
849
|
+
);
|
|
850
|
+
const emptyTestPlan = buildSecurityTestPlanArtifact({
|
|
851
|
+
projectMap: unknownMap,
|
|
852
|
+
deterministicScan: {
|
|
853
|
+
...deterministicArtifact,
|
|
854
|
+
findings: [],
|
|
855
|
+
summary: { ...deterministicArtifact.summary, totalIssues: 0, bySeverity: {} },
|
|
856
|
+
},
|
|
857
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
858
|
+
});
|
|
859
|
+
assert.equal(emptyTestPlan.testCases.length, 0, 'Security Test Plan should not emit generic filler tests');
|
|
860
|
+
assert.equal(
|
|
861
|
+
hashSecurityTestPlanArtifact(testPlanArtifact),
|
|
862
|
+
hashSecurityTestPlanArtifact(validateSecurityTestPlanArtifact(testPlanArtifact)),
|
|
863
|
+
'Security Test Plan artifact hash should be stable across validation',
|
|
864
|
+
);
|
|
865
|
+
const testPlanDefinition = securityTestPlanStepper();
|
|
866
|
+
const testPlanProjectRoot = path.join(fixtureBase, 'scan-app');
|
|
867
|
+
const projectMapForPlanResult = await projectMapDefinition.run({
|
|
868
|
+
runId: 'deep_scan_test_plan_selftest',
|
|
869
|
+
projectId: 'fixture-scan',
|
|
870
|
+
config: { rootPath: testPlanProjectRoot },
|
|
871
|
+
state: {},
|
|
872
|
+
});
|
|
873
|
+
const deterministicForPlanResult = await deterministicDefinition.run({
|
|
874
|
+
runId: 'deep_scan_test_plan_selftest',
|
|
875
|
+
projectId: 'fixture-scan',
|
|
876
|
+
config: {
|
|
877
|
+
rootPath: testPlanProjectRoot,
|
|
878
|
+
maxFiles: 25,
|
|
879
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
880
|
+
},
|
|
881
|
+
state: {},
|
|
882
|
+
});
|
|
883
|
+
const testPlanResult = await testPlanDefinition.run({
|
|
884
|
+
runId: 'deep_scan_test_plan_selftest',
|
|
885
|
+
projectId: 'fixture-scan',
|
|
886
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
887
|
+
state: {
|
|
888
|
+
artifacts: {
|
|
889
|
+
'project.map': projectMapForPlanResult.artifacts[0],
|
|
890
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
891
|
+
},
|
|
892
|
+
findings: deterministicForPlanResult.findings,
|
|
893
|
+
receipts: deterministicForPlanResult.receipts,
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
assert.equal(
|
|
897
|
+
testPlanResult.stepperId,
|
|
898
|
+
SECURITY_TEST_PLAN_STEPPER_ID,
|
|
899
|
+
'Security Test Plan stepper id should be stable',
|
|
900
|
+
);
|
|
901
|
+
assert.equal(testPlanResult.status, 'passed', 'Security Test Plan stepper should pass with Project Map and scan inputs');
|
|
902
|
+
assert.ok(
|
|
903
|
+
testPlanResult.artifacts.some((ref) => ref.type === 'security_test_plan'),
|
|
904
|
+
'Security Test Plan stepper should emit a security_test_plan artifact ref',
|
|
905
|
+
);
|
|
906
|
+
await assert.rejects(
|
|
907
|
+
() => testPlanDefinition.run({
|
|
908
|
+
runId: 'deep_scan_test_plan_missing_project_map',
|
|
909
|
+
projectId: 'fixture-scan',
|
|
910
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
911
|
+
state: {
|
|
912
|
+
artifacts: {
|
|
913
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
}),
|
|
917
|
+
/project_map|requires/i,
|
|
918
|
+
'Security Test Plan stepper should fail closed when the Project Map artifact ref is missing',
|
|
919
|
+
);
|
|
920
|
+
const testPlanArtifactRef = testPlanResult.artifacts.find((ref) => ref.type === 'security_test_plan');
|
|
921
|
+
const testPlanArtifactText = await fs.readFile(
|
|
922
|
+
path.join(testPlanProjectRoot, testPlanArtifactRef.uri),
|
|
923
|
+
'utf8',
|
|
924
|
+
);
|
|
925
|
+
assert.match(testPlanArtifactText, /security_test_plan\.v1/, 'Written Security Test Plan artifact should include schema version');
|
|
926
|
+
assert.doesNotMatch(
|
|
927
|
+
testPlanArtifactText,
|
|
928
|
+
/CorrectHorseBatteryStaple|phase3-jwt-secret|postgres:\/\/user:superpass|PythonSecretValue|python_api_key_secret|sk_live_PHASE3_SELFTEST_SECRET|jwt\.sign|const leaked/i,
|
|
929
|
+
'Written Security Test Plan artifact must remain source-safe',
|
|
930
|
+
);
|
|
931
|
+
await assert.rejects(
|
|
932
|
+
() => writeSecurityTestPlanArtifact({ rootPath: projectRoot, runId: '.', artifact: testPlanArtifact }),
|
|
933
|
+
/runId/i,
|
|
934
|
+
'Security Test Plan artifact writes should reject dot-only run ids',
|
|
935
|
+
);
|
|
936
|
+
await assert.rejects(
|
|
937
|
+
() => writeDeterministicScanArtifact({ rootPath: projectRoot, runId: '.', artifact: deterministicArtifact }),
|
|
938
|
+
/runId/i,
|
|
939
|
+
'Deterministic Scan artifact writes should reject dot-only run ids',
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
const ralphTaskArtifact = buildAgentFixTaskArtifact({
|
|
943
|
+
securityTestPlan: testPlanArtifact,
|
|
944
|
+
deterministicScan: deterministicArtifact,
|
|
945
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
946
|
+
sourceRefs: {
|
|
947
|
+
securityTestPlan: 'artifact-test-plan-for-ralph',
|
|
948
|
+
deterministicScan: 'artifact-rules-scan-for-ralph',
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
assert.equal(
|
|
952
|
+
ralphTaskArtifact.schemaVersion,
|
|
953
|
+
AGENT_FIX_TASK_SCHEMA_VERSION,
|
|
954
|
+
'Agent Fix Task artifact schema version should be stable',
|
|
955
|
+
);
|
|
956
|
+
assert.ok(ralphTaskArtifact.tasks.length > 0, 'Agent Fix Task artifact should emit evidence-tied tasks');
|
|
957
|
+
assert.ok(
|
|
958
|
+
ralphTaskArtifact.tasks.every((task) =>
|
|
959
|
+
task.taskId
|
|
960
|
+
&& task.version === AGENT_FIX_TASK_SCHEMA_VERSION
|
|
961
|
+
&& (task.findingIds.length > 0 || task.testPlanCandidateIds.length > 0)
|
|
962
|
+
&& task.artifactRefs.includes('artifact-test-plan-for-ralph')
|
|
963
|
+
&& task.safeSummary
|
|
964
|
+
&& task.targetFiles.length > 0
|
|
965
|
+
&& task.instructions.problem
|
|
966
|
+
&& task.instructions.requiredBehavior
|
|
967
|
+
&& task.instructions.verification
|
|
968
|
+
&& task.instructions.rerun
|
|
969
|
+
&& task.verification.command
|
|
970
|
+
&& task.verification.expectedOutcome
|
|
971
|
+
&& task.risk.severity
|
|
972
|
+
&& ['ready', 'manual', 'blocked'].includes(task.status)
|
|
973
|
+
&& Object.prototype.hasOwnProperty.call(task, 'acceptedRiskRef')),
|
|
974
|
+
'Every Agent Fix Task should have required schema fields, evidence refs, and explicit verification',
|
|
975
|
+
);
|
|
976
|
+
assert.ok(
|
|
977
|
+
ralphTaskArtifact.tasks.some((task) =>
|
|
978
|
+
task.findingIds.length > 0
|
|
979
|
+
&& task.status === 'ready'
|
|
980
|
+
&& task.instructions.rerun.includes('Vibesecur')
|
|
981
|
+
&& task.verification.command === 'node --test'),
|
|
982
|
+
'Finding-backed runnable candidates should become ready Ralph tasks with rerun instructions',
|
|
983
|
+
);
|
|
984
|
+
assert.ok(
|
|
985
|
+
ralphTaskArtifact.tasks.some((task) =>
|
|
986
|
+
task.status === 'manual'
|
|
987
|
+
&& task.manualReason
|
|
988
|
+
&& task.instructions.acceptedRiskPath.includes('human approval')),
|
|
989
|
+
'Manual Test Plan candidates should stay manual with a human accepted-risk path',
|
|
990
|
+
);
|
|
991
|
+
const blockedRalphArtifact = buildAgentFixTaskArtifact({
|
|
992
|
+
securityTestPlan: unknownCommandPlan,
|
|
993
|
+
deterministicScan: {
|
|
994
|
+
...deterministicArtifact,
|
|
995
|
+
findings: [],
|
|
996
|
+
summary: { ...deterministicArtifact.summary, totalIssues: 0, bySeverity: {} },
|
|
997
|
+
},
|
|
998
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
999
|
+
sourceRefs: {
|
|
1000
|
+
securityTestPlan: 'artifact-blocked-test-plan',
|
|
1001
|
+
deterministicScan: 'artifact-blocked-rules-scan',
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
assert.ok(
|
|
1005
|
+
blockedRalphArtifact.tasks.some((task) =>
|
|
1006
|
+
task.status === 'blocked'
|
|
1007
|
+
&& /No local test command/.test(task.blockedReason)
|
|
1008
|
+
&& task.verification.command === null),
|
|
1009
|
+
'Blocked Test Plan candidates should become blocked Ralph tasks without fake verification commands',
|
|
1010
|
+
);
|
|
1011
|
+
const manualFallbackArtifact = buildAgentFixTaskArtifact({
|
|
1012
|
+
securityTestPlan: {
|
|
1013
|
+
...testPlanArtifact,
|
|
1014
|
+
testCases: [{
|
|
1015
|
+
...testPlanArtifact.testCases[0],
|
|
1016
|
+
id: 'test-unknown-category',
|
|
1017
|
+
category: 'unmapped_future_category',
|
|
1018
|
+
status: 'planned',
|
|
1019
|
+
sourceFindingIds: [],
|
|
1020
|
+
suggestedCommand: null,
|
|
1021
|
+
commandHint: null,
|
|
1022
|
+
targetSurface: { type: 'finding', files: ['src/unknown.js'], routes: [] },
|
|
1023
|
+
dedupeKey: 'unknown-category-manual-fallback',
|
|
1024
|
+
}],
|
|
1025
|
+
},
|
|
1026
|
+
deterministicScan: {
|
|
1027
|
+
...deterministicArtifact,
|
|
1028
|
+
findings: [],
|
|
1029
|
+
summary: { ...deterministicArtifact.summary, totalIssues: 0, bySeverity: {} },
|
|
1030
|
+
},
|
|
1031
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1032
|
+
});
|
|
1033
|
+
assert.equal(manualFallbackArtifact.tasks[0].status, 'manual', 'Unknown categories should fall back to manual review');
|
|
1034
|
+
assert.match(
|
|
1035
|
+
manualFallbackArtifact.tasks[0].instructions.problem,
|
|
1036
|
+
/manual review/i,
|
|
1037
|
+
'Unknown category fallback should use the manual review template',
|
|
1038
|
+
);
|
|
1039
|
+
assert.doesNotMatch(
|
|
1040
|
+
JSON.stringify(ralphTaskArtifact),
|
|
1041
|
+
/CorrectHorseBatteryStaple|phase3-jwt-secret|postgres:\/\/user:superpass|PythonSecretValue|python_api_key_secret|sk_live_PHASE3_SELFTEST_SECRET|jwt\.sign|const leaked|function\s+\w+\(/i,
|
|
1042
|
+
'Agent Fix Task artifact must not retain raw source or secret values',
|
|
1043
|
+
);
|
|
1044
|
+
assert.throws(
|
|
1045
|
+
() => validateAgentFixTaskArtifact({
|
|
1046
|
+
...ralphTaskArtifact,
|
|
1047
|
+
tasks: [{ ...ralphTaskArtifact.tasks[0], artifactRefs: [] }],
|
|
1048
|
+
}),
|
|
1049
|
+
/artifactRefs/i,
|
|
1050
|
+
'Agent Fix Task validator should reject tasks without artifact refs',
|
|
1051
|
+
);
|
|
1052
|
+
assert.throws(
|
|
1053
|
+
() => validateAgentFixTaskArtifact({
|
|
1054
|
+
...ralphTaskArtifact,
|
|
1055
|
+
tasks: [{
|
|
1056
|
+
...ralphTaskArtifact.tasks[0],
|
|
1057
|
+
instructions: {
|
|
1058
|
+
...ralphTaskArtifact.tasks[0].instructions,
|
|
1059
|
+
problem: 'const secret = "sk_live_PHASE5_PROVIDER_SECRET";',
|
|
1060
|
+
},
|
|
1061
|
+
}],
|
|
1062
|
+
}),
|
|
1063
|
+
/source-safe|secret/i,
|
|
1064
|
+
'Agent Fix Task validator should reject raw source or secret-like instruction text',
|
|
1065
|
+
);
|
|
1066
|
+
assert.equal(
|
|
1067
|
+
hashAgentFixTaskArtifact(ralphTaskArtifact),
|
|
1068
|
+
hashAgentFixTaskArtifact(validateAgentFixTaskArtifact(ralphTaskArtifact)),
|
|
1069
|
+
'Agent Fix Task artifact hash should be stable across validation',
|
|
1070
|
+
);
|
|
1071
|
+
const findingBackedCandidate = testPlanArtifact.testCases.find((testCase) =>
|
|
1072
|
+
testCase.sourceFindingIds.length > 0);
|
|
1073
|
+
assert.throws(
|
|
1074
|
+
() => buildAgentFixTaskArtifact({
|
|
1075
|
+
securityTestPlan: {
|
|
1076
|
+
...testPlanArtifact,
|
|
1077
|
+
testCases: [{
|
|
1078
|
+
...findingBackedCandidate,
|
|
1079
|
+
id: 'test-dangling-finding',
|
|
1080
|
+
sourceFindingIds: ['finding-does-not-exist'],
|
|
1081
|
+
dedupeKey: 'dangling-finding-reference',
|
|
1082
|
+
}],
|
|
1083
|
+
},
|
|
1084
|
+
deterministicScan: deterministicArtifact,
|
|
1085
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1086
|
+
}),
|
|
1087
|
+
/finding-does-not-exist|sourceFindingIds/i,
|
|
1088
|
+
'Agent Fix Task generation should reject dangling deterministic finding refs',
|
|
1089
|
+
);
|
|
1090
|
+
const ralphDefinition = ralphTaskStepper();
|
|
1091
|
+
const ralphResult = await ralphDefinition.run({
|
|
1092
|
+
runId: 'deep_scan_ralph_selftest',
|
|
1093
|
+
projectId: 'fixture-scan',
|
|
1094
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1095
|
+
state: {
|
|
1096
|
+
artifacts: {
|
|
1097
|
+
'tests.plan': testPlanResult.artifacts[0],
|
|
1098
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
1102
|
+
assert.equal(ralphResult.stepperId, AGENT_FIX_TASK_STEPPER_ID, 'Ralph task stepper id should be stable');
|
|
1103
|
+
assert.equal(ralphResult.status, 'passed', 'Ralph task stepper should pass with Test Plan and scan inputs');
|
|
1104
|
+
assert.ok(
|
|
1105
|
+
ralphResult.artifacts.some((ref) => ref.type === 'agent_fix_tasks'),
|
|
1106
|
+
'Ralph task stepper should emit an agent_fix_tasks artifact ref',
|
|
1107
|
+
);
|
|
1108
|
+
const ralphArtifactRef = ralphResult.artifacts.find((ref) => ref.type === 'agent_fix_tasks');
|
|
1109
|
+
const ralphArtifactText = await fs.readFile(path.join(testPlanProjectRoot, ralphArtifactRef.uri), 'utf8');
|
|
1110
|
+
assert.match(ralphArtifactText, /agent_fix_task\.v1/, 'Written Agent Fix Task artifact should include schema version');
|
|
1111
|
+
assert.doesNotMatch(
|
|
1112
|
+
ralphArtifactText,
|
|
1113
|
+
/CorrectHorseBatteryStaple|phase3-jwt-secret|postgres:\/\/user:superpass|PythonSecretValue|python_api_key_secret|sk_live_PHASE3_SELFTEST_SECRET|jwt\.sign|const leaked/i,
|
|
1114
|
+
'Written Agent Fix Task artifact must remain source-safe',
|
|
1115
|
+
);
|
|
1116
|
+
await assert.rejects(
|
|
1117
|
+
() => writeAgentFixTaskArtifact({ rootPath: projectRoot, runId: '.', artifact: ralphTaskArtifact }),
|
|
1118
|
+
/runId/i,
|
|
1119
|
+
'Agent Fix Task artifact writes should reject dot-only run ids',
|
|
1120
|
+
);
|
|
1121
|
+
await assert.rejects(
|
|
1122
|
+
() => ralphDefinition.run({
|
|
1123
|
+
runId: 'deep_scan_ralph_bad_hash_selftest',
|
|
1124
|
+
projectId: 'fixture-scan',
|
|
1125
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1126
|
+
state: {
|
|
1127
|
+
artifacts: {
|
|
1128
|
+
'tests.plan': { ...testPlanResult.artifacts[0], hash: '0'.repeat(64) },
|
|
1129
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
}),
|
|
1133
|
+
/hash/i,
|
|
1134
|
+
'Ralph task stepper should reject Security Test Plan artifact hash mismatches',
|
|
1135
|
+
);
|
|
1136
|
+
await assert.rejects(
|
|
1137
|
+
() => ralphDefinition.run({
|
|
1138
|
+
runId: 'deep_scan_ralph_bad_scan_hash_selftest',
|
|
1139
|
+
projectId: 'fixture-scan',
|
|
1140
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1141
|
+
state: {
|
|
1142
|
+
artifacts: {
|
|
1143
|
+
'tests.plan': testPlanResult.artifacts[0],
|
|
1144
|
+
'rules.scan': { ...deterministicForPlanResult.artifacts[0], hash: '0'.repeat(64) },
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
}),
|
|
1148
|
+
/hash/i,
|
|
1149
|
+
'Ralph task stepper should reject deterministic scan artifact hash mismatches',
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
const baselineScan = {
|
|
1153
|
+
...deterministicArtifact,
|
|
1154
|
+
findings: deterministicArtifact.findings
|
|
1155
|
+
.map((finding) => (
|
|
1156
|
+
finding.id === deterministicArtifact.findings[1].id
|
|
1157
|
+
? { ...finding, severity: 'medium' }
|
|
1158
|
+
: finding
|
|
1159
|
+
))
|
|
1160
|
+
.concat([{
|
|
1161
|
+
...deterministicArtifact.findings[0],
|
|
1162
|
+
id: 'finding-selftest-baseline-only',
|
|
1163
|
+
fingerprint: 'f'.repeat(64),
|
|
1164
|
+
ruleId: 'S999',
|
|
1165
|
+
title: 'Baseline-only simulated finding',
|
|
1166
|
+
}, {
|
|
1167
|
+
...deterministicArtifact.findings[0],
|
|
1168
|
+
id: null,
|
|
1169
|
+
fingerprint: null,
|
|
1170
|
+
ruleId: 'S998',
|
|
1171
|
+
title: 'Low-confidence baseline-only simulated finding',
|
|
1172
|
+
}]),
|
|
1173
|
+
};
|
|
1174
|
+
const acceptedRiskTaskArtifact = validateAgentFixTaskArtifact({
|
|
1175
|
+
...ralphTaskArtifact,
|
|
1176
|
+
tasks: ralphTaskArtifact.tasks.map((task) => (
|
|
1177
|
+
task.findingIds.includes(deterministicArtifact.findings[2].id)
|
|
1178
|
+
? { ...task, acceptedRiskRef: 'accepted-risk-selftest-1' }
|
|
1179
|
+
: task
|
|
1180
|
+
)),
|
|
1181
|
+
});
|
|
1182
|
+
const comparisonArtifact = buildRalphComparisonArtifact({
|
|
1183
|
+
previousDeterministicScan: baselineScan,
|
|
1184
|
+
currentDeterministicScan: deterministicArtifact,
|
|
1185
|
+
agentFixTasks: acceptedRiskTaskArtifact,
|
|
1186
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1187
|
+
sourceRefs: {
|
|
1188
|
+
previousDeterministicScan: 'artifact-rules-scan-baseline',
|
|
1189
|
+
currentDeterministicScan: 'artifact-rules-scan-current',
|
|
1190
|
+
agentFixTasks: 'artifact-test-plan-for-ralph',
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
assert.equal(
|
|
1194
|
+
comparisonArtifact.schemaVersion,
|
|
1195
|
+
RALPH_COMPARISON_SCHEMA_VERSION,
|
|
1196
|
+
'Ralph comparison schema version should be stable',
|
|
1197
|
+
);
|
|
1198
|
+
assert.ok(
|
|
1199
|
+
comparisonArtifact.comparisons.some((row) => row.status === 'resolved'),
|
|
1200
|
+
'Ralph comparison should classify missing rerun findings as resolved',
|
|
1201
|
+
);
|
|
1202
|
+
assert.ok(
|
|
1203
|
+
comparisonArtifact.comparisons.some((row) => row.status === 'worsened'),
|
|
1204
|
+
'Ralph comparison should classify severity increases as worsened',
|
|
1205
|
+
);
|
|
1206
|
+
assert.ok(
|
|
1207
|
+
comparisonArtifact.comparisons.some((row) => row.status === 'acceptedRisk' && row.acceptedRiskRef),
|
|
1208
|
+
'Ralph comparison should preserve accepted-risk references for unresolved findings',
|
|
1209
|
+
);
|
|
1210
|
+
assert.ok(
|
|
1211
|
+
comparisonArtifact.comparisons.some((row) => row.status === 'manualReview' && row.manualReviewReason),
|
|
1212
|
+
'Ralph comparison should classify low-confidence identity matches as manual review',
|
|
1213
|
+
);
|
|
1214
|
+
assert.equal(
|
|
1215
|
+
hashRalphComparisonArtifact(comparisonArtifact),
|
|
1216
|
+
hashRalphComparisonArtifact(validateRalphComparisonArtifact(comparisonArtifact)),
|
|
1217
|
+
'Ralph comparison artifact hash should be stable across validation',
|
|
1218
|
+
);
|
|
1219
|
+
const baselineWrite = await writeDeterministicScanArtifact({
|
|
1220
|
+
rootPath: testPlanProjectRoot,
|
|
1221
|
+
runId: 'deep_scan_ralph_baseline_selftest',
|
|
1222
|
+
artifact: baselineScan,
|
|
1223
|
+
});
|
|
1224
|
+
const baselineRef = createArtifactRef({
|
|
1225
|
+
id: 'artifact-rules-scan-baseline-selftest',
|
|
1226
|
+
type: 'deterministic_scan',
|
|
1227
|
+
storage: 'local',
|
|
1228
|
+
uri: baselineWrite.uri,
|
|
1229
|
+
hash: baselineWrite.hash,
|
|
1230
|
+
});
|
|
1231
|
+
const comparisonDefinition = ralphComparisonStepper();
|
|
1232
|
+
const comparisonSkipped = await comparisonDefinition.run({
|
|
1233
|
+
runId: 'deep_scan_ralph_compare_skipped_selftest',
|
|
1234
|
+
projectId: 'fixture-scan',
|
|
1235
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1236
|
+
state: {
|
|
1237
|
+
artifacts: {
|
|
1238
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
1239
|
+
'ralph.tasks': ralphResult.artifacts[0],
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
});
|
|
1243
|
+
assert.equal(
|
|
1244
|
+
comparisonSkipped.status,
|
|
1245
|
+
'skipped',
|
|
1246
|
+
'Ralph comparison stepper should skip when previous deterministic scan ref is not provided',
|
|
1247
|
+
);
|
|
1248
|
+
const comparisonResult = await comparisonDefinition.run({
|
|
1249
|
+
runId: 'deep_scan_ralph_compare_selftest',
|
|
1250
|
+
projectId: 'fixture-scan',
|
|
1251
|
+
config: {
|
|
1252
|
+
rootPath: testPlanProjectRoot,
|
|
1253
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1254
|
+
previousDeterministicScanRef: baselineRef,
|
|
1255
|
+
},
|
|
1256
|
+
state: {
|
|
1257
|
+
artifacts: {
|
|
1258
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
1259
|
+
'ralph.tasks': ralphResult.artifacts[0],
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
});
|
|
1263
|
+
assert.equal(
|
|
1264
|
+
comparisonResult.stepperId,
|
|
1265
|
+
RALPH_COMPARISON_STEPPER_ID,
|
|
1266
|
+
'Ralph comparison stepper id should be stable',
|
|
1267
|
+
);
|
|
1268
|
+
assert.equal(
|
|
1269
|
+
comparisonResult.status,
|
|
1270
|
+
'passed',
|
|
1271
|
+
'Ralph comparison should pass with baseline and current deterministic scan artifacts',
|
|
1272
|
+
);
|
|
1273
|
+
assert.ok(
|
|
1274
|
+
comparisonResult.artifacts.some((ref) => ref.type === 'ralph_comparison'),
|
|
1275
|
+
'Ralph comparison stepper should emit a ralph_comparison artifact ref',
|
|
1276
|
+
);
|
|
1277
|
+
const comparisonArtifactRef = comparisonResult.artifacts.find((ref) => ref.type === 'ralph_comparison');
|
|
1278
|
+
const comparisonArtifactText = await fs.readFile(path.join(testPlanProjectRoot, comparisonArtifactRef.uri), 'utf8');
|
|
1279
|
+
assert.match(
|
|
1280
|
+
comparisonArtifactText,
|
|
1281
|
+
/ralph_comparison\.v1/,
|
|
1282
|
+
'Written Ralph comparison artifact should include schema version',
|
|
1283
|
+
);
|
|
1284
|
+
assert.match(
|
|
1285
|
+
comparisonArtifactText,
|
|
1286
|
+
/"acceptedRisk"/,
|
|
1287
|
+
'Written Ralph comparison artifact should include accepted-risk classifications when present',
|
|
1288
|
+
);
|
|
1289
|
+
assert.match(
|
|
1290
|
+
comparisonArtifactText,
|
|
1291
|
+
/"manualReview"/,
|
|
1292
|
+
'Written Ralph comparison artifact should include low-confidence manual-review classifications when present',
|
|
1293
|
+
);
|
|
1294
|
+
await assert.rejects(
|
|
1295
|
+
() => writeRalphComparisonArtifact({ rootPath: projectRoot, runId: '.', artifact: comparisonArtifact }),
|
|
1296
|
+
/runId/i,
|
|
1297
|
+
'Ralph comparison artifact writes should reject dot-only run ids',
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
const repeatedComparisonRow = comparisonArtifact.comparisons.find(
|
|
1301
|
+
(row) => (row.status === 'worsened' || row.status === 'unchanged')
|
|
1302
|
+
&& ['high', 'critical'].includes(String(row.current?.severity || row.previous?.severity).toLowerCase()),
|
|
1303
|
+
);
|
|
1304
|
+
const failureStateArtifact = buildRalphFailureStateArtifact({
|
|
1305
|
+
comparisonArtifact,
|
|
1306
|
+
previousFailureState: {
|
|
1307
|
+
failures: repeatedComparisonRow ? [{
|
|
1308
|
+
findingKey: repeatedComparisonRow.findingKey,
|
|
1309
|
+
findingId: repeatedComparisonRow.current?.id || repeatedComparisonRow.previous?.id || 'finding-repeat-selftest',
|
|
1310
|
+
taskId: 'task-repeat-selftest',
|
|
1311
|
+
severity: repeatedComparisonRow.current?.severity || repeatedComparisonRow.previous?.severity || 'high',
|
|
1312
|
+
attemptCount: 1,
|
|
1313
|
+
lastFailureReason: `${repeatedComparisonRow.status}_high`,
|
|
1314
|
+
lastSeenAt: '2026-05-27T00:00:00.000Z',
|
|
1315
|
+
escalationStatus: 'normal',
|
|
1316
|
+
comparisonStatus: repeatedComparisonRow.status,
|
|
1317
|
+
nextAction: 'Apply a narrower local fix, rerun relevant tests, and compare again through MCP Deep Scan.',
|
|
1318
|
+
}] : [],
|
|
1319
|
+
},
|
|
1320
|
+
agentFixTasks: acceptedRiskTaskArtifact,
|
|
1321
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1322
|
+
});
|
|
1323
|
+
assert.equal(
|
|
1324
|
+
failureStateArtifact.schemaVersion,
|
|
1325
|
+
RALPH_FAILURE_STATE_SCHEMA_VERSION,
|
|
1326
|
+
'Ralph failure state schema version should be stable',
|
|
1327
|
+
);
|
|
1328
|
+
const priorFailureStateForConfig = {
|
|
1329
|
+
failures: repeatedComparisonRow ? [{
|
|
1330
|
+
findingKey: repeatedComparisonRow.findingKey,
|
|
1331
|
+
findingId: repeatedComparisonRow.current?.id || repeatedComparisonRow.previous?.id || 'finding-repeat-selftest',
|
|
1332
|
+
taskId: 'task-repeat-selftest',
|
|
1333
|
+
severity: repeatedComparisonRow.current?.severity || repeatedComparisonRow.previous?.severity || 'high',
|
|
1334
|
+
attemptCount: 1,
|
|
1335
|
+
lastFailureReason: `${repeatedComparisonRow.status}_high`,
|
|
1336
|
+
lastSeenAt: '2026-05-27T00:00:00.000Z',
|
|
1337
|
+
escalationStatus: 'normal',
|
|
1338
|
+
comparisonStatus: repeatedComparisonRow.status,
|
|
1339
|
+
nextAction: 'Apply a narrower local fix, rerun relevant tests, and compare again through MCP Deep Scan.',
|
|
1340
|
+
}] : [],
|
|
1341
|
+
};
|
|
1342
|
+
if (repeatedComparisonRow) {
|
|
1343
|
+
const repeated = failureStateArtifact.failures.find((row) => row.findingKey === repeatedComparisonRow.findingKey);
|
|
1344
|
+
assert.ok(repeated, 'Ralph failure tracking should retain repeated high-risk findings');
|
|
1345
|
+
assert.equal(repeated.attemptCount, 2, 'Repeated high-risk findings should increment attempt counts');
|
|
1346
|
+
assert.equal(repeated.escalationStatus, 'escalated', 'Repeated high-risk findings should escalate at configured threshold');
|
|
1347
|
+
|
|
1348
|
+
// Config override: raising escalateAt should keep an attemptCount of 2 non-escalated.
|
|
1349
|
+
const raisedThresholdArtifact = buildRalphFailureStateArtifact({
|
|
1350
|
+
comparisonArtifact,
|
|
1351
|
+
previousFailureState: priorFailureStateForConfig,
|
|
1352
|
+
agentFixTasks: acceptedRiskTaskArtifact,
|
|
1353
|
+
retryConfig: { maxAttempts: 5, escalateAt: 3, trackSeverities: ['critical', 'high'] },
|
|
1354
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1355
|
+
});
|
|
1356
|
+
assert.equal(
|
|
1357
|
+
raisedThresholdArtifact.retryConfig.escalateAt,
|
|
1358
|
+
3,
|
|
1359
|
+
'Ralph failure state should honor configurable escalateAt from GraphProfile',
|
|
1360
|
+
);
|
|
1361
|
+
const raisedRow = raisedThresholdArtifact.failures.find((row) => row.findingKey === repeatedComparisonRow.findingKey);
|
|
1362
|
+
assert.ok(raisedRow, 'Repeated finding should still be tracked under a higher escalation threshold');
|
|
1363
|
+
assert.equal(raisedRow.attemptCount, 2, 'Attempt count should still increment under a higher threshold');
|
|
1364
|
+
assert.equal(
|
|
1365
|
+
raisedRow.escalationStatus,
|
|
1366
|
+
'normal',
|
|
1367
|
+
'Repeated finding below configured escalateAt should remain normal instead of escalating',
|
|
1368
|
+
);
|
|
1369
|
+
|
|
1370
|
+
// Config override: narrowing trackSeverities should stop tracking high-risk repeats.
|
|
1371
|
+
const criticalOnlyArtifact = buildRalphFailureStateArtifact({
|
|
1372
|
+
comparisonArtifact,
|
|
1373
|
+
previousFailureState: priorFailureStateForConfig,
|
|
1374
|
+
agentFixTasks: acceptedRiskTaskArtifact,
|
|
1375
|
+
retryConfig: { maxAttempts: 3, escalateAt: 2, trackSeverities: ['critical'] },
|
|
1376
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1377
|
+
});
|
|
1378
|
+
if (String(repeatedComparisonRow.current?.severity || repeatedComparisonRow.previous?.severity).toLowerCase() === 'high') {
|
|
1379
|
+
assert.ok(
|
|
1380
|
+
!criticalOnlyArtifact.failures.some((row) => row.findingKey === repeatedComparisonRow.findingKey),
|
|
1381
|
+
'Narrowing trackSeverities to critical should stop incrementing high-severity repeats',
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Config defaults: omitting retryConfig should fall back to documented defaults.
|
|
1387
|
+
const defaultConfigArtifact = buildRalphFailureStateArtifact({
|
|
1388
|
+
comparisonArtifact,
|
|
1389
|
+
previousFailureState: priorFailureStateForConfig,
|
|
1390
|
+
agentFixTasks: acceptedRiskTaskArtifact,
|
|
1391
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1392
|
+
});
|
|
1393
|
+
assert.deepEqual(
|
|
1394
|
+
defaultConfigArtifact.retryConfig,
|
|
1395
|
+
{ maxAttempts: DEFAULT_RALPH_LOOP_RETRY.maxAttempts, escalateAt: DEFAULT_RALPH_LOOP_RETRY.escalateAt, trackSeverities: [...DEFAULT_RALPH_LOOP_RETRY.trackSeverities] },
|
|
1396
|
+
'Ralph failure state should apply documented retry defaults when GraphProfile omits ralphLoop.retry',
|
|
1397
|
+
);
|
|
1398
|
+
assert.throws(
|
|
1399
|
+
() => buildRalphFailureStateArtifact({
|
|
1400
|
+
comparisonArtifact,
|
|
1401
|
+
previousFailureState: priorFailureStateForConfig,
|
|
1402
|
+
retryConfig: { maxAttempts: 2, escalateAt: 5, trackSeverities: ['high'] },
|
|
1403
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1404
|
+
}),
|
|
1405
|
+
/escalateAt/i,
|
|
1406
|
+
'Ralph failure state should reject escalateAt greater than maxAttempts',
|
|
1407
|
+
);
|
|
1408
|
+
assert.ok(
|
|
1409
|
+
!failureStateArtifact.failures.some((row) => row.comparisonStatus === 'resolved'),
|
|
1410
|
+
'Resolved findings should be removed from active repeated failure tracking',
|
|
1411
|
+
);
|
|
1412
|
+
assert.ok(
|
|
1413
|
+
failureStateArtifact.failures.some((row) => row.escalationStatus === 'manualReview'),
|
|
1414
|
+
'Manual review comparison rows should remain visible without silent auto-resolve',
|
|
1415
|
+
);
|
|
1416
|
+
assert.equal(
|
|
1417
|
+
hashRalphFailureStateArtifact(failureStateArtifact),
|
|
1418
|
+
hashRalphFailureStateArtifact(validateRalphFailureStateArtifact(failureStateArtifact)),
|
|
1419
|
+
'Ralph failure state artifact hash should be stable across validation',
|
|
1420
|
+
);
|
|
1421
|
+
const trackDefinition = ralphFailureTrackStepper();
|
|
1422
|
+
const trackSkipped = await trackDefinition.run({
|
|
1423
|
+
runId: 'deep_scan_ralph_track_skipped_selftest',
|
|
1424
|
+
projectId: 'fixture-scan',
|
|
1425
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1426
|
+
state: {
|
|
1427
|
+
stepResults: { 'ralph.compare': comparisonSkipped },
|
|
1428
|
+
artifacts: {
|
|
1429
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
1430
|
+
'ralph.tasks': ralphResult.artifacts[0],
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
tools: { rootPath: testPlanProjectRoot },
|
|
1434
|
+
});
|
|
1435
|
+
assert.equal(
|
|
1436
|
+
trackSkipped.status,
|
|
1437
|
+
'skipped',
|
|
1438
|
+
'Ralph failure tracking should skip when comparison was skipped',
|
|
1439
|
+
);
|
|
1440
|
+
const trackResult = await trackDefinition.run({
|
|
1441
|
+
runId: 'deep_scan_ralph_track_selftest',
|
|
1442
|
+
projectId: 'fixture-scan',
|
|
1443
|
+
config: { rootPath: testPlanProjectRoot, generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1444
|
+
state: {
|
|
1445
|
+
stepResults: { 'ralph.compare': comparisonResult },
|
|
1446
|
+
artifacts: {
|
|
1447
|
+
'rules.scan': deterministicForPlanResult.artifacts[0],
|
|
1448
|
+
'ralph.tasks': ralphResult.artifacts[0],
|
|
1449
|
+
'ralph.compare': comparisonResult.artifacts[0],
|
|
1450
|
+
},
|
|
1451
|
+
},
|
|
1452
|
+
tools: { rootPath: testPlanProjectRoot },
|
|
1453
|
+
});
|
|
1454
|
+
assert.equal(
|
|
1455
|
+
trackResult.stepperId,
|
|
1456
|
+
RALPH_FAILURE_STATE_STEPPER_ID,
|
|
1457
|
+
'Ralph failure tracking stepper id should be stable',
|
|
1458
|
+
);
|
|
1459
|
+
assert.equal(
|
|
1460
|
+
trackResult.status,
|
|
1461
|
+
'passed',
|
|
1462
|
+
'Ralph failure tracking should pass when comparison artifact is available',
|
|
1463
|
+
);
|
|
1464
|
+
assert.ok(
|
|
1465
|
+
trackResult.artifacts.some((ref) => ref.type === 'ralph_failure_state'),
|
|
1466
|
+
'Ralph failure tracking stepper should emit a ralph_failure_state artifact ref',
|
|
1467
|
+
);
|
|
1468
|
+
const failureStateRef = trackResult.artifacts.find((ref) => ref.type === 'ralph_failure_state');
|
|
1469
|
+
const failureStateText = await fs.readFile(path.join(testPlanProjectRoot, failureStateRef.uri), 'utf8');
|
|
1470
|
+
assert.match(
|
|
1471
|
+
failureStateText,
|
|
1472
|
+
/ralph_failure_state\.v1/,
|
|
1473
|
+
'Written Ralph failure state artifact should include schema version',
|
|
1474
|
+
);
|
|
1475
|
+
await assert.rejects(
|
|
1476
|
+
() => writeRalphFailureStateArtifact({ rootPath: projectRoot, runId: '.', artifact: failureStateArtifact }),
|
|
1477
|
+
/runId/i,
|
|
1478
|
+
'Ralph failure state artifact writes should reject dot-only run ids',
|
|
1479
|
+
);
|
|
1480
|
+
|
|
1481
|
+
// ── VIB-85: accepted-risk workflow ──────────────────────────────────
|
|
1482
|
+
const acceptedFindingId = deterministicArtifact.findings[0].id;
|
|
1483
|
+
const taskFindingId = deterministicArtifact.findings[2].id;
|
|
1484
|
+
// High-risk acceptance requires explicit reason, reviewer, and expiry.
|
|
1485
|
+
assert.throws(
|
|
1486
|
+
() => validateAcceptedRiskRecord({ riskId: 'risk_hr_1', findingId: acceptedFindingId, severity: 'high' }),
|
|
1487
|
+
/reason/i,
|
|
1488
|
+
'High-risk accepted risk must require an explicit reason',
|
|
1489
|
+
);
|
|
1490
|
+
assert.throws(
|
|
1491
|
+
() => validateAcceptedRiskRecord({ riskId: 'risk_hr_2', findingId: acceptedFindingId, severity: 'high', reason: 'mitigated' }),
|
|
1492
|
+
/reviewer/i,
|
|
1493
|
+
'High-risk accepted risk must require reviewer metadata',
|
|
1494
|
+
);
|
|
1495
|
+
assert.throws(
|
|
1496
|
+
() => validateAcceptedRiskRecord({ riskId: 'risk_hr_3', findingId: acceptedFindingId, severity: 'critical', reason: 'mitigated', reviewer: 'sec@selftest' }),
|
|
1497
|
+
/expiresAt/i,
|
|
1498
|
+
'High-risk accepted risk must require an expiry/review date',
|
|
1499
|
+
);
|
|
1500
|
+
assert.throws(
|
|
1501
|
+
() => validateAcceptedRiskRecord({ riskId: 'risk_no_target', severity: 'low' }),
|
|
1502
|
+
/findingKey or findingId/i,
|
|
1503
|
+
'Accepted risk must attach to a findingKey or findingId',
|
|
1504
|
+
);
|
|
1505
|
+
assert.throws(
|
|
1506
|
+
() => validateAcceptedRiskRecord({
|
|
1507
|
+
riskId: 'risk_bad_dates', findingId: acceptedFindingId, severity: 'high', reason: 'x', reviewer: 'y',
|
|
1508
|
+
acceptedAt: '2026-05-10T00:00:00.000Z', expiresAt: '2026-05-01T00:00:00.000Z',
|
|
1509
|
+
}),
|
|
1510
|
+
/expiresAt must be after acceptedAt/i,
|
|
1511
|
+
'Accepted risk expiry must be after acceptance time',
|
|
1512
|
+
);
|
|
1513
|
+
assert.throws(
|
|
1514
|
+
() => validateAcceptedRiskRecord({ riskId: 'risk_hidden', findingId: acceptedFindingId, severity: 'low', reportVisibility: 'hidden' }),
|
|
1515
|
+
/reportVisibility/i,
|
|
1516
|
+
'Accepted risk can never be hidden from reports',
|
|
1517
|
+
);
|
|
1518
|
+
const lowRiskRecord = validateAcceptedRiskRecord({ riskId: 'risk_low_1', findingId: acceptedFindingId, severity: 'low' });
|
|
1519
|
+
assert.equal(lowRiskRecord.reportVisibility, 'visible', 'Accepted risk defaults to visible report visibility');
|
|
1520
|
+
assert.throws(
|
|
1521
|
+
() => validateAcceptedRiskRegister({
|
|
1522
|
+
schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
|
|
1523
|
+
records: [
|
|
1524
|
+
{ riskId: 'risk_dupe', findingId: acceptedFindingId, severity: 'low' },
|
|
1525
|
+
{ riskId: 'risk_dupe', findingId: taskFindingId, severity: 'low' },
|
|
1526
|
+
],
|
|
1527
|
+
}),
|
|
1528
|
+
/duplicate riskId/i,
|
|
1529
|
+
'Accepted-risk register must reject duplicate riskIds',
|
|
1530
|
+
);
|
|
1531
|
+
|
|
1532
|
+
// Active accepted risk marks a finding accepted with the register riskId.
|
|
1533
|
+
const activeRegister = {
|
|
1534
|
+
schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
|
|
1535
|
+
records: [{
|
|
1536
|
+
riskId: 'risk_active_selftest',
|
|
1537
|
+
findingId: acceptedFindingId,
|
|
1538
|
+
severity: 'high',
|
|
1539
|
+
reason: 'Endpoint disabled behind a feature flag before launch.',
|
|
1540
|
+
reviewer: 'security@selftest',
|
|
1541
|
+
acceptedAt: '2026-05-01T00:00:00.000Z',
|
|
1542
|
+
expiresAt: '2026-12-31T00:00:00.000Z',
|
|
1543
|
+
}],
|
|
1544
|
+
};
|
|
1545
|
+
const comparisonAcceptedActive = buildRalphComparisonArtifact({
|
|
1546
|
+
previousDeterministicScan: baselineScan,
|
|
1547
|
+
currentDeterministicScan: deterministicArtifact,
|
|
1548
|
+
acceptedRiskRegister: activeRegister,
|
|
1549
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1550
|
+
});
|
|
1551
|
+
const activeRow = comparisonAcceptedActive.comparisons.find((row) => row.acceptedRiskRef === 'risk_active_selftest');
|
|
1552
|
+
assert.ok(activeRow && activeRow.status === 'acceptedRisk', 'Active accepted-risk register record should mark the finding acceptedRisk with the register riskId');
|
|
1553
|
+
|
|
1554
|
+
// Expired accepted risk reverts the finding to unresolved.
|
|
1555
|
+
const expiredRegister = {
|
|
1556
|
+
schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
|
|
1557
|
+
records: [{
|
|
1558
|
+
riskId: 'risk_expired_selftest',
|
|
1559
|
+
findingId: acceptedFindingId,
|
|
1560
|
+
severity: 'high',
|
|
1561
|
+
reason: 'Temporary acceptance during migration.',
|
|
1562
|
+
reviewer: 'security@selftest',
|
|
1563
|
+
acceptedAt: '2026-04-01T00:00:00.000Z',
|
|
1564
|
+
expiresAt: '2026-05-01T00:00:00.000Z',
|
|
1565
|
+
}],
|
|
1566
|
+
};
|
|
1567
|
+
const comparisonAcceptedExpired = buildRalphComparisonArtifact({
|
|
1568
|
+
previousDeterministicScan: baselineScan,
|
|
1569
|
+
currentDeterministicScan: deterministicArtifact,
|
|
1570
|
+
acceptedRiskRegister: expiredRegister,
|
|
1571
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1572
|
+
});
|
|
1573
|
+
const expiredRow = comparisonAcceptedExpired.comparisons.find((row) => row.current?.id === acceptedFindingId);
|
|
1574
|
+
assert.ok(expiredRow, 'Comparison should still track a finding whose accepted risk expired');
|
|
1575
|
+
assert.notEqual(expiredRow.status, 'acceptedRisk', 'Expired accepted risk should revert the finding to unresolved');
|
|
1576
|
+
assert.equal(expiredRow.acceptedRiskRef, null, 'Expired accepted risk should not leave an accepted-risk reference');
|
|
1577
|
+
|
|
1578
|
+
// An expired register record suppresses the legacy AgentFixTask acceptedRiskRef.
|
|
1579
|
+
const suppressRegister = {
|
|
1580
|
+
schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
|
|
1581
|
+
records: [{
|
|
1582
|
+
riskId: 'risk_suppress_selftest',
|
|
1583
|
+
findingId: taskFindingId,
|
|
1584
|
+
severity: 'high',
|
|
1585
|
+
reason: 'Was accepted, now expired and must be re-reviewed.',
|
|
1586
|
+
reviewer: 'security@selftest',
|
|
1587
|
+
acceptedAt: '2026-04-01T00:00:00.000Z',
|
|
1588
|
+
expiresAt: '2026-05-01T00:00:00.000Z',
|
|
1589
|
+
}],
|
|
1590
|
+
};
|
|
1591
|
+
const comparisonSuppressed = buildRalphComparisonArtifact({
|
|
1592
|
+
previousDeterministicScan: baselineScan,
|
|
1593
|
+
currentDeterministicScan: deterministicArtifact,
|
|
1594
|
+
agentFixTasks: acceptedRiskTaskArtifact,
|
|
1595
|
+
acceptedRiskRegister: suppressRegister,
|
|
1596
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1597
|
+
});
|
|
1598
|
+
const suppressedRow = comparisonSuppressed.comparisons.find((row) => row.current?.id === taskFindingId || row.previous?.id === taskFindingId);
|
|
1599
|
+
assert.ok(suppressedRow, 'Comparison should track the finding referenced by the legacy task acceptedRiskRef');
|
|
1600
|
+
assert.notEqual(suppressedRow.status, 'acceptedRisk', 'Expired register record should override and suppress the legacy task acceptedRiskRef');
|
|
1601
|
+
|
|
1602
|
+
// Accepted-risk state artifact shows active and expired separately, and flags reverted findings.
|
|
1603
|
+
const mixedRegister = {
|
|
1604
|
+
schemaVersion: ACCEPTED_RISK_REGISTER_SCHEMA_VERSION,
|
|
1605
|
+
records: [activeRegister.records[0], expiredRegister.records[0]],
|
|
1606
|
+
};
|
|
1607
|
+
const acceptedRiskState = buildAcceptedRiskStateArtifact({
|
|
1608
|
+
register: mixedRegister,
|
|
1609
|
+
comparisonArtifact: comparisonAcceptedExpired,
|
|
1610
|
+
generatedAt: '2026-05-28T00:00:00.000Z',
|
|
1611
|
+
});
|
|
1612
|
+
assert.equal(acceptedRiskState.schemaVersion, ACCEPTED_RISK_STATE_SCHEMA_VERSION, 'Accepted-risk state schema version should be stable');
|
|
1613
|
+
assert.equal(acceptedRiskState.summary.activeCount, 1, 'Accepted-risk state should count one active record');
|
|
1614
|
+
assert.equal(acceptedRiskState.summary.expiredCount, 1, 'Accepted-risk state should count one expired record');
|
|
1615
|
+
assert.equal(acceptedRiskState.summary.expiredRevertedCount, 1, 'Accepted-risk state should flag the expired record whose finding reverted to unresolved');
|
|
1616
|
+
assert.ok(acceptedRiskState.reportHandoff.activeAcceptedRisk.length === 1, 'Report handoff should list active accepted risk separately');
|
|
1617
|
+
assert.ok(acceptedRiskState.reportHandoff.expiredAcceptedRisk.length === 1, 'Report handoff should list expired accepted risk separately');
|
|
1618
|
+
assert.equal(
|
|
1619
|
+
hashAcceptedRiskStateArtifact(acceptedRiskState),
|
|
1620
|
+
hashAcceptedRiskStateArtifact(validateAcceptedRiskStateArtifact(acceptedRiskState)),
|
|
1621
|
+
'Accepted-risk state artifact hash should be stable across validation',
|
|
1622
|
+
);
|
|
1623
|
+
|
|
1624
|
+
// Register append entrypoint + read-back; high-risk validation enforced on append.
|
|
1625
|
+
const acceptDir = path.join(tmpBase, 'accept-risk-register');
|
|
1626
|
+
await fs.mkdir(acceptDir, { recursive: true });
|
|
1627
|
+
const appended = await appendAcceptedRiskRecord({
|
|
1628
|
+
rootPath: acceptDir,
|
|
1629
|
+
record: {
|
|
1630
|
+
findingId: acceptedFindingId,
|
|
1631
|
+
severity: 'high',
|
|
1632
|
+
reason: 'Mitigated by WAF rule pending code fix.',
|
|
1633
|
+
reviewer: 'security@selftest',
|
|
1634
|
+
expiresAt: '2027-01-01T00:00:00.000Z',
|
|
1635
|
+
},
|
|
1636
|
+
});
|
|
1637
|
+
assert.match(appended.record.riskId, /^risk_/, 'appendAcceptedRiskRecord should generate a riskId when omitted');
|
|
1638
|
+
const readBack = await readAcceptedRiskRegister({ rootPath: acceptDir });
|
|
1639
|
+
assert.equal(readBack.exists, true, 'Accepted-risk register should persist locally');
|
|
1640
|
+
assert.equal(readBack.register.records.length, 1, 'Accepted-risk register should contain the appended record');
|
|
1641
|
+
await assert.rejects(
|
|
1642
|
+
() => appendAcceptedRiskRecord({ rootPath: acceptDir, record: { findingId: taskFindingId, severity: 'critical' } }),
|
|
1643
|
+
/reason/i,
|
|
1644
|
+
'appendAcceptedRiskRecord should reject high-risk acceptance without reason/reviewer/expiry',
|
|
1645
|
+
);
|
|
1646
|
+
|
|
1647
|
+
// Stepper skips without a register and passes (writing a source-safe artifact) with one.
|
|
1648
|
+
const acceptStepper = ralphAcceptStepper();
|
|
1649
|
+
const acceptSkip = await acceptStepper.run({
|
|
1650
|
+
runId: 'deep_scan_accept_skip_selftest',
|
|
1651
|
+
config: { generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1652
|
+
state: {},
|
|
1653
|
+
tools: { rootPath: testPlanProjectRoot },
|
|
1654
|
+
});
|
|
1655
|
+
assert.equal(acceptSkip.status, 'skipped', 'ralph.accept should skip when no accepted-risk register exists');
|
|
1656
|
+
const acceptPass = await acceptStepper.run({
|
|
1657
|
+
runId: 'deep_scan_accept_pass_selftest',
|
|
1658
|
+
config: { generatedAt: '2026-05-28T00:00:00.000Z' },
|
|
1659
|
+
state: {},
|
|
1660
|
+
tools: { rootPath: acceptDir },
|
|
1661
|
+
});
|
|
1662
|
+
assert.equal(acceptPass.stepperId, ACCEPTED_RISK_STEPPER_ID, 'ralph.accept stepper id should be stable');
|
|
1663
|
+
assert.equal(acceptPass.status, 'passed', 'ralph.accept should pass when an accepted-risk register exists');
|
|
1664
|
+
assert.ok(acceptPass.artifacts.some((ref) => ref.type === 'ralph_accepted_risk'), 'ralph.accept should emit a ralph_accepted_risk artifact');
|
|
1665
|
+
const acceptStateRef = acceptPass.artifacts.find((ref) => ref.type === 'ralph_accepted_risk');
|
|
1666
|
+
const acceptStateText = await fs.readFile(path.join(acceptDir, acceptStateRef.uri), 'utf8');
|
|
1667
|
+
assert.match(acceptStateText, /ralph_accepted_risk\.v1/, 'Written accepted-risk state artifact should include schema version');
|
|
1668
|
+
assert.doesNotMatch(acceptStateText, /sk_live|sourceCode|const secret|function\s+\w+\(/i, 'Accepted-risk state artifact must remain source-safe');
|
|
1669
|
+
|
|
1670
|
+
const registry = createSampleStepperRegistry(new StepperRegistry());
|
|
1671
|
+
assert.ok(registry.get('project.map'), 'Default Deep Scan registry should include the Project Map stepper');
|
|
1672
|
+
assert.ok(registry.get('rules.scan'), 'Default Deep Scan registry should include the deterministic scan stepper');
|
|
1673
|
+
assert.ok(registry.get('tests.plan'), 'Default Deep Scan registry should include the Security Test Plan stepper');
|
|
1674
|
+
assert.ok(registry.get('ralph.tasks'), 'Default Deep Scan registry should include the Agent Fix Task stepper');
|
|
1675
|
+
assert.ok(registry.get('ralph.compare'), 'Default Deep Scan registry should include the Ralph comparison stepper');
|
|
1676
|
+
assert.ok(registry.get('ralph.accept'), 'Default Deep Scan registry should include the Ralph accepted-risk stepper');
|
|
1677
|
+
assert.ok(registry.get('ralph.track'), 'Default Deep Scan registry should include the Ralph failure tracking stepper');
|
|
1678
|
+
assert.throws(
|
|
1679
|
+
() => registry.register(sampleNoopStepper()),
|
|
1680
|
+
/duplicate/i,
|
|
1681
|
+
'StepperRegistry should reject duplicate stepper ids',
|
|
1682
|
+
);
|
|
1683
|
+
assert.equal(
|
|
1684
|
+
registry.validateProfile({
|
|
1685
|
+
id: 'deep-scan-test',
|
|
1686
|
+
version: '1.0.0',
|
|
1687
|
+
checkpoint: true,
|
|
1688
|
+
steppers: [
|
|
1689
|
+
{ id: 'sample.noop', required: true },
|
|
1690
|
+
{ id: 'project.map', required: true },
|
|
1691
|
+
{ id: 'rules.scan', required: true },
|
|
1692
|
+
{ id: 'tests.plan', required: true },
|
|
1693
|
+
{ id: 'ralph.tasks', required: true },
|
|
1694
|
+
{ id: 'ralph.compare', required: true },
|
|
1695
|
+
{ id: 'ralph.accept', required: true },
|
|
1696
|
+
{ id: 'ralph.track', required: true },
|
|
1697
|
+
{ id: 'sample.disabledOptional', required: false, enabled: false },
|
|
1698
|
+
{ id: 'sample.needsHuman', required: true },
|
|
1699
|
+
],
|
|
1700
|
+
}).valid,
|
|
1701
|
+
true,
|
|
1702
|
+
'GraphProfile validation should accept registered, reorderable, disabled optional steps',
|
|
1703
|
+
);
|
|
1704
|
+
const missingValidation = registry.validateProfile({
|
|
1705
|
+
id: 'bad-profile',
|
|
1706
|
+
version: '1.0.0',
|
|
1707
|
+
steppers: [{ id: 'sample.missing', required: true }],
|
|
1708
|
+
});
|
|
1709
|
+
assert.equal(missingValidation.valid, false, 'GraphProfile validation should reject missing stepper ids');
|
|
1710
|
+
assert.match(missingValidation.errors[0].message, /sample\.missing/, 'missing step errors should name the stepper id');
|
|
1711
|
+
|
|
1712
|
+
const unsafeRegistry = new StepperRegistry().register({
|
|
1713
|
+
id: 'sample.external',
|
|
1714
|
+
version: '1.0.0',
|
|
1715
|
+
title: 'External Stepper',
|
|
1716
|
+
category: 'runtime_proof',
|
|
1717
|
+
unsafeExternal: true,
|
|
1718
|
+
async run() {
|
|
1719
|
+
throw new Error('unsafe external stepper should not run without definition-level approval requirements');
|
|
1720
|
+
},
|
|
1721
|
+
});
|
|
1722
|
+
const unsafeValidation = unsafeRegistry.validateProfile({
|
|
1723
|
+
id: 'unsafe-profile',
|
|
1724
|
+
version: '1.0.0',
|
|
1725
|
+
steppers: [{ id: 'sample.external', required: true, requiresApproval: true }],
|
|
1726
|
+
});
|
|
1727
|
+
assert.equal(
|
|
1728
|
+
unsafeValidation.valid,
|
|
1729
|
+
false,
|
|
1730
|
+
'unsafe external steppers must define concrete approval requirements before profiles can enable them',
|
|
1731
|
+
);
|
|
1732
|
+
assert.match(
|
|
1733
|
+
unsafeValidation.errors[0].message,
|
|
1734
|
+
/approval requirements/i,
|
|
1735
|
+
'unsafe external profile errors should explain the missing approval requirement',
|
|
1736
|
+
);
|
|
1737
|
+
|
|
1738
|
+
const store = new LocalDeepScanStore({ rootPath: projectRoot });
|
|
1739
|
+
assert.throws(
|
|
1740
|
+
() => new LocalDeepScanStore({ rootPath: projectRoot, directory: '../outside' }),
|
|
1741
|
+
/inside rootPath/i,
|
|
1742
|
+
'Deep Scan store should reject directories that escape the bound root',
|
|
1743
|
+
);
|
|
1744
|
+
const outsideProjectMapOverride = path.join(tmpBase, 'outside-project-map-override');
|
|
1745
|
+
await fs.mkdir(outsideProjectMapOverride, { recursive: true });
|
|
1746
|
+
await fs.writeFile(
|
|
1747
|
+
path.join(outsideProjectMapOverride, 'package.json'),
|
|
1748
|
+
JSON.stringify({ scripts: { test: 'echo should-not-map' }, dependencies: { fastify: '^5.0.0' } }, null, 2),
|
|
1749
|
+
'utf8',
|
|
1750
|
+
);
|
|
1751
|
+
const runtime = new DeepScanRuntime({ registry, store });
|
|
1752
|
+
const run = await runtime.createRun({
|
|
1753
|
+
projectId: 'project_123',
|
|
1754
|
+
projectHash: 'f'.repeat(64),
|
|
1755
|
+
graphProfile: {
|
|
1756
|
+
id: 'deep-scan-test',
|
|
1757
|
+
version: '1.0.0',
|
|
1758
|
+
checkpoint: true,
|
|
1759
|
+
steppers: [
|
|
1760
|
+
{ id: 'sample.noop', required: true },
|
|
1761
|
+
{ id: 'sample.needsHuman', required: true },
|
|
1762
|
+
{ id: 'project.map', required: true, config: { rootPath: outsideProjectMapOverride } },
|
|
1763
|
+
{ id: 'rules.scan', required: true, config: { rootPath: outsideProjectMapOverride } },
|
|
1764
|
+
{ id: 'tests.plan', required: true, config: { rootPath: outsideProjectMapOverride } },
|
|
1765
|
+
{ id: 'ralph.tasks', required: true, config: { rootPath: outsideProjectMapOverride } },
|
|
1766
|
+
{ id: 'ralph.compare', required: true, config: { rootPath: outsideProjectMapOverride } },
|
|
1767
|
+
{ id: 'ralph.accept', required: true, config: { rootPath: outsideProjectMapOverride } },
|
|
1768
|
+
{ id: 'ralph.track', required: true, config: { rootPath: outsideProjectMapOverride } },
|
|
1769
|
+
],
|
|
1770
|
+
},
|
|
1771
|
+
metadata: { runtimeBoundary: 'mcp-local', artifactStorage: 'refs-only' },
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
const paused = await runtime.startRun(run.runId);
|
|
1775
|
+
assert.equal(paused.status, 'needs_human', 'runtime should pause at approval-gated step');
|
|
1776
|
+
assert.equal(Object.keys(paused.stepResults).length, 2, 'runtime should checkpoint after every completed or paused step');
|
|
1777
|
+
assert.equal(paused.stepResults['sample.noop'].status, 'passed', 'completed step should be checkpointed');
|
|
1778
|
+
assert.equal(paused.stepResults['sample.needsHuman'].status, 'needs_human', 'approval step should be checkpointed');
|
|
1779
|
+
|
|
1780
|
+
const checkpoint = await store.getRun(run.runId);
|
|
1781
|
+
assert.equal(checkpoint.status, 'needs_human', 'local store should persist checkpointed status');
|
|
1782
|
+
assert.doesNotMatch(JSON.stringify(checkpoint), /sk_live|sourceCode|rawSource|const secret|function\s+\w+\(/i);
|
|
1783
|
+
|
|
1784
|
+
const status = buildDeepScanStatus(checkpoint);
|
|
1785
|
+
assert.equal(status.runId, run.runId, 'status inspection should include the run id');
|
|
1786
|
+
assert.match(status.nextAction, /approval/i, 'status inspection should explain the next safe approval action');
|
|
1787
|
+
|
|
1788
|
+
await runtime.recordApproval({
|
|
1789
|
+
runId: run.runId,
|
|
1790
|
+
requirementId: 'sample.needsHuman',
|
|
1791
|
+
decision: 'approved',
|
|
1792
|
+
approvedBy: 'selftest',
|
|
1793
|
+
reason: 'Approve fixture-only stepper execution.',
|
|
1794
|
+
metadata: { scope: 'fixture' },
|
|
1795
|
+
});
|
|
1796
|
+
const resumed = await runtime.resumeRun(run.runId);
|
|
1797
|
+
assert.equal(resumed.status, 'passed', 'runtime should finish after approval is recorded');
|
|
1798
|
+
assert.equal(Object.keys(resumed.stepResults).length, 9, 'resume should not duplicate completed steps');
|
|
1799
|
+
assert.match(resumed.artifacts['project.map'].hash, /^[a-f0-9]{64}$/i, 'Project Map artifact references should be indexed by stepper id');
|
|
1800
|
+
assert.match(resumed.artifacts['rules.scan'].hash, /^[a-f0-9]{64}$/i, 'Deterministic scan artifact references should be indexed by stepper id');
|
|
1801
|
+
assert.match(resumed.artifacts['tests.plan'].hash, /^[a-f0-9]{64}$/i, 'Security Test Plan artifact references should be indexed by stepper id');
|
|
1802
|
+
assert.match(resumed.artifacts['ralph.tasks'].hash, /^[a-f0-9]{64}$/i, 'Agent Fix Task artifact references should be indexed by stepper id');
|
|
1803
|
+
assert.equal(resumed.receipts.length, 1, 'runtime should aggregate deterministic scan receipt refs');
|
|
1804
|
+
assert.equal(resumed.receipts[0].engine.counts.deterministic > 0, true, 'runtime receipt refs should preserve rule counts');
|
|
1805
|
+
await fs.access(path.join(projectRoot, resumed.artifacts['project.map'].uri));
|
|
1806
|
+
await fs.access(path.join(projectRoot, resumed.artifacts['rules.scan'].uri));
|
|
1807
|
+
await fs.access(path.join(projectRoot, resumed.artifacts['tests.plan'].uri));
|
|
1808
|
+
await fs.access(path.join(projectRoot, resumed.artifacts['ralph.tasks'].uri));
|
|
1809
|
+
await assert.rejects(
|
|
1810
|
+
fs.access(path.join(outsideProjectMapOverride, resumed.artifacts['project.map'].uri)),
|
|
1811
|
+
'Project Map runtime must ignore config.rootPath overrides when a bound root is provided',
|
|
1812
|
+
);
|
|
1813
|
+
await assert.rejects(
|
|
1814
|
+
fs.access(path.join(outsideProjectMapOverride, resumed.artifacts['rules.scan'].uri)),
|
|
1815
|
+
'Deterministic Scan runtime must ignore config.rootPath overrides when a bound root is provided',
|
|
1816
|
+
);
|
|
1817
|
+
await assert.rejects(
|
|
1818
|
+
fs.access(path.join(outsideProjectMapOverride, resumed.artifacts['tests.plan'].uri)),
|
|
1819
|
+
'Security Test Plan runtime must ignore config.rootPath overrides when a bound root is provided',
|
|
1820
|
+
);
|
|
1821
|
+
await assert.rejects(
|
|
1822
|
+
fs.access(path.join(outsideProjectMapOverride, resumed.artifacts['ralph.tasks'].uri)),
|
|
1823
|
+
'Agent Fix Task runtime must ignore config.rootPath overrides when a bound root is provided',
|
|
1824
|
+
);
|
|
1825
|
+
const completedStatus = buildDeepScanStatus(resumed);
|
|
1826
|
+
assert.equal(
|
|
1827
|
+
resumed.stepResults['ralph.compare'].status,
|
|
1828
|
+
'skipped',
|
|
1829
|
+
'Ralph comparison should skip in default profile when no baseline deterministic scan ref is configured',
|
|
1830
|
+
);
|
|
1831
|
+
assert.equal(
|
|
1832
|
+
resumed.stepResults['ralph.accept'].status,
|
|
1833
|
+
'skipped',
|
|
1834
|
+
'Ralph accepted-risk should skip in default profile when no accepted-risk register exists',
|
|
1835
|
+
);
|
|
1836
|
+
assert.equal(
|
|
1837
|
+
resumed.stepResults['ralph.track'].status,
|
|
1838
|
+
'skipped',
|
|
1839
|
+
'Ralph failure tracking should skip in default profile when comparison was skipped',
|
|
1840
|
+
);
|
|
1841
|
+
assert.deepEqual(
|
|
1842
|
+
completedStatus.steps.map((step) => step.stepperId),
|
|
1843
|
+
['sample.noop', 'sample.needsHuman', 'project.map', 'rules.scan', 'tests.plan', 'ralph.tasks', 'ralph.compare', 'ralph.accept', 'ralph.track'],
|
|
1844
|
+
'Deep Scan status should preserve graph profile order for progress monitoring',
|
|
1845
|
+
);
|
|
1846
|
+
const artifactIds = completedStatus.artifactRefs.map((ref) => ref.id);
|
|
1847
|
+
assert.equal(
|
|
1848
|
+
artifactIds.length,
|
|
1849
|
+
new Set(artifactIds).size,
|
|
1850
|
+
'Deep Scan status should expose each artifact ref once even when runtime indexes by stepper id and artifact id',
|
|
1851
|
+
);
|
|
1852
|
+
assert.doesNotMatch(JSON.stringify(resumed), /sk_live|sourceCode|rawSource|const secret|function\s+\w+\(/i);
|
|
1853
|
+
|
|
1854
|
+
const deniedRun = await runtime.createRun({
|
|
1855
|
+
projectId: 'project_123',
|
|
1856
|
+
projectHash: 'f'.repeat(64),
|
|
1857
|
+
graphProfile: {
|
|
1858
|
+
id: 'deep-scan-denial-test',
|
|
1859
|
+
version: '1.0.0',
|
|
1860
|
+
checkpoint: true,
|
|
1861
|
+
steppers: [
|
|
1862
|
+
{ id: 'sample.noop', required: true },
|
|
1863
|
+
{ id: 'sample.needsHuman', required: true },
|
|
1864
|
+
{ id: 'project.map', required: true },
|
|
1865
|
+
{ id: 'rules.scan', required: true },
|
|
1866
|
+
],
|
|
1867
|
+
},
|
|
1868
|
+
metadata: { runtimeBoundary: 'mcp-local', artifactStorage: 'refs-only' },
|
|
1869
|
+
});
|
|
1870
|
+
await runtime.startRun(deniedRun.runId);
|
|
1871
|
+
await runtime.recordApproval({
|
|
1872
|
+
runId: deniedRun.runId,
|
|
1873
|
+
requirementId: 'sample.needsHuman',
|
|
1874
|
+
decision: 'denied',
|
|
1875
|
+
approvedBy: 'selftest',
|
|
1876
|
+
reason: 'Deny fixture-only approval to prove fail-closed behavior.',
|
|
1877
|
+
metadata: { scope: 'fixture' },
|
|
1878
|
+
});
|
|
1879
|
+
const blocked = await runtime.resumeRun(deniedRun.runId);
|
|
1880
|
+
assert.equal(blocked.status, 'blocked', 'runtime should block when required approval is denied');
|
|
1881
|
+
assert.equal(blocked.stepResults['sample.needsHuman'].status, 'blocked', 'denied approval should become a blocked step');
|
|
1882
|
+
assert.match(blocked.stepResults['sample.needsHuman'].blockedReason, /denied/i);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
await withMockMcpBackend(async (apiBase) => {
|
|
1886
|
+
const bind = await requestServerBind({
|
|
1887
|
+
lockedRootPath: projectRoot,
|
|
1888
|
+
authToken: 'test-token',
|
|
1889
|
+
apiBase,
|
|
1890
|
+
});
|
|
1891
|
+
assert.equal(bind.ok, true, 'mock backend bind should succeed with auth token');
|
|
1892
|
+
assert.match(bind.json?.data?.installToken, /^[a-f0-9]{64}$/, 'bind should return backend-issued token');
|
|
1893
|
+
|
|
1894
|
+
const verified = await verifyInstallBinding({
|
|
1895
|
+
installToken: bind.json.data.installToken,
|
|
1896
|
+
lockedRootHash: bind.json.data.lockedRootHash,
|
|
1897
|
+
apiBase,
|
|
1898
|
+
});
|
|
1899
|
+
assert.equal(verified.ok, true, 'backend-issued bind should verify successfully');
|
|
1900
|
+
assert.equal(verified.json?.success, true, 'verify-install should return success:true');
|
|
1901
|
+
|
|
1902
|
+
const rejected = await verifyInstallBinding({
|
|
1903
|
+
installToken: 'e'.repeat(64),
|
|
1904
|
+
lockedRootHash: bind.json.data.lockedRootHash,
|
|
1905
|
+
apiBase,
|
|
1906
|
+
});
|
|
1907
|
+
assert.equal(rejected.ok, false, 'local-only token/hash pairs must be rejected by verify-install');
|
|
1908
|
+
assert.equal(rejected.json?.code, 'MCP_INSTALL_INVALID', 'verify-install rejection code must be preserved');
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
await withMockMcpBackend(async (apiBase, ctx) => {
|
|
1912
|
+
const bind = await requestServerBind({
|
|
1913
|
+
lockedRootPath: projectRoot,
|
|
1914
|
+
authToken: 'same-user-token',
|
|
1915
|
+
apiBase,
|
|
1916
|
+
});
|
|
1917
|
+
assert.equal(bind.ok, true, 'E1: bind should succeed before in-folder scan');
|
|
1918
|
+
await createLock({
|
|
1919
|
+
rootPath: projectRoot,
|
|
1920
|
+
account: 'selftest',
|
|
1921
|
+
installToken: bind.json.data.installToken,
|
|
1922
|
+
lockedRootHash: bind.json.data.lockedRootHash,
|
|
1923
|
+
source: 'server',
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
const guardPath = createBindingGuard({
|
|
1927
|
+
boundRoot: projectRoot,
|
|
1928
|
+
installToken: bind.json.data.installToken,
|
|
1929
|
+
apiBase,
|
|
1930
|
+
});
|
|
1931
|
+
const guard = await guardPath(projectRoot);
|
|
1932
|
+
assert.equal(guard.ok, true, 'E1: guard should accept the bound project root');
|
|
1933
|
+
|
|
1934
|
+
const savedBase = process.env.VIBESECUR_API_BASE;
|
|
1935
|
+
process.env.VIBESECUR_API_BASE = apiBase;
|
|
1936
|
+
try {
|
|
1937
|
+
const { runScan } = await import(
|
|
1938
|
+
pathToFileURL(path.resolve('./src/orchestrator/runScan.js')).href
|
|
1939
|
+
);
|
|
1940
|
+
const remoteOutcome = await runScan({
|
|
1941
|
+
code: 'const token = "sk_live_1234567890abcdef";',
|
|
1942
|
+
lang: 'js',
|
|
1943
|
+
projectRoot: guard.resolvedRoot,
|
|
1944
|
+
installToken: bind.json.data.installToken,
|
|
1945
|
+
lockedRootHash: bind.json.data.lockedRootHash,
|
|
1946
|
+
engineVersion: '0.0.0-test',
|
|
1947
|
+
});
|
|
1948
|
+
assert.equal(remoteOutcome.ok, true, 'E1: valid bound scan should succeed');
|
|
1949
|
+
assert.equal(remoteOutcome.mode, 'remote', 'E1: valid bound scan should use remote mode');
|
|
1950
|
+
assert.equal(remoteOutcome.result.receiptId, 'scan-receipt-1', 'E1: receipt metadata should be preserved');
|
|
1951
|
+
assert.deepEqual(remoteOutcome.quota, { limit: 10, remaining: 9 }, 'E1: quota metadata should be preserved');
|
|
1952
|
+
|
|
1953
|
+
// L1: a successful remote scan must persist metadata to /scan/log so the
|
|
1954
|
+
// Projects dashboard reflects it. Best-effort logging is wired here.
|
|
1955
|
+
assert.equal(remoteOutcome.logged?.ok, true, 'L1: successful scan should report logged:true');
|
|
1956
|
+
assert.equal(ctx.scanLogCalls.length, 1, 'L1: exactly one /scan/log call should be made');
|
|
1957
|
+
const logCall = ctx.scanLogCalls[0];
|
|
1958
|
+
assert.equal(logCall.source, 'mcp', 'L1: scan log should mark source as mcp');
|
|
1959
|
+
assert.equal(logCall.installToken, bind.json.data.installToken, 'L1: scan log should carry the install token');
|
|
1960
|
+
assert.equal(logCall.lockedRootHash, bind.json.data.lockedRootHash, 'L1: scan log should carry the locked root hash');
|
|
1961
|
+
assert.equal(Object.prototype.hasOwnProperty.call(logCall, 'code'), false, 'L1: scan log must never include raw code');
|
|
1962
|
+
assert.ok(Array.isArray(logCall.findings), 'L1: scan log should include a findings array');
|
|
1963
|
+
|
|
1964
|
+
const quotaOutcome = await runScan({
|
|
1965
|
+
code: 'quota-exhausted',
|
|
1966
|
+
lang: 'js',
|
|
1967
|
+
projectRoot: guard.resolvedRoot,
|
|
1968
|
+
installToken: bind.json.data.installToken,
|
|
1969
|
+
lockedRootHash: bind.json.data.lockedRootHash,
|
|
1970
|
+
engineVersion: '0.0.0-test',
|
|
1971
|
+
});
|
|
1972
|
+
assert.equal(quotaOutcome.ok, false, 'Q1: 402 should be returned as an error outcome');
|
|
1973
|
+
assert.equal(quotaOutcome.errorCode, 'UPGRADE_REQUIRED', 'Q1: 402 should map to UPGRADE_REQUIRED');
|
|
1974
|
+
assert.match(quotaOutcome.errorPayload.upgradeUrl, /#pricing$/, 'Q1: upgrade URL should be actionable');
|
|
1975
|
+
|
|
1976
|
+
const fallbackOutcome = await runScan({
|
|
1977
|
+
code: 'remote-ok-without-data eval("1")',
|
|
1978
|
+
lang: 'js',
|
|
1979
|
+
projectRoot: guard.resolvedRoot,
|
|
1980
|
+
installToken: bind.json.data.installToken,
|
|
1981
|
+
lockedRootHash: bind.json.data.lockedRootHash,
|
|
1982
|
+
engineVersion: '0.0.0-test',
|
|
1983
|
+
});
|
|
1984
|
+
assert.equal(fallbackOutcome.ok, true, 'F1: 2xx response without data should use local fallback');
|
|
1985
|
+
assert.equal(fallbackOutcome.mode, 'offline', 'F1: degraded 2xx fallback should be explicit offline mode');
|
|
1986
|
+
} finally {
|
|
1987
|
+
if (savedBase === undefined) delete process.env.VIBESECUR_API_BASE;
|
|
1988
|
+
else process.env.VIBESECUR_API_BASE = savedBase;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
const outsideRoot = path.join(tmpBase, 'outside');
|
|
1992
|
+
await fs.mkdir(outsideRoot, { recursive: true });
|
|
1993
|
+
const outside = await guardPath(outsideRoot);
|
|
1994
|
+
assert.equal(outside.ok, false, 'O1: outside root should be rejected');
|
|
1995
|
+
assert.equal(outside.httpStatus, 403, 'O1: outside root should be a 403');
|
|
1996
|
+
assert.equal(outside.code, 'OUT_OF_FOLDER', 'O1: outside root should use OUT_OF_FOLDER');
|
|
1997
|
+
assert.match(outside.rebindHint, /vibesecur-mcp rebind/, 'O1: outside root should include rebind hint');
|
|
1998
|
+
|
|
1999
|
+
const mismatchGuard = createBindingGuard({
|
|
2000
|
+
boundRoot: projectRoot,
|
|
2001
|
+
installToken: 'f'.repeat(64),
|
|
2002
|
+
apiBase,
|
|
2003
|
+
});
|
|
2004
|
+
const mismatch = await mismatchGuard(projectRoot);
|
|
2005
|
+
assert.equal(mismatch.ok, false, 'T1: mismatched token should fail closed');
|
|
2006
|
+
assert.equal(mismatch.httpStatus, 403, 'T1: mismatched token should be a 403');
|
|
2007
|
+
assert.equal(mismatch.code, 'TOKEN_MISMATCH', 'T1: mismatched token should preserve local diagnostic code');
|
|
2008
|
+
|
|
2009
|
+
const reboundRoot = path.join(tmpBase, 'rebound-project');
|
|
2010
|
+
await fs.mkdir(reboundRoot, { recursive: true });
|
|
2011
|
+
const rebound = await requestServerBind({
|
|
2012
|
+
lockedRootPath: reboundRoot,
|
|
2013
|
+
authToken: 'same-user-token',
|
|
2014
|
+
apiBase,
|
|
2015
|
+
});
|
|
2016
|
+
assert.equal(rebound.ok, true, 'R2: rebind should issue a new token');
|
|
2017
|
+
|
|
2018
|
+
const staleGuard = await guardPath(projectRoot);
|
|
2019
|
+
assert.equal(staleGuard.ok, false, 'R2: warmed runtime guard should reject old token immediately after rebind');
|
|
2020
|
+
assert.equal(staleGuard.code, 'MCP_INSTALL_INVALID', 'R2: warmed runtime guard should preserve backend invalidation code');
|
|
2021
|
+
|
|
2022
|
+
const oldVerify = await verifyInstallBinding({
|
|
2023
|
+
installToken: bind.json.data.installToken,
|
|
2024
|
+
lockedRootHash: bind.json.data.lockedRootHash,
|
|
2025
|
+
apiBase,
|
|
2026
|
+
});
|
|
2027
|
+
assert.equal(oldVerify.ok, false, 'R2: old token should fail after rebind');
|
|
2028
|
+
assert.equal(oldVerify.json?.code, 'MCP_INSTALL_INVALID', 'R2: old token failure should preserve code');
|
|
2029
|
+
|
|
2030
|
+
const cliPath = path.resolve('./src/cli.js');
|
|
2031
|
+
const staleCli = await execFileAsync(
|
|
2032
|
+
process.execPath,
|
|
2033
|
+
[cliPath, 'deep-scan-start', projectRoot, `--api-base=${apiBase}`],
|
|
2034
|
+
{ cwd: path.resolve('.'), env: { ...process.env, VIBESECUR_API_BASE: apiBase } },
|
|
2035
|
+
).then(
|
|
2036
|
+
() => ({ code: 0, stdout: '', stderr: '' }),
|
|
2037
|
+
(err) => ({ code: err.code, stdout: err.stdout || '', stderr: err.stderr || '' }),
|
|
2038
|
+
);
|
|
2039
|
+
assert.notEqual(staleCli.code, 0, 'R3: Deep Scan CLI should reject revoked server locks');
|
|
2040
|
+
assert.match(
|
|
2041
|
+
`${staleCli.stdout}\n${staleCli.stderr}`,
|
|
2042
|
+
/MCP_INSTALL_INVALID|server-issued MCP binding|verify/i,
|
|
2043
|
+
'R3: Deep Scan CLI should surface backend binding verification failure',
|
|
2044
|
+
);
|
|
2045
|
+
|
|
2046
|
+
const newVerify = await verifyInstallBinding({
|
|
2047
|
+
installToken: rebound.json.data.installToken,
|
|
2048
|
+
lockedRootHash: rebound.json.data.lockedRootHash,
|
|
2049
|
+
apiBase,
|
|
2050
|
+
});
|
|
2051
|
+
assert.equal(newVerify.ok, true, 'R2: new token should verify after rebind');
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
{
|
|
2055
|
+
const savedBase = process.env.VIBESECUR_API_BASE;
|
|
2056
|
+
process.env.VIBESECUR_API_BASE = 'http://127.0.0.1:9';
|
|
2057
|
+
try {
|
|
2058
|
+
const { runScan } = await import(
|
|
2059
|
+
pathToFileURL(path.resolve('./src/orchestrator/runScan.js')).href
|
|
2060
|
+
);
|
|
2061
|
+
const unavailable = await runScan({
|
|
2062
|
+
code: 'const x = eval("1")',
|
|
2063
|
+
lang: 'js',
|
|
2064
|
+
projectRoot,
|
|
2065
|
+
installToken: 'd'.repeat(64),
|
|
2066
|
+
lockedRootHash: buildLockHashFromLockModule(projectRoot),
|
|
2067
|
+
engineVersion: '0.0.0-test',
|
|
2068
|
+
});
|
|
2069
|
+
assert.equal(unavailable.ok, false, 'A1: unavailable API should fail closed');
|
|
2070
|
+
assert.equal(
|
|
2071
|
+
unavailable.errorCode,
|
|
2072
|
+
'REMOTE_VERIFICATION_REQUIRED',
|
|
2073
|
+
'A1: unavailable API should not silently fall back to local scan',
|
|
2074
|
+
);
|
|
2075
|
+
} finally {
|
|
2076
|
+
if (savedBase === undefined) delete process.env.VIBESECUR_API_BASE;
|
|
2077
|
+
else process.env.VIBESECUR_API_BASE = savedBase;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
{
|
|
2082
|
+
const cliProject = path.join(tmpBase, 'cli-local-fallback');
|
|
2083
|
+
await fs.mkdir(cliProject, { recursive: true });
|
|
2084
|
+
const cliPath = path.resolve('./src/cli.js');
|
|
2085
|
+
const cliEnv = {
|
|
2086
|
+
...process.env,
|
|
2087
|
+
VIBESECUR_API_BASE: 'http://127.0.0.1:9',
|
|
2088
|
+
VIBESECUR_AUTH_TOKEN: '',
|
|
2089
|
+
};
|
|
2090
|
+
const noFallback = await execFileAsync(
|
|
2091
|
+
process.execPath,
|
|
2092
|
+
[cliPath, 'bind', cliProject, '--api-base=http://127.0.0.1:9'],
|
|
2093
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2094
|
+
).then(
|
|
2095
|
+
() => ({ code: 0, stdout: '', stderr: '' }),
|
|
2096
|
+
(err) => ({ code: err.code, stdout: err.stdout || '', stderr: err.stderr || '' }),
|
|
2097
|
+
);
|
|
2098
|
+
assert.notEqual(noFallback.code, 0, 'C1: bind without fallback should fail when API is unavailable');
|
|
2099
|
+
|
|
2100
|
+
const fallback = await execFileAsync(
|
|
2101
|
+
process.execPath,
|
|
2102
|
+
[cliPath, 'bind', cliProject, '--api-base=http://127.0.0.1:9', '--allow-local-fallback=true'],
|
|
2103
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2104
|
+
);
|
|
2105
|
+
assert.match(
|
|
2106
|
+
`${fallback.stdout}\n${fallback.stderr}`,
|
|
2107
|
+
/Local diagnostic lock created|local diagnostic lock/i,
|
|
2108
|
+
'C1: explicit fallback should create a diagnostic local lock',
|
|
2109
|
+
);
|
|
2110
|
+
const fallbackDiag = await diagnosticLock(cliProject);
|
|
2111
|
+
assert.equal(fallbackDiag.runtimeCompatible, false, 'C1: local fallback lock must not be runtime compatible');
|
|
2112
|
+
|
|
2113
|
+
const unboundStatus = await execFileAsync(
|
|
2114
|
+
process.execPath,
|
|
2115
|
+
[cliPath, 'deep-scan-status', 'deep_scan_unbound_status', cliProject],
|
|
2116
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2117
|
+
).then(
|
|
2118
|
+
() => ({ code: 0, stdout: '', stderr: '' }),
|
|
2119
|
+
(err) => ({ code: err.code, stdout: err.stdout || '', stderr: err.stderr || '' }),
|
|
2120
|
+
);
|
|
2121
|
+
assert.notEqual(unboundStatus.code, 0, 'C3: Deep Scan status should fail for an unbound project');
|
|
2122
|
+
assert.match(
|
|
2123
|
+
`${unboundStatus.stdout}\n${unboundStatus.stderr}`,
|
|
2124
|
+
/server-issued MCP binding/i,
|
|
2125
|
+
'C3: Deep Scan status should fail on binding, not read arbitrary local run files',
|
|
2126
|
+
);
|
|
2127
|
+
|
|
2128
|
+
await createLock({
|
|
2129
|
+
rootPath: projectRoot,
|
|
2130
|
+
account: 'selftest',
|
|
2131
|
+
installToken: '8'.repeat(64),
|
|
2132
|
+
lockedRootHash: buildLockHashFromLockModule(projectRoot),
|
|
2133
|
+
source: 'server',
|
|
2134
|
+
});
|
|
2135
|
+
const copiedLockProject = path.join(tmpBase, 'cli-copied-lock');
|
|
2136
|
+
await fs.mkdir(path.join(copiedLockProject, '.vibesecur'), { recursive: true });
|
|
2137
|
+
await fs.copyFile(
|
|
2138
|
+
path.join(projectRoot, '.vibesecur', 'lock.json'),
|
|
2139
|
+
path.join(copiedLockProject, '.vibesecur', 'lock.json'),
|
|
2140
|
+
);
|
|
2141
|
+
const copiedLockStatus = await execFileAsync(
|
|
2142
|
+
process.execPath,
|
|
2143
|
+
[cliPath, 'deep-scan-status', 'deep_scan_copied_lock', copiedLockProject],
|
|
2144
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2145
|
+
).then(
|
|
2146
|
+
() => ({ code: 0, stdout: '', stderr: '' }),
|
|
2147
|
+
(err) => ({ code: err.code, stdout: err.stdout || '', stderr: err.stderr || '' }),
|
|
2148
|
+
);
|
|
2149
|
+
assert.notEqual(copiedLockStatus.code, 0, 'C4: Deep Scan CLI should reject copied locks outside the bound root');
|
|
2150
|
+
assert.match(
|
|
2151
|
+
`${copiedLockStatus.stdout}\n${copiedLockStatus.stderr}`,
|
|
2152
|
+
/outside the locked project folder|server-issued MCP binding/i,
|
|
2153
|
+
'C4: copied lock failure should explain the bound-root mismatch',
|
|
2154
|
+
);
|
|
2155
|
+
|
|
2156
|
+
const configProject = path.join(tmpBase, 'cli-config-server-lock');
|
|
2157
|
+
await fs.mkdir(configProject, { recursive: true });
|
|
2158
|
+
await createLock({
|
|
2159
|
+
rootPath: configProject,
|
|
2160
|
+
account: 'selftest',
|
|
2161
|
+
installToken: '9'.repeat(64),
|
|
2162
|
+
lockedRootHash: buildLockHashFromLockModule(configProject),
|
|
2163
|
+
source: 'server',
|
|
2164
|
+
});
|
|
2165
|
+
const config = await execFileAsync(
|
|
2166
|
+
process.execPath,
|
|
2167
|
+
[cliPath, 'config', configProject, '--api-base=http://127.0.0.1:4000'],
|
|
2168
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2169
|
+
);
|
|
2170
|
+
const configOutput = `${config.stdout}\n${config.stderr}`;
|
|
2171
|
+
assert.match(configOutput, /Cursor/i, 'C2: config output should include Cursor snippet');
|
|
2172
|
+
assert.match(configOutput, /VS Code/i, 'C2: config output should include VS Code snippet');
|
|
2173
|
+
assert.match(configOutput, /Windsurf/i, 'C2: config output should include Windsurf snippet');
|
|
2174
|
+
assert.match(configOutput, /VIBESECUR_INSTALL_TOKEN/i, 'C2: config output should include install token env');
|
|
2175
|
+
assert.match(configOutput, /VIBESECUR_BOUND_ROOT/i, 'C2: config output should include bound root env');
|
|
2176
|
+
assert.match(configOutput, /@saiteja1123\/mcp-server|server\.js/i, 'C2: config output should include runtime command');
|
|
2177
|
+
|
|
2178
|
+
const demoProject = path.join(tmpBase, 'cli-deep-scan-demo');
|
|
2179
|
+
await fs.mkdir(demoProject, { recursive: true });
|
|
2180
|
+
await fs.writeFile(
|
|
2181
|
+
path.join(demoProject, 'package.json'),
|
|
2182
|
+
JSON.stringify({ scripts: { test: 'node --test' }, dependencies: { express: '^5.0.0' } }, null, 2),
|
|
2183
|
+
'utf8',
|
|
2184
|
+
);
|
|
2185
|
+
const demoStart = await execFileAsync(
|
|
2186
|
+
process.execPath,
|
|
2187
|
+
[cliPath, 'deep-scan-start', demoProject, '--demo=true'],
|
|
2188
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2189
|
+
);
|
|
2190
|
+
const demoOutput = `${demoStart.stdout}\n${demoStart.stderr}`;
|
|
2191
|
+
const demoJson = JSON.parse(demoOutput.slice(demoOutput.indexOf('{')));
|
|
2192
|
+
assert.deepEqual(
|
|
2193
|
+
demoJson.steps.map((step) => step.stepperId),
|
|
2194
|
+
['project.map', 'rules.scan', 'tests.plan', 'ralph.tasks', 'ralph.compare', 'ralph.accept', 'ralph.track'],
|
|
2195
|
+
'C5: Deep Scan CLI default profile should run project.map, rules.scan, tests.plan, ralph.tasks, ralph.compare, ralph.accept, and ralph.track in dependency order without sample proof steppers',
|
|
2196
|
+
);
|
|
2197
|
+
assert.ok(
|
|
2198
|
+
demoJson.artifactRefs.some((ref) => ref.type === 'project_map'),
|
|
2199
|
+
'C5: Deep Scan CLI default profile should emit a Project Map artifact',
|
|
2200
|
+
);
|
|
2201
|
+
assert.ok(
|
|
2202
|
+
demoJson.artifactRefs.some((ref) => ref.type === 'deterministic_scan'),
|
|
2203
|
+
'C5: Deep Scan CLI default profile should emit a deterministic scan artifact',
|
|
2204
|
+
);
|
|
2205
|
+
assert.ok(
|
|
2206
|
+
demoJson.artifactRefs.some((ref) => ref.type === 'security_test_plan'),
|
|
2207
|
+
'C5: Deep Scan CLI default profile should emit a Security Test Plan artifact',
|
|
2208
|
+
);
|
|
2209
|
+
assert.ok(
|
|
2210
|
+
demoJson.artifactRefs.some((ref) => ref.type === 'agent_fix_tasks'),
|
|
2211
|
+
'C5: Deep Scan CLI default profile should emit a Ralph Agent Fix Task artifact',
|
|
2212
|
+
);
|
|
2213
|
+
assert.equal(
|
|
2214
|
+
demoJson.steps.find((step) => step.stepperId === 'ralph.compare')?.status,
|
|
2215
|
+
'skipped',
|
|
2216
|
+
'C5: First Deep Scan run should skip ralph.compare when no baseline run/scan ref is provided',
|
|
2217
|
+
);
|
|
2218
|
+
assert.equal(
|
|
2219
|
+
demoJson.steps.find((step) => step.stepperId === 'ralph.track')?.status,
|
|
2220
|
+
'skipped',
|
|
2221
|
+
'C5: First Deep Scan run should skip ralph.track when comparison is skipped',
|
|
2222
|
+
);
|
|
2223
|
+
assert.ok(
|
|
2224
|
+
demoJson.receiptRefs.some((ref) => ref.type === 'deterministic_scan' && ref.engine?.counts?.deterministic > 0),
|
|
2225
|
+
'C5: Deep Scan CLI default profile should expose deterministic scan receipt metadata',
|
|
2226
|
+
);
|
|
2227
|
+
const projectMapRef = demoJson.artifactRefs.find((ref) => ref.type === 'project_map');
|
|
2228
|
+
await fs.access(path.join(demoProject, projectMapRef.uri));
|
|
2229
|
+
const deterministicScanRef = demoJson.artifactRefs.find((ref) => ref.type === 'deterministic_scan');
|
|
2230
|
+
await fs.access(path.join(demoProject, deterministicScanRef.uri));
|
|
2231
|
+
const securityTestPlanRef = demoJson.artifactRefs.find((ref) => ref.type === 'security_test_plan');
|
|
2232
|
+
await fs.access(path.join(demoProject, securityTestPlanRef.uri));
|
|
2233
|
+
const agentFixTaskRef = demoJson.artifactRefs.find((ref) => ref.type === 'agent_fix_tasks');
|
|
2234
|
+
await fs.access(path.join(demoProject, agentFixTaskRef.uri));
|
|
2235
|
+
assert.equal(
|
|
2236
|
+
demoJson.steps.find((step) => step.stepperId === 'ralph.accept')?.status,
|
|
2237
|
+
'skipped',
|
|
2238
|
+
'C5: First Deep Scan run should skip ralph.accept when no accepted-risk register exists',
|
|
2239
|
+
);
|
|
2240
|
+
|
|
2241
|
+
const demoRerun = await execFileAsync(
|
|
2242
|
+
process.execPath,
|
|
2243
|
+
[cliPath, 'deep-scan-start', demoProject, '--demo=true', `--previous-run-id=${demoJson.runId}`],
|
|
2244
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2245
|
+
);
|
|
2246
|
+
const demoRerunOutput = `${demoRerun.stdout}\n${demoRerun.stderr}`;
|
|
2247
|
+
const demoRerunJson = JSON.parse(demoRerunOutput.slice(demoRerunOutput.indexOf('{')));
|
|
2248
|
+
assert.equal(
|
|
2249
|
+
demoRerunJson.steps.find((step) => step.stepperId === 'ralph.compare')?.status,
|
|
2250
|
+
'passed',
|
|
2251
|
+
'C5: Deep Scan rerun with --previous-run-id should execute ralph.compare',
|
|
2252
|
+
);
|
|
2253
|
+
assert.ok(
|
|
2254
|
+
demoRerunJson.artifactRefs.some((ref) => ref.type === 'ralph_comparison'),
|
|
2255
|
+
'C5: Deep Scan rerun with baseline should emit a ralph_comparison artifact',
|
|
2256
|
+
);
|
|
2257
|
+
const comparisonRef = demoRerunJson.artifactRefs.find((ref) => ref.type === 'ralph_comparison');
|
|
2258
|
+
await fs.access(path.join(demoProject, comparisonRef.uri));
|
|
2259
|
+
assert.equal(
|
|
2260
|
+
demoRerunJson.steps.find((step) => step.stepperId === 'ralph.track')?.status,
|
|
2261
|
+
'passed',
|
|
2262
|
+
'C5: Deep Scan rerun with baseline should execute ralph.track',
|
|
2263
|
+
);
|
|
2264
|
+
assert.ok(
|
|
2265
|
+
demoRerunJson.artifactRefs.some((ref) => ref.type === 'ralph_failure_state'),
|
|
2266
|
+
'C5: Deep Scan rerun with baseline should emit a ralph_failure_state artifact',
|
|
2267
|
+
);
|
|
2268
|
+
const failureStateRef = demoRerunJson.artifactRefs.find((ref) => ref.type === 'ralph_failure_state');
|
|
2269
|
+
await fs.access(path.join(demoProject, failureStateRef.uri));
|
|
2270
|
+
|
|
2271
|
+
const demoRerun2 = await execFileAsync(
|
|
2272
|
+
process.execPath,
|
|
2273
|
+
[cliPath, 'deep-scan-start', demoProject, '--demo=true', `--previous-run-id=${demoRerunJson.runId}`],
|
|
2274
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2275
|
+
);
|
|
2276
|
+
const demoRerun2Output = `${demoRerun2.stdout}\n${demoRerun2.stderr}`;
|
|
2277
|
+
const demoRerun2Json = JSON.parse(demoRerun2Output.slice(demoRerun2Output.indexOf('{')));
|
|
2278
|
+
assert.equal(
|
|
2279
|
+
demoRerun2Json.steps.find((step) => step.stepperId === 'ralph.track')?.status,
|
|
2280
|
+
'passed',
|
|
2281
|
+
'C5: Third Deep Scan rerun should carry prior failure state forward through --previous-run-id',
|
|
2282
|
+
);
|
|
2283
|
+
const failureStateText = await fs.readFile(path.join(demoProject, demoRerun2Json.artifactRefs.find((ref) => ref.type === 'ralph_failure_state').uri), 'utf8');
|
|
2284
|
+
const failureStateJson = JSON.parse(failureStateText);
|
|
2285
|
+
assert.ok(
|
|
2286
|
+
failureStateJson.summary.repeatedFailureCount >= 0,
|
|
2287
|
+
'C5: Carried-forward failure state should expose repeated failure summary counts',
|
|
2288
|
+
);
|
|
2289
|
+
|
|
2290
|
+
// VIB-85: record an accepted risk via CLI, then rerun so ralph.accept evaluates it.
|
|
2291
|
+
const acceptCli = await execFileAsync(
|
|
2292
|
+
process.execPath,
|
|
2293
|
+
[cliPath, 'deep-scan-accept-risk', demoProject, '--demo=true', '--severity=low', '--finding-id=finding-cli-selftest', '--reason=demo acceptance', '--reviewer=selftest'],
|
|
2294
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2295
|
+
);
|
|
2296
|
+
const acceptCliOutput = `${acceptCli.stdout}\n${acceptCli.stderr}`;
|
|
2297
|
+
const acceptCliJson = JSON.parse(acceptCliOutput.slice(acceptCliOutput.indexOf('{')));
|
|
2298
|
+
assert.match(acceptCliJson.record.riskId, /^risk_/, 'C5: deep-scan-accept-risk should persist an accepted-risk record with a generated riskId');
|
|
2299
|
+
await fs.access(path.join(demoProject, '.vibesecur/accepted-risks.json'));
|
|
2300
|
+
|
|
2301
|
+
const demoRerun3 = await execFileAsync(
|
|
2302
|
+
process.execPath,
|
|
2303
|
+
[cliPath, 'deep-scan-start', demoProject, '--demo=true', `--previous-run-id=${demoRerun2Json.runId}`],
|
|
2304
|
+
{ cwd: path.resolve('.'), env: cliEnv },
|
|
2305
|
+
);
|
|
2306
|
+
const demoRerun3Output = `${demoRerun3.stdout}\n${demoRerun3.stderr}`;
|
|
2307
|
+
const demoRerun3Json = JSON.parse(demoRerun3Output.slice(demoRerun3Output.indexOf('{')));
|
|
2308
|
+
assert.equal(
|
|
2309
|
+
demoRerun3Json.steps.find((step) => step.stepperId === 'ralph.accept')?.status,
|
|
2310
|
+
'passed',
|
|
2311
|
+
'C5: Deep Scan run with an accepted-risk register should execute ralph.accept',
|
|
2312
|
+
);
|
|
2313
|
+
assert.ok(
|
|
2314
|
+
demoRerun3Json.artifactRefs.some((ref) => ref.type === 'ralph_accepted_risk'),
|
|
2315
|
+
'C5: Deep Scan run with an accepted-risk register should emit a ralph_accepted_risk artifact',
|
|
2316
|
+
);
|
|
2317
|
+
const acceptedRiskRef = demoRerun3Json.artifactRefs.find((ref) => ref.type === 'ralph_accepted_risk');
|
|
2318
|
+
await fs.access(path.join(demoProject, acceptedRiskRef.uri));
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
await import(pathToFileURL(path.resolve('./src/index.js')).href);
|
|
2322
|
+
|
|
2323
|
+
// ── runScan unit tests ──────────────────────────────────────────────────
|
|
2324
|
+
// We test runScan() by controlling VIBESECUR_API_BASE. When it is unset,
|
|
2325
|
+
// postRemoteLocalScan returns { skipped: true } which exercises case 1.
|
|
2326
|
+
// For the remaining cases we build a minimal inline test harness that
|
|
2327
|
+
// calls runScan with a code sample that triggers local engine rules,
|
|
2328
|
+
// verifying the output contracts defined in orchestrator/runScan.js.
|
|
2329
|
+
|
|
2330
|
+
const savedApiBase = process.env.VIBESECUR_API_BASE;
|
|
2331
|
+
|
|
2332
|
+
// ── Test R1: REMOTE_VERIFICATION_REQUIRED when API base is not set ──────
|
|
2333
|
+
delete process.env.VIBESECUR_API_BASE;
|
|
2334
|
+
{
|
|
2335
|
+
// Re-import with cache-busting is not needed here: runScan re-reads
|
|
2336
|
+
// process.env.VIBESECUR_API_BASE at call time via postRemoteLocalScan
|
|
2337
|
+
// → normalizeApiBase → which returns '' when env is unset.
|
|
2338
|
+
const { runScan } = await import(
|
|
2339
|
+
pathToFileURL(path.resolve('./src/orchestrator/runScan.js')).href
|
|
2340
|
+
);
|
|
2341
|
+
const outcome = await runScan({
|
|
2342
|
+
code: 'const x = 1;',
|
|
2343
|
+
lang: 'js',
|
|
2344
|
+
projectRoot: projectRoot,
|
|
2345
|
+
installToken: 'a'.repeat(64),
|
|
2346
|
+
lockedRootHash: undefined,
|
|
2347
|
+
engineVersion: '0.0.0-test',
|
|
2348
|
+
});
|
|
2349
|
+
assert.equal(outcome.ok, false, 'R1: outcome.ok must be false when API base unset');
|
|
2350
|
+
assert.equal(
|
|
2351
|
+
outcome.errorCode,
|
|
2352
|
+
'REMOTE_VERIFICATION_REQUIRED',
|
|
2353
|
+
'R1: errorCode must be REMOTE_VERIFICATION_REQUIRED',
|
|
2354
|
+
);
|
|
2355
|
+
assert.ok(
|
|
2356
|
+
typeof outcome.errorPayload.message === 'string',
|
|
2357
|
+
'R1: errorPayload.message must be a string',
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// ── Test R4: offline fallback returns local engine findings ─────────────
|
|
2362
|
+
// Still no API base — postRemoteLocalScan skips → runScan case 1 fires.
|
|
2363
|
+
// To reach the local fallback (case 4) we need a 2xx-with-no-data scenario.
|
|
2364
|
+
// We can verify the local engine branch by pointing to a URL that returns
|
|
2365
|
+
// 2xx with an empty JSON body. However, that requires a live server.
|
|
2366
|
+
// Instead we verify that local rule engine fires correctly when called
|
|
2367
|
+
// directly, and that runScan in offline (skipped) mode surfaces a clear
|
|
2368
|
+
// REMOTE_VERIFICATION_REQUIRED (not a silent empty result).
|
|
2369
|
+
{
|
|
2370
|
+
const { runScan } = await import(
|
|
2371
|
+
pathToFileURL(path.resolve('./src/orchestrator/runScan.js')).href
|
|
2372
|
+
);
|
|
2373
|
+
const evalCode = 'const danger = eval("1+1");';
|
|
2374
|
+
const outcome = await runScan({
|
|
2375
|
+
code: evalCode,
|
|
2376
|
+
lang: 'js',
|
|
2377
|
+
projectRoot: projectRoot,
|
|
2378
|
+
installToken: 'a'.repeat(64),
|
|
2379
|
+
lockedRootHash: undefined,
|
|
2380
|
+
engineVersion: '0.0.0-test',
|
|
2381
|
+
});
|
|
2382
|
+
// When API base is unset, we expect the hard error (not silent local).
|
|
2383
|
+
assert.equal(outcome.ok, false, 'R4-pre: must surface error when API base unset, not silently fall back');
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// Restore env for any subsequent tests
|
|
2387
|
+
if (savedApiBase !== undefined) {
|
|
2388
|
+
process.env.VIBESECUR_API_BASE = savedApiBase;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// ── Test R2 / R3 (structural): verify ScanOutcome shape contracts ───────
|
|
2392
|
+
// We import localScan directly and verify the shape that runScan would
|
|
2393
|
+
// return in mode='offline' matches what tool handlers expect.
|
|
2394
|
+
{
|
|
2395
|
+
const { localScan } = await import(
|
|
2396
|
+
pathToFileURL(path.resolve('./src/rule-engine/index.js')).href
|
|
2397
|
+
);
|
|
2398
|
+
const result = localScan('const x = eval("bad")', 'js');
|
|
2399
|
+
assert.ok(typeof result.score === 'number', 'R2: local result.score must be number');
|
|
2400
|
+
assert.ok(typeof result.grade === 'string', 'R2: local result.grade must be string');
|
|
2401
|
+
assert.ok(typeof result.verdict === 'string', 'R2: local result.verdict must be string');
|
|
2402
|
+
assert.ok(Array.isArray(result.findings), 'R2: local result.findings must be array');
|
|
2403
|
+
assert.ok(result.findings.length > 0, 'R2: eval() must trigger at least one finding');
|
|
2404
|
+
// Verify the offline enrichment shape that the localScan tool handler builds
|
|
2405
|
+
const engineVersion = '0.0.0-test';
|
|
2406
|
+
const humanSummary = `${result.verdict} Score ${result.score} (${result.grade}) - ${result.findings.length} finding(s).`;
|
|
2407
|
+
const enriched = { ...result, humanSummary, engineVersion, mode: 'offline' };
|
|
2408
|
+
assert.equal(enriched.mode, 'offline', 'R2: enriched.mode must be offline');
|
|
2409
|
+
assert.equal(enriched.engineVersion, engineVersion, 'R2: enriched.engineVersion must match');
|
|
2410
|
+
assert.ok(enriched.humanSummary.includes('finding(s)'), 'R2: humanSummary format valid');
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// ── Governance middleware unit tests ────────────────────────────────────
|
|
2414
|
+
|
|
2415
|
+
{
|
|
2416
|
+
const { buildGovernanceContext, evaluateGovernance } = await import(
|
|
2417
|
+
pathToFileURL(path.resolve('./src/middleware/governance.js')).href
|
|
2418
|
+
);
|
|
2419
|
+
|
|
2420
|
+
// G1: buildGovernanceContext returns correct shape
|
|
2421
|
+
const ctx = buildGovernanceContext({
|
|
2422
|
+
toolName: 'localScan',
|
|
2423
|
+
requestedPath: projectRoot,
|
|
2424
|
+
installToken: 'a'.repeat(64),
|
|
2425
|
+
lockedRootHash: 'b'.repeat(64),
|
|
2426
|
+
lang: 'js',
|
|
2427
|
+
codeSize: 42,
|
|
2428
|
+
maxFiles: undefined,
|
|
2429
|
+
});
|
|
2430
|
+
assert.equal(ctx.toolName, 'localScan', 'G1: toolName must be set');
|
|
2431
|
+
assert.equal(ctx.requestedPath, projectRoot, 'G1: requestedPath must match');
|
|
2432
|
+
assert.equal(ctx.scanMetadata.lang, 'js', 'G1: lang must be in scanMetadata');
|
|
2433
|
+
assert.equal(ctx.scanMetadata.codeSize, 42, 'G1: codeSize must be in scanMetadata');
|
|
2434
|
+
assert.equal(ctx.scanMetadata.maxFiles, undefined, 'G1: maxFiles undefined for code-string tool');
|
|
2435
|
+
assert.equal(ctx.executionMode, 'unknown', 'G1: executionMode starts as unknown');
|
|
2436
|
+
assert.ok(typeof ctx.sessionId === 'string' && ctx.sessionId.length > 0, 'G1: sessionId must be non-empty string');
|
|
2437
|
+
|
|
2438
|
+
// G2: evaluateGovernance Phase 1 always returns allowed:true (pass-through)
|
|
2439
|
+
const decision = await evaluateGovernance(ctx);
|
|
2440
|
+
assert.equal(decision.allowed, true, 'G2: Phase 1 governance must allow all requests');
|
|
2441
|
+
assert.equal(typeof decision.reason, 'string', 'G2: reason must be a string');
|
|
2442
|
+
assert.ok(typeof decision.annotations === 'object', 'G2: annotations must be an object');
|
|
2443
|
+
assert.ok(typeof decision.metadata === 'object', 'G2: metadata must be an object');
|
|
2444
|
+
|
|
2445
|
+
// G3: evaluateGovernance does not mutate the context
|
|
2446
|
+
const ctxCopy = JSON.stringify(ctx);
|
|
2447
|
+
await evaluateGovernance(ctx);
|
|
2448
|
+
assert.equal(JSON.stringify(ctx), ctxCopy, 'G3: governance must not mutate GovernanceContext');
|
|
2449
|
+
|
|
2450
|
+
// G4: evaluateGovernance is callable with minimal context without throwing
|
|
2451
|
+
const minCtx = buildGovernanceContext({
|
|
2452
|
+
toolName: 'health',
|
|
2453
|
+
requestedPath: '.',
|
|
2454
|
+
installToken: '',
|
|
2455
|
+
lockedRootHash: '',
|
|
2456
|
+
lang: 'auto',
|
|
2457
|
+
codeSize: 0,
|
|
2458
|
+
maxFiles: undefined,
|
|
2459
|
+
});
|
|
2460
|
+
const minDecision = await evaluateGovernance(minCtx);
|
|
2461
|
+
assert.equal(minDecision.allowed, true, 'G4: minimal context must still pass governance');
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
console.log('mcp selftest passed');
|
|
2465
|
+
} finally {
|
|
2466
|
+
await fs.rm(tmpBase, { recursive: true, force: true });
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
run().catch((err) => {
|
|
2471
|
+
console.error(`mcp selftest failed: ${err.message}`);
|
|
2472
|
+
process.exit(1);
|
|
2473
|
+
});
|