@syntesseraai/opencode-feature-factory 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/discovery.js +19 -16
- package/dist/discovery.test.js +2 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +10 -6
- package/dist/output.js +6 -2
- package/dist/output.test.js +30 -28
- package/dist/quality-gate-config.js +14 -6
- package/dist/quality-gate-config.test.js +24 -22
- package/dist/stop-quality-gate.d.ts +1 -1
- package/dist/stop-quality-gate.js +11 -5
- package/dist/stop-quality-gate.test.js +84 -82
- package/dist/types.js +2 -1
- package/package.json +2 -11
package/dist/discovery.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCommands = resolveCommands;
|
|
4
|
+
const quality_gate_config_1 = require("./quality-gate-config");
|
|
2
5
|
// ============================================================================
|
|
3
6
|
// Package Manager Detection
|
|
4
7
|
// ============================================================================
|
|
@@ -11,15 +14,15 @@ async function detectPackageManager($, directory, override) {
|
|
|
11
14
|
return override;
|
|
12
15
|
}
|
|
13
16
|
// Priority order: pnpm > bun > yarn > npm
|
|
14
|
-
if (await fileExists($, `${directory}/pnpm-lock.yaml`))
|
|
17
|
+
if (await (0, quality_gate_config_1.fileExists)($, `${directory}/pnpm-lock.yaml`))
|
|
15
18
|
return 'pnpm';
|
|
16
|
-
if (await fileExists($, `${directory}/bun.lockb`))
|
|
19
|
+
if (await (0, quality_gate_config_1.fileExists)($, `${directory}/bun.lockb`))
|
|
17
20
|
return 'bun';
|
|
18
|
-
if (await fileExists($, `${directory}/bun.lock`))
|
|
21
|
+
if (await (0, quality_gate_config_1.fileExists)($, `${directory}/bun.lock`))
|
|
19
22
|
return 'bun';
|
|
20
|
-
if (await fileExists($, `${directory}/yarn.lock`))
|
|
23
|
+
if (await (0, quality_gate_config_1.fileExists)($, `${directory}/yarn.lock`))
|
|
21
24
|
return 'yarn';
|
|
22
|
-
if (await fileExists($, `${directory}/package-lock.json`))
|
|
25
|
+
if (await (0, quality_gate_config_1.fileExists)($, `${directory}/package-lock.json`))
|
|
23
26
|
return 'npm';
|
|
24
27
|
return 'npm'; // fallback
|
|
25
28
|
}
|
|
@@ -45,7 +48,7 @@ function buildRunCommand(pm, script) {
|
|
|
45
48
|
* Discover Node.js lint/build/test commands from package.json scripts
|
|
46
49
|
*/
|
|
47
50
|
async function discoverNodeCommands($, directory, pm) {
|
|
48
|
-
const pkgJson = await readJsonFile($, `${directory}/package.json`);
|
|
51
|
+
const pkgJson = await (0, quality_gate_config_1.readJsonFile)($, `${directory}/package.json`);
|
|
49
52
|
if (!pkgJson?.scripts)
|
|
50
53
|
return [];
|
|
51
54
|
const scripts = pkgJson.scripts;
|
|
@@ -74,7 +77,7 @@ async function discoverNodeCommands($, directory, pm) {
|
|
|
74
77
|
* Discover Rust commands from Cargo.toml presence
|
|
75
78
|
*/
|
|
76
79
|
async function discoverRustCommands($, directory, includeClippy) {
|
|
77
|
-
if (!(await fileExists($, `${directory}/Cargo.toml`)))
|
|
80
|
+
if (!(await (0, quality_gate_config_1.fileExists)($, `${directory}/Cargo.toml`)))
|
|
78
81
|
return [];
|
|
79
82
|
const steps = [{ step: 'lint (fmt)', cmd: 'cargo fmt --check' }];
|
|
80
83
|
if (includeClippy) {
|
|
@@ -94,7 +97,7 @@ async function discoverRustCommands($, directory, includeClippy) {
|
|
|
94
97
|
* Discover Go commands from go.mod presence
|
|
95
98
|
*/
|
|
96
99
|
async function discoverGoCommands($, directory) {
|
|
97
|
-
if (!(await fileExists($, `${directory}/go.mod`)))
|
|
100
|
+
if (!(await (0, quality_gate_config_1.fileExists)($, `${directory}/go.mod`)))
|
|
98
101
|
return [];
|
|
99
102
|
return [{ step: 'test', cmd: 'go test ./...' }];
|
|
100
103
|
}
|
|
@@ -106,8 +109,8 @@ async function discoverGoCommands($, directory) {
|
|
|
106
109
|
*/
|
|
107
110
|
async function discoverPythonCommands($, directory) {
|
|
108
111
|
// Only add pytest if we have strong signal
|
|
109
|
-
const hasPytestIni = await fileExists($, `${directory}/pytest.ini`);
|
|
110
|
-
const hasPyproject = await fileExists($, `${directory}/pyproject.toml`);
|
|
112
|
+
const hasPytestIni = await (0, quality_gate_config_1.fileExists)($, `${directory}/pytest.ini`);
|
|
113
|
+
const hasPyproject = await (0, quality_gate_config_1.fileExists)($, `${directory}/pyproject.toml`);
|
|
111
114
|
if (!hasPytestIni && !hasPyproject)
|
|
112
115
|
return [];
|
|
113
116
|
// pytest.ini is strong signal
|
|
@@ -142,11 +145,11 @@ async function discoverPythonCommands($, directory) {
|
|
|
142
145
|
*
|
|
143
146
|
* @returns Array of command steps to execute, or empty if nothing found
|
|
144
147
|
*/
|
|
145
|
-
|
|
148
|
+
async function resolveCommands(args) {
|
|
146
149
|
const { $, directory, config } = args;
|
|
147
|
-
const mergedConfig = { ...DEFAULT_QUALITY_GATE, ...config };
|
|
150
|
+
const mergedConfig = { ...quality_gate_config_1.DEFAULT_QUALITY_GATE, ...config };
|
|
148
151
|
// 1. Configured commands take priority (do NOT run ci.sh if these exist)
|
|
149
|
-
if (hasConfiguredCommands(config)) {
|
|
152
|
+
if ((0, quality_gate_config_1.hasConfiguredCommands)(config)) {
|
|
150
153
|
const steps = [];
|
|
151
154
|
const order = mergedConfig.steps;
|
|
152
155
|
for (const stepName of order) {
|
|
@@ -160,14 +163,14 @@ export async function resolveCommands(args) {
|
|
|
160
163
|
// 2. Feature Factory CI script (only if no configured commands)
|
|
161
164
|
if (mergedConfig.useCiSh !== 'never') {
|
|
162
165
|
const ciShPath = `${directory}/management/ci.sh`;
|
|
163
|
-
if (await fileExists($, ciShPath)) {
|
|
166
|
+
if (await (0, quality_gate_config_1.fileExists)($, ciShPath)) {
|
|
164
167
|
return [{ step: 'ci', cmd: `bash ${ciShPath}` }];
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
170
|
// 3. Conventional discovery (only if no ci.sh)
|
|
168
171
|
const pm = await detectPackageManager($, directory, mergedConfig.packageManager);
|
|
169
172
|
// Try Node first (most common)
|
|
170
|
-
if (await fileExists($, `${directory}/package.json`)) {
|
|
173
|
+
if (await (0, quality_gate_config_1.fileExists)($, `${directory}/package.json`)) {
|
|
171
174
|
const nodeSteps = await discoverNodeCommands($, directory, pm);
|
|
172
175
|
if (nodeSteps.length > 0)
|
|
173
176
|
return nodeSteps;
|
package/dist/discovery.test.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for discovery module
|
|
3
4
|
*
|
|
@@ -7,6 +8,7 @@
|
|
|
7
8
|
* Note: Most functions in discovery.ts require shell access ($) so they're
|
|
8
9
|
* integration-tested elsewhere. This file tests the pure utility functions.
|
|
9
10
|
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
12
|
// Re-implement buildRunCommand for testing since it's not exported
|
|
11
13
|
function buildRunCommand(pm, script) {
|
|
12
14
|
switch (pm) {
|
|
@@ -92,4 +94,3 @@ describe('PackageManager type', () => {
|
|
|
92
94
|
});
|
|
93
95
|
});
|
|
94
96
|
});
|
|
95
|
-
export {};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StopQualityGatePlugin = exports.AGENT_PATHS = exports.SKILL_PATHS = void 0;
|
|
4
|
+
const stop_quality_gate_1 = require("./stop-quality-gate");
|
|
2
5
|
// Export skill and agent paths for programmatic access
|
|
3
|
-
|
|
6
|
+
exports.SKILL_PATHS = {
|
|
4
7
|
'ff-mini-plan': './skills/ff-mini-plan/SKILL.md',
|
|
5
8
|
'ff-todo-management': './skills/ff-todo-management/SKILL.md',
|
|
6
9
|
'ff-severity-classification': './skills/ff-severity-classification/SKILL.md',
|
|
7
10
|
'ff-report-templates': './skills/ff-report-templates/SKILL.md',
|
|
8
11
|
};
|
|
9
|
-
|
|
12
|
+
exports.AGENT_PATHS = {
|
|
10
13
|
'ff-acceptance': './agents/ff-acceptance.md',
|
|
11
14
|
'ff-review': './agents/ff-review.md',
|
|
12
15
|
'ff-security': './agents/ff-security.md',
|
|
@@ -56,7 +59,7 @@ function resolveRootDir(input) {
|
|
|
56
59
|
* - On failure: passes full CI output to LLM for fix instructions
|
|
57
60
|
* - If management/ci.sh does not exist, quality gate does not run
|
|
58
61
|
*/
|
|
59
|
-
|
|
62
|
+
const StopQualityGatePlugin = async (input) => {
|
|
60
63
|
const { worktree, directory, client } = input;
|
|
61
64
|
const rootDir = resolveRootDir({ worktree, directory });
|
|
62
65
|
// Skip quality gate if no valid directory (e.g., global config with no project)
|
|
@@ -66,7 +69,7 @@ export const StopQualityGatePlugin = async (input) => {
|
|
|
66
69
|
// Create quality gate hooks
|
|
67
70
|
let qualityGateHooks = {};
|
|
68
71
|
try {
|
|
69
|
-
qualityGateHooks = await createQualityGateHooks(input);
|
|
72
|
+
qualityGateHooks = await (0, stop_quality_gate_1.createQualityGateHooks)(input);
|
|
70
73
|
}
|
|
71
74
|
catch (error) {
|
|
72
75
|
await log(client, 'error', 'quality-gate.init-error', {
|
|
@@ -77,5 +80,6 @@ export const StopQualityGatePlugin = async (input) => {
|
|
|
77
80
|
...qualityGateHooks,
|
|
78
81
|
};
|
|
79
82
|
};
|
|
83
|
+
exports.StopQualityGatePlugin = StopQualityGatePlugin;
|
|
80
84
|
// Default export for OpenCode plugin discovery
|
|
81
|
-
|
|
85
|
+
exports.default = exports.StopQualityGatePlugin;
|
package/dist/output.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractErrorLines = extractErrorLines;
|
|
4
|
+
exports.tailLines = tailLines;
|
|
1
5
|
/**
|
|
2
6
|
* Regex pattern for detecting error-like lines in command output
|
|
3
7
|
*/
|
|
@@ -10,7 +14,7 @@ const ERROR_PATTERNS = /(error|failed|failure|panic|assert|exception|traceback|T
|
|
|
10
14
|
* @param maxLines - Maximum number of error lines to extract
|
|
11
15
|
* @returns Array of error-like lines (up to maxLines)
|
|
12
16
|
*/
|
|
13
|
-
|
|
17
|
+
function extractErrorLines(output, maxLines) {
|
|
14
18
|
const lines = output.split('\n');
|
|
15
19
|
const errorLines = [];
|
|
16
20
|
for (let i = 0; i < lines.length && errorLines.length < maxLines; i++) {
|
|
@@ -37,7 +41,7 @@ export function extractErrorLines(output, maxLines) {
|
|
|
37
41
|
* @param maxLines - Maximum number of lines to return
|
|
38
42
|
* @returns Truncated output with notice, or full output if short enough
|
|
39
43
|
*/
|
|
40
|
-
|
|
44
|
+
function tailLines(output, maxLines) {
|
|
41
45
|
const lines = output.split('\n');
|
|
42
46
|
if (lines.length <= maxLines) {
|
|
43
47
|
return output;
|
package/dist/output.test.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for output module
|
|
3
4
|
*
|
|
@@ -5,10 +6,11 @@
|
|
|
5
6
|
* - extractErrorLines: extracts error-like lines from command output
|
|
6
7
|
* - tailLines: returns last N lines with truncation notice
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const output_1 = require("./output");
|
|
9
11
|
describe('extractErrorLines', () => {
|
|
10
12
|
it('should return empty array for empty input', () => {
|
|
11
|
-
expect(extractErrorLines('', 10)).toEqual([]);
|
|
13
|
+
expect((0, output_1.extractErrorLines)('', 10)).toEqual([]);
|
|
12
14
|
});
|
|
13
15
|
it('should return empty array when no errors found', () => {
|
|
14
16
|
const output = `
|
|
@@ -16,7 +18,7 @@ Build started
|
|
|
16
18
|
Compiling modules...
|
|
17
19
|
Build completed successfully
|
|
18
20
|
`;
|
|
19
|
-
expect(extractErrorLines(output, 10)).toEqual([]);
|
|
21
|
+
expect((0, output_1.extractErrorLines)(output, 10)).toEqual([]);
|
|
20
22
|
});
|
|
21
23
|
it('should extract lines containing "error"', () => {
|
|
22
24
|
const output = `
|
|
@@ -24,7 +26,7 @@ Starting build
|
|
|
24
26
|
error: Cannot find module 'missing'
|
|
25
27
|
Build failed
|
|
26
28
|
`;
|
|
27
|
-
const result = extractErrorLines(output, 10);
|
|
29
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
28
30
|
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
29
31
|
});
|
|
30
32
|
it('should extract lines containing "Error" (case insensitive)', () => {
|
|
@@ -33,7 +35,7 @@ Running tests
|
|
|
33
35
|
TypeError: undefined is not a function
|
|
34
36
|
at test.js:10:5
|
|
35
37
|
`;
|
|
36
|
-
const result = extractErrorLines(output, 10);
|
|
38
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
37
39
|
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
38
40
|
});
|
|
39
41
|
it('should extract lines containing "failed"', () => {
|
|
@@ -42,7 +44,7 @@ Test suite: MyTests
|
|
|
42
44
|
Test failed: should work correctly
|
|
43
45
|
1 test failed
|
|
44
46
|
`;
|
|
45
|
-
const result = extractErrorLines(output, 10);
|
|
47
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
46
48
|
expect(result.some((line) => line.toLowerCase().includes('failed'))).toBe(true);
|
|
47
49
|
});
|
|
48
50
|
it('should extract lines containing "FAILED"', () => {
|
|
@@ -50,7 +52,7 @@ Test failed: should work correctly
|
|
|
50
52
|
Running: test_function
|
|
51
53
|
FAILED test_function - assertion error
|
|
52
54
|
`;
|
|
53
|
-
const result = extractErrorLines(output, 10);
|
|
55
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
54
56
|
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
55
57
|
});
|
|
56
58
|
it('should extract lines containing "panic"', () => {
|
|
@@ -58,7 +60,7 @@ FAILED test_function - assertion error
|
|
|
58
60
|
thread 'main' panicked at 'assertion failed'
|
|
59
61
|
note: run with RUST_BACKTRACE=1
|
|
60
62
|
`;
|
|
61
|
-
const result = extractErrorLines(output, 10);
|
|
63
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
62
64
|
expect(result.some((line) => line.includes('panic'))).toBe(true);
|
|
63
65
|
});
|
|
64
66
|
it('should extract lines containing "exception"', () => {
|
|
@@ -66,7 +68,7 @@ note: run with RUST_BACKTRACE=1
|
|
|
66
68
|
Exception in thread "main" java.lang.NullPointerException
|
|
67
69
|
at Main.main(Main.java:5)
|
|
68
70
|
`;
|
|
69
|
-
const result = extractErrorLines(output, 10);
|
|
71
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
70
72
|
expect(result.some((line) => line.includes('Exception'))).toBe(true);
|
|
71
73
|
});
|
|
72
74
|
it('should extract lines containing "traceback"', () => {
|
|
@@ -75,7 +77,7 @@ Traceback (most recent call last):
|
|
|
75
77
|
File "test.py", line 10
|
|
76
78
|
NameError: name 'x' is not defined
|
|
77
79
|
`;
|
|
78
|
-
const result = extractErrorLines(output, 10);
|
|
80
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
79
81
|
expect(result.some((line) => line.includes('Traceback'))).toBe(true);
|
|
80
82
|
});
|
|
81
83
|
it('should include 2 lines of context after error', () => {
|
|
@@ -84,7 +86,7 @@ error: something went wrong
|
|
|
84
86
|
context line 1
|
|
85
87
|
context line 2
|
|
86
88
|
line5`;
|
|
87
|
-
const result = extractErrorLines(output, 10);
|
|
89
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
88
90
|
expect(result).toContain('error: something went wrong');
|
|
89
91
|
expect(result).toContain('context line 1');
|
|
90
92
|
expect(result).toContain('context line 2');
|
|
@@ -101,36 +103,36 @@ error: third error
|
|
|
101
103
|
context5
|
|
102
104
|
context6
|
|
103
105
|
`;
|
|
104
|
-
const result = extractErrorLines(output, 5);
|
|
106
|
+
const result = (0, output_1.extractErrorLines)(output, 5);
|
|
105
107
|
expect(result.length).toBeLessThanOrEqual(5);
|
|
106
108
|
});
|
|
107
109
|
it('should handle error at end of output without enough context lines', () => {
|
|
108
110
|
const output = `line1
|
|
109
111
|
line2
|
|
110
112
|
error: final error`;
|
|
111
|
-
const result = extractErrorLines(output, 10);
|
|
113
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
112
114
|
expect(result).toContain('error: final error');
|
|
113
115
|
});
|
|
114
116
|
it('should extract ReferenceError', () => {
|
|
115
117
|
const output = `ReferenceError: x is not defined`;
|
|
116
|
-
const result = extractErrorLines(output, 10);
|
|
118
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
117
119
|
expect(result).toContain('ReferenceError: x is not defined');
|
|
118
120
|
});
|
|
119
121
|
it('should extract SyntaxError', () => {
|
|
120
122
|
const output = `SyntaxError: Unexpected token '}'`;
|
|
121
|
-
const result = extractErrorLines(output, 10);
|
|
123
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
122
124
|
expect(result).toContain("SyntaxError: Unexpected token '}'");
|
|
123
125
|
});
|
|
124
126
|
it('should extract ERR! (npm style errors)', () => {
|
|
125
127
|
const output = `npm ERR! code ENOENT
|
|
126
128
|
npm ERR! path /app/package.json`;
|
|
127
|
-
const result = extractErrorLines(output, 10);
|
|
129
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
128
130
|
expect(result.some((line) => line.includes('ERR!'))).toBe(true);
|
|
129
131
|
});
|
|
130
132
|
it('should extract FAIL (test runner style)', () => {
|
|
131
133
|
const output = `FAIL src/test.ts
|
|
132
134
|
Test suite failed to run`;
|
|
133
|
-
const result = extractErrorLines(output, 10);
|
|
135
|
+
const result = (0, output_1.extractErrorLines)(output, 10);
|
|
134
136
|
expect(result.some((line) => line.includes('FAIL'))).toBe(true);
|
|
135
137
|
});
|
|
136
138
|
it('should extract multiple error types in same output', () => {
|
|
@@ -141,7 +143,7 @@ TypeError: Cannot read property 'x'
|
|
|
141
143
|
FAILED: test_something
|
|
142
144
|
Build process ended
|
|
143
145
|
`;
|
|
144
|
-
const result = extractErrorLines(output, 20);
|
|
146
|
+
const result = (0, output_1.extractErrorLines)(output, 20);
|
|
145
147
|
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
146
148
|
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
147
149
|
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
@@ -150,15 +152,15 @@ Build process ended
|
|
|
150
152
|
describe('tailLines', () => {
|
|
151
153
|
it('should return full output when lines count is less than maxLines', () => {
|
|
152
154
|
const output = 'line1\nline2\nline3';
|
|
153
|
-
expect(tailLines(output, 10)).toBe(output);
|
|
155
|
+
expect((0, output_1.tailLines)(output, 10)).toBe(output);
|
|
154
156
|
});
|
|
155
157
|
it('should return full output when lines count equals maxLines', () => {
|
|
156
158
|
const output = 'line1\nline2\nline3';
|
|
157
|
-
expect(tailLines(output, 3)).toBe(output);
|
|
159
|
+
expect((0, output_1.tailLines)(output, 3)).toBe(output);
|
|
158
160
|
});
|
|
159
161
|
it('should truncate and add notice when output exceeds maxLines', () => {
|
|
160
162
|
const output = 'line1\nline2\nline3\nline4\nline5';
|
|
161
|
-
const result = tailLines(output, 3);
|
|
163
|
+
const result = (0, output_1.tailLines)(output, 3);
|
|
162
164
|
expect(result).toContain('... (2 lines truncated)');
|
|
163
165
|
expect(result).toContain('line3');
|
|
164
166
|
expect(result).toContain('line4');
|
|
@@ -168,38 +170,38 @@ describe('tailLines', () => {
|
|
|
168
170
|
});
|
|
169
171
|
it('should handle single line output', () => {
|
|
170
172
|
const output = 'single line';
|
|
171
|
-
expect(tailLines(output, 5)).toBe('single line');
|
|
173
|
+
expect((0, output_1.tailLines)(output, 5)).toBe('single line');
|
|
172
174
|
});
|
|
173
175
|
it('should handle empty output', () => {
|
|
174
|
-
expect(tailLines('', 5)).toBe('');
|
|
176
|
+
expect((0, output_1.tailLines)('', 5)).toBe('');
|
|
175
177
|
});
|
|
176
178
|
it('should handle maxLines of 1', () => {
|
|
177
179
|
const output = 'line1\nline2\nline3';
|
|
178
|
-
const result = tailLines(output, 1);
|
|
180
|
+
const result = (0, output_1.tailLines)(output, 1);
|
|
179
181
|
expect(result).toContain('... (2 lines truncated)');
|
|
180
182
|
expect(result).toContain('line3');
|
|
181
183
|
});
|
|
182
184
|
it('should correctly count truncated lines', () => {
|
|
183
185
|
const lines = Array.from({ length: 100 }, (_, i) => `line${i + 1}`);
|
|
184
186
|
const output = lines.join('\n');
|
|
185
|
-
const result = tailLines(output, 10);
|
|
187
|
+
const result = (0, output_1.tailLines)(output, 10);
|
|
186
188
|
expect(result).toContain('... (90 lines truncated)');
|
|
187
189
|
});
|
|
188
190
|
it('should preserve line content in tail', () => {
|
|
189
191
|
const output = 'first\nsecond\nthird\nfourth\nfifth';
|
|
190
|
-
const result = tailLines(output, 2);
|
|
192
|
+
const result = (0, output_1.tailLines)(output, 2);
|
|
191
193
|
const resultLines = result.split('\n');
|
|
192
194
|
expect(resultLines[resultLines.length - 1]).toBe('fifth');
|
|
193
195
|
expect(resultLines[resultLines.length - 2]).toBe('fourth');
|
|
194
196
|
});
|
|
195
197
|
it('should handle output with empty lines', () => {
|
|
196
198
|
const output = 'line1\n\nline3\n\nline5';
|
|
197
|
-
const result = tailLines(output, 3);
|
|
199
|
+
const result = (0, output_1.tailLines)(output, 3);
|
|
198
200
|
expect(result).toContain('... (2 lines truncated)');
|
|
199
201
|
});
|
|
200
202
|
it('should handle output with only newlines', () => {
|
|
201
203
|
const output = '\n\n\n\n';
|
|
202
|
-
const result = tailLines(output, 2);
|
|
204
|
+
const result = (0, output_1.tailLines)(output, 2);
|
|
203
205
|
expect(result).toContain('... (3 lines truncated)');
|
|
204
206
|
});
|
|
205
207
|
});
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_QUALITY_GATE = void 0;
|
|
4
|
+
exports.readJsonFile = readJsonFile;
|
|
5
|
+
exports.fileExists = fileExists;
|
|
6
|
+
exports.mergeQualityGateConfig = mergeQualityGateConfig;
|
|
7
|
+
exports.loadQualityGateConfig = loadQualityGateConfig;
|
|
8
|
+
exports.hasConfiguredCommands = hasConfiguredCommands;
|
|
1
9
|
/**
|
|
2
10
|
* Default configuration values for StopQualityGate
|
|
3
11
|
*/
|
|
4
|
-
|
|
12
|
+
exports.DEFAULT_QUALITY_GATE = {
|
|
5
13
|
steps: ['lint', 'build', 'test'],
|
|
6
14
|
useCiSh: 'auto',
|
|
7
15
|
packageManager: 'auto',
|
|
@@ -15,7 +23,7 @@ export const DEFAULT_QUALITY_GATE = {
|
|
|
15
23
|
/**
|
|
16
24
|
* Read a JSON file safely, returning null if not found or invalid
|
|
17
25
|
*/
|
|
18
|
-
|
|
26
|
+
async function readJsonFile($, path) {
|
|
19
27
|
try {
|
|
20
28
|
const result = await $ `cat ${path}`.quiet();
|
|
21
29
|
return JSON.parse(result.text());
|
|
@@ -27,7 +35,7 @@ export async function readJsonFile($, path) {
|
|
|
27
35
|
/**
|
|
28
36
|
* Check if a file exists
|
|
29
37
|
*/
|
|
30
|
-
|
|
38
|
+
async function fileExists($, path) {
|
|
31
39
|
try {
|
|
32
40
|
await $ `test -f ${path}`.quiet();
|
|
33
41
|
return true;
|
|
@@ -42,7 +50,7 @@ export async function fileExists($, path) {
|
|
|
42
50
|
* Precedence: `.opencode/opencode.json` overrides `opencode.json` (root).
|
|
43
51
|
* Deep-merges nested objects like `include`.
|
|
44
52
|
*/
|
|
45
|
-
|
|
53
|
+
function mergeQualityGateConfig(root, dotOpencode) {
|
|
46
54
|
const base = root ?? {};
|
|
47
55
|
const override = dotOpencode ?? {};
|
|
48
56
|
return {
|
|
@@ -64,7 +72,7 @@ export function mergeQualityGateConfig(root, dotOpencode) {
|
|
|
64
72
|
*
|
|
65
73
|
* Merges ONLY the `qualityGate` key. `.opencode/opencode.json` takes precedence.
|
|
66
74
|
*/
|
|
67
|
-
|
|
75
|
+
async function loadQualityGateConfig($, directory) {
|
|
68
76
|
const rootConfigPath = `${directory}/opencode.json`;
|
|
69
77
|
const dotOpencodeConfigPath = `${directory}/.opencode/opencode.json`;
|
|
70
78
|
const [rootJson, dotOpencodeJson] = await Promise.all([
|
|
@@ -79,6 +87,6 @@ export async function loadQualityGateConfig($, directory) {
|
|
|
79
87
|
* Check if any explicit commands are configured (lint/build/test).
|
|
80
88
|
* If so, these take priority over ci.sh and discovery.
|
|
81
89
|
*/
|
|
82
|
-
|
|
90
|
+
function hasConfiguredCommands(config) {
|
|
83
91
|
return !!(config.lint || config.build || config.test);
|
|
84
92
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for quality-gate-config module
|
|
3
4
|
*
|
|
@@ -6,21 +7,22 @@
|
|
|
6
7
|
* - hasConfiguredCommands: checks if explicit commands are configured
|
|
7
8
|
* - DEFAULT_QUALITY_GATE: verify default values
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const quality_gate_config_1 = require("./quality-gate-config");
|
|
10
12
|
describe('DEFAULT_QUALITY_GATE', () => {
|
|
11
13
|
it('should have correct default values', () => {
|
|
12
|
-
expect(DEFAULT_QUALITY_GATE.steps).toEqual(['lint', 'build', 'test']);
|
|
13
|
-
expect(DEFAULT_QUALITY_GATE.useCiSh).toBe('auto');
|
|
14
|
-
expect(DEFAULT_QUALITY_GATE.packageManager).toBe('auto');
|
|
15
|
-
expect(DEFAULT_QUALITY_GATE.cacheSeconds).toBe(30);
|
|
16
|
-
expect(DEFAULT_QUALITY_GATE.maxOutputLines).toBe(160);
|
|
17
|
-
expect(DEFAULT_QUALITY_GATE.maxErrorLines).toBe(60);
|
|
18
|
-
expect(DEFAULT_QUALITY_GATE.include.rustClippy).toBe(true);
|
|
14
|
+
expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.steps).toEqual(['lint', 'build', 'test']);
|
|
15
|
+
expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.useCiSh).toBe('auto');
|
|
16
|
+
expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.packageManager).toBe('auto');
|
|
17
|
+
expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.cacheSeconds).toBe(30);
|
|
18
|
+
expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.maxOutputLines).toBe(160);
|
|
19
|
+
expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.maxErrorLines).toBe(60);
|
|
20
|
+
expect(quality_gate_config_1.DEFAULT_QUALITY_GATE.include.rustClippy).toBe(true);
|
|
19
21
|
});
|
|
20
22
|
});
|
|
21
23
|
describe('mergeQualityGateConfig', () => {
|
|
22
24
|
it('should return empty config when both inputs are undefined', () => {
|
|
23
|
-
const result = mergeQualityGateConfig(undefined, undefined);
|
|
25
|
+
const result = (0, quality_gate_config_1.mergeQualityGateConfig)(undefined, undefined);
|
|
24
26
|
expect(result).toEqual({ include: {} });
|
|
25
27
|
});
|
|
26
28
|
it('should return root config when dotOpencode is undefined', () => {
|
|
@@ -28,7 +30,7 @@ describe('mergeQualityGateConfig', () => {
|
|
|
28
30
|
lint: 'pnpm lint',
|
|
29
31
|
cacheSeconds: 60,
|
|
30
32
|
};
|
|
31
|
-
const result = mergeQualityGateConfig(root, undefined);
|
|
33
|
+
const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, undefined);
|
|
32
34
|
expect(result).toEqual({ lint: 'pnpm lint', cacheSeconds: 60, include: {} });
|
|
33
35
|
});
|
|
34
36
|
it('should return dotOpencode config when root is undefined', () => {
|
|
@@ -36,7 +38,7 @@ describe('mergeQualityGateConfig', () => {
|
|
|
36
38
|
build: 'npm run build',
|
|
37
39
|
maxOutputLines: 200,
|
|
38
40
|
};
|
|
39
|
-
const result = mergeQualityGateConfig(undefined, dotOpencode);
|
|
41
|
+
const result = (0, quality_gate_config_1.mergeQualityGateConfig)(undefined, dotOpencode);
|
|
40
42
|
expect(result).toEqual({ build: 'npm run build', maxOutputLines: 200, include: {} });
|
|
41
43
|
});
|
|
42
44
|
it('should override root values with dotOpencode values', () => {
|
|
@@ -49,7 +51,7 @@ describe('mergeQualityGateConfig', () => {
|
|
|
49
51
|
lint: 'npm run lint:strict',
|
|
50
52
|
cacheSeconds: 60,
|
|
51
53
|
};
|
|
52
|
-
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
54
|
+
const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
|
|
53
55
|
expect(result.lint).toBe('npm run lint:strict');
|
|
54
56
|
expect(result.build).toBe('pnpm build');
|
|
55
57
|
expect(result.cacheSeconds).toBe(60);
|
|
@@ -65,7 +67,7 @@ describe('mergeQualityGateConfig', () => {
|
|
|
65
67
|
rustClippy: true,
|
|
66
68
|
},
|
|
67
69
|
};
|
|
68
|
-
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
70
|
+
const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
|
|
69
71
|
expect(result.include?.rustClippy).toBe(true);
|
|
70
72
|
});
|
|
71
73
|
it('should preserve root include values not overridden by dotOpencode', () => {
|
|
@@ -78,7 +80,7 @@ describe('mergeQualityGateConfig', () => {
|
|
|
78
80
|
lint: 'custom lint',
|
|
79
81
|
// No include specified
|
|
80
82
|
};
|
|
81
|
-
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
83
|
+
const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
|
|
82
84
|
expect(result.include?.rustClippy).toBe(true);
|
|
83
85
|
expect(result.lint).toBe('custom lint');
|
|
84
86
|
});
|
|
@@ -102,7 +104,7 @@ describe('mergeQualityGateConfig', () => {
|
|
|
102
104
|
useCiSh: 'never',
|
|
103
105
|
include: { rustClippy: true },
|
|
104
106
|
};
|
|
105
|
-
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
107
|
+
const result = (0, quality_gate_config_1.mergeQualityGateConfig)(root, dotOpencode);
|
|
106
108
|
expect(result.lint).toBe('lint-override');
|
|
107
109
|
expect(result.build).toBe('build-root');
|
|
108
110
|
expect(result.test).toBe('test-root');
|
|
@@ -118,7 +120,7 @@ describe('mergeQualityGateConfig', () => {
|
|
|
118
120
|
});
|
|
119
121
|
describe('hasConfiguredCommands', () => {
|
|
120
122
|
it('should return false for empty config', () => {
|
|
121
|
-
expect(hasConfiguredCommands({})).toBe(false);
|
|
123
|
+
expect((0, quality_gate_config_1.hasConfiguredCommands)({})).toBe(false);
|
|
122
124
|
});
|
|
123
125
|
it('should return false when only non-command properties are set', () => {
|
|
124
126
|
const config = {
|
|
@@ -127,25 +129,25 @@ describe('hasConfiguredCommands', () => {
|
|
|
127
129
|
useCiSh: 'always',
|
|
128
130
|
packageManager: 'pnpm',
|
|
129
131
|
};
|
|
130
|
-
expect(hasConfiguredCommands(config)).toBe(false);
|
|
132
|
+
expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(false);
|
|
131
133
|
});
|
|
132
134
|
it('should return true when lint is configured', () => {
|
|
133
135
|
const config = {
|
|
134
136
|
lint: 'pnpm lint',
|
|
135
137
|
};
|
|
136
|
-
expect(hasConfiguredCommands(config)).toBe(true);
|
|
138
|
+
expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
|
|
137
139
|
});
|
|
138
140
|
it('should return true when build is configured', () => {
|
|
139
141
|
const config = {
|
|
140
142
|
build: 'pnpm build',
|
|
141
143
|
};
|
|
142
|
-
expect(hasConfiguredCommands(config)).toBe(true);
|
|
144
|
+
expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
|
|
143
145
|
});
|
|
144
146
|
it('should return true when test is configured', () => {
|
|
145
147
|
const config = {
|
|
146
148
|
test: 'pnpm test',
|
|
147
149
|
};
|
|
148
|
-
expect(hasConfiguredCommands(config)).toBe(true);
|
|
150
|
+
expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
|
|
149
151
|
});
|
|
150
152
|
it('should return true when multiple commands are configured', () => {
|
|
151
153
|
const config = {
|
|
@@ -153,12 +155,12 @@ describe('hasConfiguredCommands', () => {
|
|
|
153
155
|
build: 'pnpm build',
|
|
154
156
|
test: 'pnpm test',
|
|
155
157
|
};
|
|
156
|
-
expect(hasConfiguredCommands(config)).toBe(true);
|
|
158
|
+
expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(true);
|
|
157
159
|
});
|
|
158
160
|
it('should return false for empty string commands', () => {
|
|
159
161
|
const config = {
|
|
160
162
|
lint: '',
|
|
161
163
|
};
|
|
162
|
-
expect(hasConfiguredCommands(config)).toBe(false);
|
|
164
|
+
expect((0, quality_gate_config_1.hasConfiguredCommands)(config)).toBe(false);
|
|
163
165
|
});
|
|
164
166
|
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SECRET_PATTERNS = void 0;
|
|
4
|
+
exports.sanitizeOutput = sanitizeOutput;
|
|
5
|
+
exports.truncateOutput = truncateOutput;
|
|
6
|
+
exports.createQualityGateHooks = createQualityGateHooks;
|
|
1
7
|
const SERVICE_NAME = 'feature-factory';
|
|
2
8
|
const IDLE_DEBOUNCE_MS = 500;
|
|
3
9
|
const CI_TIMEOUT_MS = 300000; // 5 minutes
|
|
4
10
|
const SESSION_TTL_MS = 3600000; // 1 hour
|
|
5
11
|
const CLEANUP_INTERVAL_MS = 600000; // 10 minutes
|
|
6
|
-
|
|
12
|
+
exports.SECRET_PATTERNS = [
|
|
7
13
|
// AWS Access Key IDs
|
|
8
14
|
{ pattern: /AKIA[0-9A-Z]{16}/g, replacement: '[REDACTED_AWS_KEY]' },
|
|
9
15
|
// GitHub Personal Access Tokens (classic)
|
|
@@ -67,9 +73,9 @@ export const SECRET_PATTERNS = [
|
|
|
67
73
|
* Sanitizes CI output by redacting common secret patterns before sending to the LLM.
|
|
68
74
|
* This helps prevent accidental exposure of sensitive information in prompts.
|
|
69
75
|
*/
|
|
70
|
-
|
|
76
|
+
function sanitizeOutput(output) {
|
|
71
77
|
let sanitized = output;
|
|
72
|
-
for (const { pattern, replacement } of SECRET_PATTERNS) {
|
|
78
|
+
for (const { pattern, replacement } of exports.SECRET_PATTERNS) {
|
|
73
79
|
sanitized = sanitized.replace(pattern, replacement);
|
|
74
80
|
}
|
|
75
81
|
return sanitized;
|
|
@@ -78,7 +84,7 @@ export function sanitizeOutput(output) {
|
|
|
78
84
|
* Truncates CI output to the last N lines to reduce prompt size and focus on relevant errors.
|
|
79
85
|
* Adds a header indicating truncation if the output was longer than the limit.
|
|
80
86
|
*/
|
|
81
|
-
|
|
87
|
+
function truncateOutput(output, maxLines = 20) {
|
|
82
88
|
const lines = output.split('\n');
|
|
83
89
|
if (lines.length <= maxLines) {
|
|
84
90
|
return output;
|
|
@@ -154,7 +160,7 @@ async function log(client, level, message, extra) {
|
|
|
154
160
|
return undefined;
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
|
-
|
|
163
|
+
async function createQualityGateHooks(input) {
|
|
158
164
|
const { client, $, directory } = input;
|
|
159
165
|
async function ciShExists() {
|
|
160
166
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for stop-quality-gate module
|
|
3
4
|
*
|
|
@@ -5,7 +6,8 @@
|
|
|
5
6
|
* - sanitizeOutput: redacts secrets from CI output
|
|
6
7
|
* - isSessionReadOnly: determines if session has write permissions
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const stop_quality_gate_1 = require("./stop-quality-gate");
|
|
9
11
|
function isSessionReadOnly(permission) {
|
|
10
12
|
if (!permission)
|
|
11
13
|
return false;
|
|
@@ -15,178 +17,178 @@ describe('sanitizeOutput', () => {
|
|
|
15
17
|
describe('AWS credentials', () => {
|
|
16
18
|
it('should redact AWS Access Key IDs', () => {
|
|
17
19
|
const input = 'Found credentials: AKIAIOSFODNN7EXAMPLE in config';
|
|
18
|
-
const result = sanitizeOutput(input);
|
|
20
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
19
21
|
expect(result).toBe('Found credentials: [REDACTED_AWS_KEY] in config');
|
|
20
22
|
});
|
|
21
23
|
it('should redact multiple AWS keys', () => {
|
|
22
24
|
const input = 'Key1: AKIAIOSFODNN7EXAMPLE Key2: AKIAI44QH8DHBEXAMPLE';
|
|
23
|
-
const result = sanitizeOutput(input);
|
|
25
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
24
26
|
expect(result).toBe('Key1: [REDACTED_AWS_KEY] Key2: [REDACTED_AWS_KEY]');
|
|
25
27
|
});
|
|
26
28
|
it('should not redact partial AWS key patterns', () => {
|
|
27
29
|
const input = 'AKIA123'; // Too short
|
|
28
|
-
const result = sanitizeOutput(input);
|
|
30
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
29
31
|
expect(result).toBe('AKIA123');
|
|
30
32
|
});
|
|
31
33
|
});
|
|
32
34
|
describe('GitHub tokens', () => {
|
|
33
35
|
it('should redact GitHub Personal Access Tokens (classic)', () => {
|
|
34
36
|
const input = 'Using ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
35
|
-
const result = sanitizeOutput(input);
|
|
37
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
36
38
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
37
39
|
});
|
|
38
40
|
it('should redact GitHub Personal Access Tokens (fine-grained)', () => {
|
|
39
41
|
const input = 'Using github_pat_11ABCDEFG_abcdefghijklmnopqrstuvwxyz';
|
|
40
|
-
const result = sanitizeOutput(input);
|
|
42
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
41
43
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
42
44
|
});
|
|
43
45
|
it('should redact GitHub OAuth tokens', () => {
|
|
44
46
|
const input = 'oauth=gho_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
45
|
-
const result = sanitizeOutput(input);
|
|
47
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
46
48
|
expect(result).toBe('oauth=[REDACTED_GH_TOKEN]');
|
|
47
49
|
});
|
|
48
50
|
it('should redact GitHub App user-to-server tokens', () => {
|
|
49
51
|
const input = 'Using ghu_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
50
|
-
const result = sanitizeOutput(input);
|
|
52
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
51
53
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
52
54
|
});
|
|
53
55
|
it('should redact GitHub App server-to-server tokens', () => {
|
|
54
56
|
const input = 'Using ghs_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
55
|
-
const result = sanitizeOutput(input);
|
|
57
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
56
58
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
57
59
|
});
|
|
58
60
|
it('should not redact partial GitHub token patterns', () => {
|
|
59
61
|
const input = 'ghp_abc'; // Too short
|
|
60
|
-
const result = sanitizeOutput(input);
|
|
62
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
61
63
|
expect(result).toBe('ghp_abc');
|
|
62
64
|
});
|
|
63
65
|
});
|
|
64
66
|
describe('GitLab tokens', () => {
|
|
65
67
|
it('should redact GitLab Personal Access Tokens', () => {
|
|
66
68
|
const input = 'Using glpat-abcdefghij1234567890';
|
|
67
|
-
const result = sanitizeOutput(input);
|
|
69
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
68
70
|
expect(result).toBe('Using [REDACTED_GITLAB_TOKEN]');
|
|
69
71
|
});
|
|
70
72
|
it('should redact GitLab tokens with hyphens', () => {
|
|
71
73
|
const input = 'Using glpat-abc-def-ghi-jkl-mnop-qrs';
|
|
72
|
-
const result = sanitizeOutput(input);
|
|
74
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
73
75
|
expect(result).toBe('Using [REDACTED_GITLAB_TOKEN]');
|
|
74
76
|
});
|
|
75
77
|
it('should not redact partial GitLab token patterns', () => {
|
|
76
78
|
const input = 'glpat-short'; // Too short
|
|
77
|
-
const result = sanitizeOutput(input);
|
|
79
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
78
80
|
expect(result).toBe('glpat-short');
|
|
79
81
|
});
|
|
80
82
|
});
|
|
81
83
|
describe('npm tokens', () => {
|
|
82
84
|
it('should redact npm tokens', () => {
|
|
83
85
|
const input = 'Using npm_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
84
|
-
const result = sanitizeOutput(input);
|
|
86
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
85
87
|
expect(result).toBe('Using [REDACTED_NPM_TOKEN]');
|
|
86
88
|
});
|
|
87
89
|
it('should not redact partial npm token patterns', () => {
|
|
88
90
|
const input = 'npm_abc'; // Too short
|
|
89
|
-
const result = sanitizeOutput(input);
|
|
91
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
90
92
|
expect(result).toBe('npm_abc');
|
|
91
93
|
});
|
|
92
94
|
});
|
|
93
95
|
describe('Bearer tokens', () => {
|
|
94
96
|
it('should redact Bearer tokens', () => {
|
|
95
97
|
const input = 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
|
96
|
-
const result = sanitizeOutput(input);
|
|
98
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
97
99
|
expect(result).toBe('Authorization: Bearer [REDACTED]');
|
|
98
100
|
});
|
|
99
101
|
it('should handle case-insensitive Bearer', () => {
|
|
100
102
|
const input = 'bearer abc123-token.value';
|
|
101
|
-
const result = sanitizeOutput(input);
|
|
103
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
102
104
|
expect(result).toBe('Bearer [REDACTED]');
|
|
103
105
|
});
|
|
104
106
|
});
|
|
105
107
|
describe('API keys', () => {
|
|
106
108
|
it('should redact api_key assignments', () => {
|
|
107
109
|
const input = 'api_key=sk-1234567890abcdef';
|
|
108
|
-
const result = sanitizeOutput(input);
|
|
110
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
109
111
|
expect(result).toBe('api_key=[REDACTED]');
|
|
110
112
|
});
|
|
111
113
|
it('should redact api-key with hyphen', () => {
|
|
112
114
|
const input = 'api-key: my-secret-key';
|
|
113
|
-
const result = sanitizeOutput(input);
|
|
115
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
114
116
|
expect(result).toBe('api_key=[REDACTED]');
|
|
115
117
|
});
|
|
116
118
|
it('should redact apikey without separator', () => {
|
|
117
119
|
const input = 'apikey=abc123';
|
|
118
|
-
const result = sanitizeOutput(input);
|
|
120
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
119
121
|
expect(result).toBe('api_key=[REDACTED]');
|
|
120
122
|
});
|
|
121
123
|
it('should redact quoted api keys', () => {
|
|
122
124
|
const input = "api_key='secret-value'";
|
|
123
|
-
const result = sanitizeOutput(input);
|
|
125
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
124
126
|
expect(result).toBe('api_key=[REDACTED]');
|
|
125
127
|
});
|
|
126
128
|
});
|
|
127
129
|
describe('tokens', () => {
|
|
128
130
|
it('should redact token assignments with 8+ char values', () => {
|
|
129
131
|
const input = 'token=ghp_xxxxxxxxxxxxxxxxxxxx';
|
|
130
|
-
const result = sanitizeOutput(input);
|
|
132
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
131
133
|
expect(result).toBe('token=[REDACTED]');
|
|
132
134
|
});
|
|
133
135
|
it('should redact tokens plural with 8+ char values', () => {
|
|
134
136
|
const input = 'tokens: secret12345';
|
|
135
|
-
const result = sanitizeOutput(input);
|
|
137
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
136
138
|
expect(result).toBe('token=[REDACTED]');
|
|
137
139
|
});
|
|
138
140
|
it('should NOT redact token with short values (less than 8 chars)', () => {
|
|
139
141
|
const input = 'token=abc123';
|
|
140
|
-
const result = sanitizeOutput(input);
|
|
142
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
141
143
|
expect(result).toBe('token=abc123');
|
|
142
144
|
});
|
|
143
145
|
it('should NOT redact phrases like "token count: 5" (value too short)', () => {
|
|
144
146
|
const input = 'token count: 5';
|
|
145
|
-
const result = sanitizeOutput(input);
|
|
147
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
146
148
|
expect(result).toBe('token count: 5');
|
|
147
149
|
});
|
|
148
150
|
it('should NOT redact "token: abc" (value too short)', () => {
|
|
149
151
|
const input = 'token: abc';
|
|
150
|
-
const result = sanitizeOutput(input);
|
|
152
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
151
153
|
expect(result).toBe('token: abc');
|
|
152
154
|
});
|
|
153
155
|
it('should redact actual token values that are 8+ chars', () => {
|
|
154
156
|
const input = 'token=abcd1234efgh';
|
|
155
|
-
const result = sanitizeOutput(input);
|
|
157
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
156
158
|
expect(result).toBe('token=[REDACTED]');
|
|
157
159
|
});
|
|
158
160
|
it('should redact quoted tokens with 8+ char values', () => {
|
|
159
161
|
const input = 'token="my-secret-token-value"';
|
|
160
|
-
const result = sanitizeOutput(input);
|
|
162
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
161
163
|
expect(result).toBe('token=[REDACTED]');
|
|
162
164
|
});
|
|
163
165
|
});
|
|
164
166
|
describe('passwords', () => {
|
|
165
167
|
it('should redact password assignments', () => {
|
|
166
168
|
const input = 'password=super-secret-123!';
|
|
167
|
-
const result = sanitizeOutput(input);
|
|
169
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
168
170
|
expect(result).toBe('password=[REDACTED]');
|
|
169
171
|
});
|
|
170
172
|
it('should redact passwords plural', () => {
|
|
171
173
|
const input = 'passwords: "admin123"';
|
|
172
|
-
const result = sanitizeOutput(input);
|
|
174
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
173
175
|
expect(result).toBe('password=[REDACTED]');
|
|
174
176
|
});
|
|
175
177
|
it('should handle special characters in passwords', () => {
|
|
176
178
|
const input = 'password=P@$$w0rd!#%';
|
|
177
|
-
const result = sanitizeOutput(input);
|
|
179
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
178
180
|
expect(result).toBe('password=[REDACTED]');
|
|
179
181
|
});
|
|
180
182
|
});
|
|
181
183
|
describe('generic secrets', () => {
|
|
182
184
|
it('should redact secret assignments', () => {
|
|
183
185
|
const input = 'secret=my-app-secret-key';
|
|
184
|
-
const result = sanitizeOutput(input);
|
|
186
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
185
187
|
expect(result).toBe('secret=[REDACTED]');
|
|
186
188
|
});
|
|
187
189
|
it('should redact secrets plural', () => {
|
|
188
190
|
const input = 'secrets: "confidential-data"';
|
|
189
|
-
const result = sanitizeOutput(input);
|
|
191
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
190
192
|
expect(result).toBe('secret=[REDACTED]');
|
|
191
193
|
});
|
|
192
194
|
});
|
|
@@ -194,40 +196,40 @@ describe('sanitizeOutput', () => {
|
|
|
194
196
|
it('should redact long base64-like strings', () => {
|
|
195
197
|
const base64 = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODk=';
|
|
196
198
|
const input = `Encoded value: ${base64}`;
|
|
197
|
-
const result = sanitizeOutput(input);
|
|
199
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
198
200
|
expect(result).toBe('Encoded value: [REDACTED_BASE64]');
|
|
199
201
|
});
|
|
200
202
|
it('should not redact short base64 strings', () => {
|
|
201
203
|
const input = 'Short: abc123';
|
|
202
|
-
const result = sanitizeOutput(input);
|
|
204
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
203
205
|
expect(result).toBe('Short: abc123');
|
|
204
206
|
});
|
|
205
207
|
it('should redact base64 without padding', () => {
|
|
206
208
|
const base64 = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw';
|
|
207
|
-
const result = sanitizeOutput(base64);
|
|
209
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
208
210
|
expect(result).toBe('[REDACTED_BASE64]');
|
|
209
211
|
});
|
|
210
212
|
it('should redact base64 strings up to 500 chars (ReDoS prevention)', () => {
|
|
211
213
|
// Generate a 500-char base64 string
|
|
212
214
|
const base64 = 'A'.repeat(500);
|
|
213
|
-
const result = sanitizeOutput(base64);
|
|
215
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
214
216
|
expect(result).toBe('[REDACTED_BASE64]');
|
|
215
217
|
});
|
|
216
218
|
it('should handle base64 strings over 500 chars by matching only first 500 (ReDoS prevention)', () => {
|
|
217
219
|
// Generate a 501-char base64 string - only first 500 chars match
|
|
218
220
|
const base64 = 'A'.repeat(501);
|
|
219
|
-
const result = sanitizeOutput(base64);
|
|
221
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
220
222
|
// Pattern matches the first 500 chars, leaving 1 char behind
|
|
221
223
|
expect(result).toBe('[REDACTED_BASE64]A');
|
|
222
224
|
});
|
|
223
225
|
it('should handle base64 at exactly 40 chars (minimum threshold)', () => {
|
|
224
226
|
const base64 = 'A'.repeat(40);
|
|
225
|
-
const result = sanitizeOutput(base64);
|
|
227
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
226
228
|
expect(result).toBe('[REDACTED_BASE64]');
|
|
227
229
|
});
|
|
228
230
|
it('should NOT redact base64 strings under 40 chars', () => {
|
|
229
231
|
const base64 = 'A'.repeat(39);
|
|
230
|
-
const result = sanitizeOutput(base64);
|
|
232
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
231
233
|
expect(result).toBe(base64);
|
|
232
234
|
});
|
|
233
235
|
});
|
|
@@ -238,7 +240,7 @@ describe('sanitizeOutput', () => {
|
|
|
238
240
|
Auth: Bearer my-jwt-token
|
|
239
241
|
password=admin123
|
|
240
242
|
`;
|
|
241
|
-
const result = sanitizeOutput(input);
|
|
243
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
242
244
|
expect(result).toContain('[REDACTED_AWS_KEY]');
|
|
243
245
|
expect(result).toContain('Bearer [REDACTED]');
|
|
244
246
|
expect(result).toContain('password=[REDACTED]');
|
|
@@ -252,28 +254,28 @@ describe('sanitizeOutput', () => {
|
|
|
252
254
|
const input = `-----BEGIN RSA PRIVATE KEY-----
|
|
253
255
|
MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
|
|
254
256
|
-----END RSA PRIVATE KEY-----`;
|
|
255
|
-
const result = sanitizeOutput(input);
|
|
257
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
256
258
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
257
259
|
});
|
|
258
260
|
it('should redact OpenSSH private keys', () => {
|
|
259
261
|
const input = `-----BEGIN OPENSSH PRIVATE KEY-----
|
|
260
262
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAA
|
|
261
263
|
-----END OPENSSH PRIVATE KEY-----`;
|
|
262
|
-
const result = sanitizeOutput(input);
|
|
264
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
263
265
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
264
266
|
});
|
|
265
267
|
it('should redact EC private keys', () => {
|
|
266
268
|
const input = `-----BEGIN EC PRIVATE KEY-----
|
|
267
269
|
MHQCAQEEICg7E4NN6YPWoU6/FXa5ON6Pt6LKBfA8WL
|
|
268
270
|
-----END EC PRIVATE KEY-----`;
|
|
269
|
-
const result = sanitizeOutput(input);
|
|
271
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
270
272
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
271
273
|
});
|
|
272
274
|
it('should redact generic private keys', () => {
|
|
273
275
|
const input = `-----BEGIN PRIVATE KEY-----
|
|
274
276
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASC
|
|
275
277
|
-----END PRIVATE KEY-----`;
|
|
276
|
-
const result = sanitizeOutput(input);
|
|
278
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
277
279
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
278
280
|
});
|
|
279
281
|
it('should redact private keys embedded in output', () => {
|
|
@@ -282,7 +284,7 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASC
|
|
|
282
284
|
MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
|
|
283
285
|
-----END RSA PRIVATE KEY-----
|
|
284
286
|
Done loading.`;
|
|
285
|
-
const result = sanitizeOutput(input);
|
|
287
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
286
288
|
expect(result).toBe(`Loading configuration...
|
|
287
289
|
[REDACTED_PRIVATE_KEY]
|
|
288
290
|
Done loading.`);
|
|
@@ -291,49 +293,49 @@ Done loading.`);
|
|
|
291
293
|
describe('GCP credentials', () => {
|
|
292
294
|
it('should redact GCP API keys', () => {
|
|
293
295
|
const input = 'Using GCP key: AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe';
|
|
294
|
-
const result = sanitizeOutput(input);
|
|
296
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
295
297
|
expect(result).toBe('Using GCP key: [REDACTED_GCP_KEY]');
|
|
296
298
|
});
|
|
297
299
|
it('should redact multiple GCP API keys', () => {
|
|
298
300
|
const input = 'Key1: AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe Key2: AIzaSyB-1234567890abcdefghijklmnopqrstu';
|
|
299
|
-
const result = sanitizeOutput(input);
|
|
301
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
300
302
|
expect(result).toBe('Key1: [REDACTED_GCP_KEY] Key2: [REDACTED_GCP_KEY]');
|
|
301
303
|
});
|
|
302
304
|
it('should not redact partial GCP API key patterns', () => {
|
|
303
305
|
const input = 'AIza123'; // Too short
|
|
304
|
-
const result = sanitizeOutput(input);
|
|
306
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
305
307
|
expect(result).toBe('AIza123');
|
|
306
308
|
});
|
|
307
309
|
it('should redact GCP OAuth tokens', () => {
|
|
308
310
|
const input = 'Authorization: ya29.a0AfH6SMBx-example-token-value_123';
|
|
309
|
-
const result = sanitizeOutput(input);
|
|
311
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
310
312
|
expect(result).toBe('Authorization: [REDACTED_GCP_TOKEN]');
|
|
311
313
|
});
|
|
312
314
|
it('should redact GCP OAuth tokens with various characters', () => {
|
|
313
315
|
const input = 'token=ya29.Gl-abc_XYZ-123';
|
|
314
|
-
const result = sanitizeOutput(input);
|
|
316
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
315
317
|
expect(result).toBe('token=[REDACTED_GCP_TOKEN]');
|
|
316
318
|
});
|
|
317
319
|
});
|
|
318
320
|
describe('Slack tokens', () => {
|
|
319
321
|
it('should redact Slack bot tokens', () => {
|
|
320
322
|
const input = 'SLACK_BOT_TOKEN=xoxb-123456789012-1234567890123-AbCdEfGhIjKlMnOpQrStUvWx';
|
|
321
|
-
const result = sanitizeOutput(input);
|
|
323
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
322
324
|
expect(result).toBe('SLACK_BOT_TOKEN=[REDACTED_SLACK_TOKEN]');
|
|
323
325
|
});
|
|
324
326
|
it('should redact Slack user tokens', () => {
|
|
325
327
|
const input = 'Using xoxp-123456789012-123456789012-123456789012-abcdef1234567890abcdef1234567890';
|
|
326
|
-
const result = sanitizeOutput(input);
|
|
328
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
327
329
|
expect(result).toBe('Using [REDACTED_SLACK_TOKEN]');
|
|
328
330
|
});
|
|
329
331
|
it('should redact Slack app tokens', () => {
|
|
330
332
|
const input = 'APP_TOKEN=xapp-1-A0123BCDEFG-1234567890123-abcdefghijklmnopqrstuvwxyz0123456789';
|
|
331
|
-
const result = sanitizeOutput(input);
|
|
333
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
332
334
|
expect(result).toBe('APP_TOKEN=[REDACTED_SLACK_TOKEN]');
|
|
333
335
|
});
|
|
334
336
|
it('should redact Slack webhook URLs', () => {
|
|
335
337
|
const input = 'Webhook: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX';
|
|
336
|
-
const result = sanitizeOutput(input);
|
|
338
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
337
339
|
expect(result).toBe('Webhook: https://[REDACTED_SLACK_WEBHOOK]');
|
|
338
340
|
});
|
|
339
341
|
it('should redact multiple different Slack token types', () => {
|
|
@@ -342,7 +344,7 @@ Done loading.`);
|
|
|
342
344
|
User: xoxp-789-012-def
|
|
343
345
|
App: xapp-345-ghi
|
|
344
346
|
`;
|
|
345
|
-
const result = sanitizeOutput(input);
|
|
347
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
346
348
|
expect(result).toContain('[REDACTED_SLACK_TOKEN]');
|
|
347
349
|
expect(result).not.toContain('xoxb-');
|
|
348
350
|
expect(result).not.toContain('xoxp-');
|
|
@@ -353,100 +355,100 @@ Done loading.`);
|
|
|
353
355
|
it('should redact Stripe live secret keys', () => {
|
|
354
356
|
// 24 chars after sk_live_: 51ABC123DEF456GHI789JKLM
|
|
355
357
|
const input = 'STRIPE_SECRET_KEY=sk_live_51ABC123DEF456GHI789JKLM';
|
|
356
|
-
const result = sanitizeOutput(input);
|
|
358
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
357
359
|
expect(result).toBe('STRIPE_SECRET_KEY=[REDACTED_STRIPE_KEY]');
|
|
358
360
|
});
|
|
359
361
|
it('should redact Stripe test secret keys', () => {
|
|
360
362
|
// 24 chars after sk_test_: 51ABC123DEF456GHI789JKLM
|
|
361
363
|
const input = 'Using sk_test_51ABC123DEF456GHI789JKLM for testing';
|
|
362
|
-
const result = sanitizeOutput(input);
|
|
364
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
363
365
|
expect(result).toBe('Using [REDACTED_STRIPE_KEY] for testing');
|
|
364
366
|
});
|
|
365
367
|
it('should redact Stripe live restricted keys', () => {
|
|
366
368
|
// 24 chars after rk_live_: 51ABC123DEF456GHI789JKLM
|
|
367
369
|
const input = 'RESTRICTED_KEY=rk_live_51ABC123DEF456GHI789JKLM';
|
|
368
|
-
const result = sanitizeOutput(input);
|
|
370
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
369
371
|
expect(result).toBe('RESTRICTED_KEY=[REDACTED_STRIPE_KEY]');
|
|
370
372
|
});
|
|
371
373
|
it('should redact Stripe test restricted keys', () => {
|
|
372
374
|
// 24 chars after rk_test_: 51ABC123DEF456GHI789JKLM
|
|
373
375
|
const input = 'Using rk_test_51ABC123DEF456GHI789JKLM for testing';
|
|
374
|
-
const result = sanitizeOutput(input);
|
|
376
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
375
377
|
expect(result).toBe('Using [REDACTED_STRIPE_KEY] for testing');
|
|
376
378
|
});
|
|
377
379
|
it('should not redact Stripe keys that are too short', () => {
|
|
378
380
|
const input = 'sk_live_short'; // Less than 24 characters after prefix
|
|
379
|
-
const result = sanitizeOutput(input);
|
|
381
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
380
382
|
expect(result).toBe('sk_live_short');
|
|
381
383
|
});
|
|
382
384
|
it('should redact multiple Stripe keys', () => {
|
|
383
385
|
// 24 chars after each prefix
|
|
384
386
|
const input = 'Live: sk_live_51ABC123DEF456GHI789JKLM Test: sk_test_51XYZ789ABC123DEF456GHIJ';
|
|
385
|
-
const result = sanitizeOutput(input);
|
|
387
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
386
388
|
expect(result).toBe('Live: [REDACTED_STRIPE_KEY] Test: [REDACTED_STRIPE_KEY]');
|
|
387
389
|
});
|
|
388
390
|
it('should redact long Stripe keys', () => {
|
|
389
391
|
const input = 'sk_live_51ABC123DEF456GHI789JKLmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOP';
|
|
390
|
-
const result = sanitizeOutput(input);
|
|
392
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
391
393
|
expect(result).toBe('[REDACTED_STRIPE_KEY]');
|
|
392
394
|
});
|
|
393
395
|
});
|
|
394
396
|
describe('database connection strings', () => {
|
|
395
397
|
it('should redact PostgreSQL connection strings', () => {
|
|
396
398
|
const input = 'DATABASE_URL=postgres://admin:secretpass123@db.example.com:5432/mydb';
|
|
397
|
-
const result = sanitizeOutput(input);
|
|
399
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
398
400
|
expect(result).toBe('DATABASE_URL=[REDACTED_CONNECTION_STRING]');
|
|
399
401
|
});
|
|
400
402
|
it('should redact MongoDB connection strings with +srv', () => {
|
|
401
403
|
const input = 'Connecting to mongodb+srv://user:p@ssw0rd@cluster.mongodb.net/database';
|
|
402
|
-
const result = sanitizeOutput(input);
|
|
404
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
403
405
|
expect(result).toBe('Connecting to [REDACTED_CONNECTION_STRING]');
|
|
404
406
|
});
|
|
405
407
|
it('should redact Redis connection strings', () => {
|
|
406
408
|
const input = 'REDIS_URL=redis://default:myredispassword@redis.example.com:6379';
|
|
407
|
-
const result = sanitizeOutput(input);
|
|
409
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
408
410
|
expect(result).toBe('REDIS_URL=[REDACTED_CONNECTION_STRING]');
|
|
409
411
|
});
|
|
410
412
|
it('should redact MySQL connection strings', () => {
|
|
411
413
|
const input = 'mysql://root:rootpassword@localhost:3306/testdb';
|
|
412
|
-
const result = sanitizeOutput(input);
|
|
414
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
413
415
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
414
416
|
});
|
|
415
417
|
it('should redact rediss (TLS) connection strings', () => {
|
|
416
418
|
const input = 'rediss://user:password@secure-redis.example.com:6380';
|
|
417
|
-
const result = sanitizeOutput(input);
|
|
419
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
418
420
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
419
421
|
});
|
|
420
422
|
it('should redact connection strings with URL-encoded passwords', () => {
|
|
421
423
|
const input = 'mongodb://user:p%40ss%23word@host/db';
|
|
422
|
-
const result = sanitizeOutput(input);
|
|
424
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
423
425
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
424
426
|
});
|
|
425
427
|
it('should redact PostgreSQL with URL-encoded @ in password', () => {
|
|
426
428
|
const input = 'postgres://admin:secret%40pass@db.example.com:5432/mydb';
|
|
427
|
-
const result = sanitizeOutput(input);
|
|
429
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
428
430
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
429
431
|
});
|
|
430
432
|
it('should redact connection strings with multiple URL-encoded characters', () => {
|
|
431
433
|
const input = 'mysql://root:p%40ss%3Dw%26rd%21@localhost:3306/testdb';
|
|
432
|
-
const result = sanitizeOutput(input);
|
|
434
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
433
435
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
434
436
|
});
|
|
435
437
|
it('should redact MongoDB+srv with URL-encoded password', () => {
|
|
436
438
|
const input = 'mongodb+srv://user:my%40complex%23pass@cluster.mongodb.net/database';
|
|
437
|
-
const result = sanitizeOutput(input);
|
|
439
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
438
440
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
439
441
|
});
|
|
440
442
|
});
|
|
441
443
|
describe('non-secret content', () => {
|
|
442
444
|
it('should preserve normal log output', () => {
|
|
443
445
|
const input = 'Build completed successfully in 2.5s';
|
|
444
|
-
const result = sanitizeOutput(input);
|
|
446
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
445
447
|
expect(result).toBe(input);
|
|
446
448
|
});
|
|
447
449
|
it('should preserve error messages without secrets', () => {
|
|
448
450
|
const input = 'Error: Cannot find module "./missing-file"';
|
|
449
|
-
const result = sanitizeOutput(input);
|
|
451
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
450
452
|
expect(result).toBe(input);
|
|
451
453
|
});
|
|
452
454
|
it('should preserve stack traces', () => {
|
|
@@ -455,7 +457,7 @@ Done loading.`);
|
|
|
455
457
|
at Object.<anonymous> (/app/test.js:10:5)
|
|
456
458
|
at Module._compile (internal/modules/cjs/loader.js:1085:14)
|
|
457
459
|
`;
|
|
458
|
-
const result = sanitizeOutput(input);
|
|
460
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
459
461
|
expect(result).toBe(input);
|
|
460
462
|
});
|
|
461
463
|
});
|
|
@@ -506,17 +508,17 @@ describe('isSessionReadOnly', () => {
|
|
|
506
508
|
describe('truncateOutput', () => {
|
|
507
509
|
it('should return output unchanged if under maxLines', () => {
|
|
508
510
|
const input = 'line1\nline2\nline3';
|
|
509
|
-
expect(truncateOutput(input, 20)).toBe(input);
|
|
511
|
+
expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
|
|
510
512
|
});
|
|
511
513
|
it('should return output unchanged if exactly at maxLines', () => {
|
|
512
514
|
const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`);
|
|
513
515
|
const input = lines.join('\n');
|
|
514
|
-
expect(truncateOutput(input, 20)).toBe(input);
|
|
516
|
+
expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
|
|
515
517
|
});
|
|
516
518
|
it('should truncate to last 20 lines by default', () => {
|
|
517
519
|
const lines = Array.from({ length: 30 }, (_, i) => `line${i + 1}`);
|
|
518
520
|
const input = lines.join('\n');
|
|
519
|
-
const result = truncateOutput(input);
|
|
521
|
+
const result = (0, stop_quality_gate_1.truncateOutput)(input);
|
|
520
522
|
expect(result).toContain('... (10 lines omitted)');
|
|
521
523
|
expect(result).toContain('line11');
|
|
522
524
|
expect(result).toContain('line30');
|
|
@@ -526,7 +528,7 @@ describe('truncateOutput', () => {
|
|
|
526
528
|
it('should truncate to custom maxLines', () => {
|
|
527
529
|
const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`);
|
|
528
530
|
const input = lines.join('\n');
|
|
529
|
-
const result = truncateOutput(input, 5);
|
|
531
|
+
const result = (0, stop_quality_gate_1.truncateOutput)(input, 5);
|
|
530
532
|
expect(result).toContain('... (10 lines omitted)');
|
|
531
533
|
expect(result).toContain('line11');
|
|
532
534
|
expect(result).toContain('line15');
|
|
@@ -534,15 +536,15 @@ describe('truncateOutput', () => {
|
|
|
534
536
|
});
|
|
535
537
|
it('should handle single line output', () => {
|
|
536
538
|
const input = 'single line';
|
|
537
|
-
expect(truncateOutput(input, 20)).toBe(input);
|
|
539
|
+
expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
|
|
538
540
|
});
|
|
539
541
|
it('should handle empty output', () => {
|
|
540
|
-
expect(truncateOutput('', 20)).toBe('');
|
|
542
|
+
expect((0, stop_quality_gate_1.truncateOutput)('', 20)).toBe('');
|
|
541
543
|
});
|
|
542
544
|
it('should preserve line content exactly', () => {
|
|
543
545
|
const lines = Array.from({ length: 25 }, (_, i) => `Error at line ${i + 1}: something failed`);
|
|
544
546
|
const input = lines.join('\n');
|
|
545
|
-
const result = truncateOutput(input, 10);
|
|
547
|
+
const result = (0, stop_quality_gate_1.truncateOutput)(input, 10);
|
|
546
548
|
expect(result).toContain('Error at line 16: something failed');
|
|
547
549
|
expect(result).toContain('Error at line 25: something failed');
|
|
548
550
|
});
|
package/dist/types.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/package.json
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@syntesseraai/opencode-feature-factory",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
|
|
6
|
-
"type": "module",
|
|
7
6
|
"license": "MIT",
|
|
8
7
|
"main": "./dist/index.js",
|
|
9
|
-
"module": "./dist/index.js",
|
|
10
8
|
"types": "./dist/index.d.ts",
|
|
11
9
|
"bin": {
|
|
12
10
|
"ff-deploy": "./bin/ff-deploy.js"
|
|
13
11
|
},
|
|
14
|
-
"exports": {
|
|
15
|
-
".": {
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
|
-
"import": "./dist/index.js",
|
|
18
|
-
"default": "./dist/index.js"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
12
|
"files": [
|
|
22
13
|
"dist",
|
|
23
14
|
"assets",
|
|
@@ -40,7 +31,7 @@
|
|
|
40
31
|
],
|
|
41
32
|
"scripts": {},
|
|
42
33
|
"dependencies": {
|
|
43
|
-
"@opencode-ai/plugin": "1.1.
|
|
34
|
+
"@opencode-ai/plugin": "1.1.36"
|
|
44
35
|
},
|
|
45
36
|
"devDependencies": {
|
|
46
37
|
"@types/bun": "^1.2.6",
|