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