@syntesseraai/opencode-feature-factory 0.2.3 → 0.2.4
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 +18 -0
- package/dist/discovery.js +189 -0
- package/dist/discovery.test.d.ts +10 -0
- package/dist/discovery.test.js +95 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +81 -0
- package/dist/output.d.ts +17 -0
- package/dist/output.js +48 -0
- package/dist/output.test.d.ts +8 -0
- package/dist/output.test.js +205 -0
- package/dist/quality-gate-config.d.ts +37 -0
- package/dist/quality-gate-config.js +84 -0
- package/dist/quality-gate-config.test.d.ts +9 -0
- package/dist/quality-gate-config.test.js +164 -0
- package/dist/stop-quality-gate.d.ts +16 -0
- package/dist/stop-quality-gate.js +378 -0
- package/dist/stop-quality-gate.test.d.ts +8 -0
- package/dist/stop-quality-gate.test.js +549 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/package.json +8 -6
- package/src/discovery.test.ts +0 -114
- package/src/discovery.ts +0 -242
- package/src/index.ts +0 -105
- package/src/output.test.ts +0 -233
- package/src/output.ts +0 -55
- package/src/quality-gate-config.test.ts +0 -186
- package/src/quality-gate-config.ts +0 -106
- package/src/stop-quality-gate.test.ts +0 -655
- package/src/stop-quality-gate.ts +0 -450
- package/src/types.ts +0 -72
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for output module
|
|
3
|
+
*
|
|
4
|
+
* Tests focus on pure functions:
|
|
5
|
+
* - extractErrorLines: extracts error-like lines from command output
|
|
6
|
+
* - tailLines: returns last N lines with truncation notice
|
|
7
|
+
*/
|
|
8
|
+
import { extractErrorLines, tailLines } from './output';
|
|
9
|
+
describe('extractErrorLines', () => {
|
|
10
|
+
it('should return empty array for empty input', () => {
|
|
11
|
+
expect(extractErrorLines('', 10)).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
it('should return empty array when no errors found', () => {
|
|
14
|
+
const output = `
|
|
15
|
+
Build started
|
|
16
|
+
Compiling modules...
|
|
17
|
+
Build completed successfully
|
|
18
|
+
`;
|
|
19
|
+
expect(extractErrorLines(output, 10)).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
it('should extract lines containing "error"', () => {
|
|
22
|
+
const output = `
|
|
23
|
+
Starting build
|
|
24
|
+
error: Cannot find module 'missing'
|
|
25
|
+
Build failed
|
|
26
|
+
`;
|
|
27
|
+
const result = extractErrorLines(output, 10);
|
|
28
|
+
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('should extract lines containing "Error" (case insensitive)', () => {
|
|
31
|
+
const output = `
|
|
32
|
+
Running tests
|
|
33
|
+
TypeError: undefined is not a function
|
|
34
|
+
at test.js:10:5
|
|
35
|
+
`;
|
|
36
|
+
const result = extractErrorLines(output, 10);
|
|
37
|
+
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('should extract lines containing "failed"', () => {
|
|
40
|
+
const output = `
|
|
41
|
+
Test suite: MyTests
|
|
42
|
+
Test failed: should work correctly
|
|
43
|
+
1 test failed
|
|
44
|
+
`;
|
|
45
|
+
const result = extractErrorLines(output, 10);
|
|
46
|
+
expect(result.some((line) => line.toLowerCase().includes('failed'))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('should extract lines containing "FAILED"', () => {
|
|
49
|
+
const output = `
|
|
50
|
+
Running: test_function
|
|
51
|
+
FAILED test_function - assertion error
|
|
52
|
+
`;
|
|
53
|
+
const result = extractErrorLines(output, 10);
|
|
54
|
+
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('should extract lines containing "panic"', () => {
|
|
57
|
+
const output = `
|
|
58
|
+
thread 'main' panicked at 'assertion failed'
|
|
59
|
+
note: run with RUST_BACKTRACE=1
|
|
60
|
+
`;
|
|
61
|
+
const result = extractErrorLines(output, 10);
|
|
62
|
+
expect(result.some((line) => line.includes('panic'))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('should extract lines containing "exception"', () => {
|
|
65
|
+
const output = `
|
|
66
|
+
Exception in thread "main" java.lang.NullPointerException
|
|
67
|
+
at Main.main(Main.java:5)
|
|
68
|
+
`;
|
|
69
|
+
const result = extractErrorLines(output, 10);
|
|
70
|
+
expect(result.some((line) => line.includes('Exception'))).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('should extract lines containing "traceback"', () => {
|
|
73
|
+
const output = `
|
|
74
|
+
Traceback (most recent call last):
|
|
75
|
+
File "test.py", line 10
|
|
76
|
+
NameError: name 'x' is not defined
|
|
77
|
+
`;
|
|
78
|
+
const result = extractErrorLines(output, 10);
|
|
79
|
+
expect(result.some((line) => line.includes('Traceback'))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('should include 2 lines of context after error', () => {
|
|
82
|
+
const output = `line1
|
|
83
|
+
error: something went wrong
|
|
84
|
+
context line 1
|
|
85
|
+
context line 2
|
|
86
|
+
line5`;
|
|
87
|
+
const result = extractErrorLines(output, 10);
|
|
88
|
+
expect(result).toContain('error: something went wrong');
|
|
89
|
+
expect(result).toContain('context line 1');
|
|
90
|
+
expect(result).toContain('context line 2');
|
|
91
|
+
});
|
|
92
|
+
it('should respect maxLines limit', () => {
|
|
93
|
+
const output = `
|
|
94
|
+
error: first error
|
|
95
|
+
context1
|
|
96
|
+
context2
|
|
97
|
+
error: second error
|
|
98
|
+
context3
|
|
99
|
+
context4
|
|
100
|
+
error: third error
|
|
101
|
+
context5
|
|
102
|
+
context6
|
|
103
|
+
`;
|
|
104
|
+
const result = extractErrorLines(output, 5);
|
|
105
|
+
expect(result.length).toBeLessThanOrEqual(5);
|
|
106
|
+
});
|
|
107
|
+
it('should handle error at end of output without enough context lines', () => {
|
|
108
|
+
const output = `line1
|
|
109
|
+
line2
|
|
110
|
+
error: final error`;
|
|
111
|
+
const result = extractErrorLines(output, 10);
|
|
112
|
+
expect(result).toContain('error: final error');
|
|
113
|
+
});
|
|
114
|
+
it('should extract ReferenceError', () => {
|
|
115
|
+
const output = `ReferenceError: x is not defined`;
|
|
116
|
+
const result = extractErrorLines(output, 10);
|
|
117
|
+
expect(result).toContain('ReferenceError: x is not defined');
|
|
118
|
+
});
|
|
119
|
+
it('should extract SyntaxError', () => {
|
|
120
|
+
const output = `SyntaxError: Unexpected token '}'`;
|
|
121
|
+
const result = extractErrorLines(output, 10);
|
|
122
|
+
expect(result).toContain("SyntaxError: Unexpected token '}'");
|
|
123
|
+
});
|
|
124
|
+
it('should extract ERR! (npm style errors)', () => {
|
|
125
|
+
const output = `npm ERR! code ENOENT
|
|
126
|
+
npm ERR! path /app/package.json`;
|
|
127
|
+
const result = extractErrorLines(output, 10);
|
|
128
|
+
expect(result.some((line) => line.includes('ERR!'))).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it('should extract FAIL (test runner style)', () => {
|
|
131
|
+
const output = `FAIL src/test.ts
|
|
132
|
+
Test suite failed to run`;
|
|
133
|
+
const result = extractErrorLines(output, 10);
|
|
134
|
+
expect(result.some((line) => line.includes('FAIL'))).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
it('should extract multiple error types in same output', () => {
|
|
137
|
+
const output = `
|
|
138
|
+
Starting build...
|
|
139
|
+
error: Build failed
|
|
140
|
+
TypeError: Cannot read property 'x'
|
|
141
|
+
FAILED: test_something
|
|
142
|
+
Build process ended
|
|
143
|
+
`;
|
|
144
|
+
const result = extractErrorLines(output, 20);
|
|
145
|
+
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
146
|
+
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
147
|
+
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('tailLines', () => {
|
|
151
|
+
it('should return full output when lines count is less than maxLines', () => {
|
|
152
|
+
const output = 'line1\nline2\nline3';
|
|
153
|
+
expect(tailLines(output, 10)).toBe(output);
|
|
154
|
+
});
|
|
155
|
+
it('should return full output when lines count equals maxLines', () => {
|
|
156
|
+
const output = 'line1\nline2\nline3';
|
|
157
|
+
expect(tailLines(output, 3)).toBe(output);
|
|
158
|
+
});
|
|
159
|
+
it('should truncate and add notice when output exceeds maxLines', () => {
|
|
160
|
+
const output = 'line1\nline2\nline3\nline4\nline5';
|
|
161
|
+
const result = tailLines(output, 3);
|
|
162
|
+
expect(result).toContain('... (2 lines truncated)');
|
|
163
|
+
expect(result).toContain('line3');
|
|
164
|
+
expect(result).toContain('line4');
|
|
165
|
+
expect(result).toContain('line5');
|
|
166
|
+
expect(result).not.toContain('line1');
|
|
167
|
+
expect(result).not.toContain('line2');
|
|
168
|
+
});
|
|
169
|
+
it('should handle single line output', () => {
|
|
170
|
+
const output = 'single line';
|
|
171
|
+
expect(tailLines(output, 5)).toBe('single line');
|
|
172
|
+
});
|
|
173
|
+
it('should handle empty output', () => {
|
|
174
|
+
expect(tailLines('', 5)).toBe('');
|
|
175
|
+
});
|
|
176
|
+
it('should handle maxLines of 1', () => {
|
|
177
|
+
const output = 'line1\nline2\nline3';
|
|
178
|
+
const result = tailLines(output, 1);
|
|
179
|
+
expect(result).toContain('... (2 lines truncated)');
|
|
180
|
+
expect(result).toContain('line3');
|
|
181
|
+
});
|
|
182
|
+
it('should correctly count truncated lines', () => {
|
|
183
|
+
const lines = Array.from({ length: 100 }, (_, i) => `line${i + 1}`);
|
|
184
|
+
const output = lines.join('\n');
|
|
185
|
+
const result = tailLines(output, 10);
|
|
186
|
+
expect(result).toContain('... (90 lines truncated)');
|
|
187
|
+
});
|
|
188
|
+
it('should preserve line content in tail', () => {
|
|
189
|
+
const output = 'first\nsecond\nthird\nfourth\nfifth';
|
|
190
|
+
const result = tailLines(output, 2);
|
|
191
|
+
const resultLines = result.split('\n');
|
|
192
|
+
expect(resultLines[resultLines.length - 1]).toBe('fifth');
|
|
193
|
+
expect(resultLines[resultLines.length - 2]).toBe('fourth');
|
|
194
|
+
});
|
|
195
|
+
it('should handle output with empty lines', () => {
|
|
196
|
+
const output = 'line1\n\nline3\n\nline5';
|
|
197
|
+
const result = tailLines(output, 3);
|
|
198
|
+
expect(result).toContain('... (2 lines truncated)');
|
|
199
|
+
});
|
|
200
|
+
it('should handle output with only newlines', () => {
|
|
201
|
+
const output = '\n\n\n\n';
|
|
202
|
+
const result = tailLines(output, 2);
|
|
203
|
+
expect(result).toContain('... (3 lines truncated)');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { QualityGateConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Default configuration values for StopQualityGate
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_QUALITY_GATE: Required<Omit<QualityGateConfig, 'lint' | 'build' | 'test' | 'cwd'>>;
|
|
6
|
+
type BunShell = any;
|
|
7
|
+
/**
|
|
8
|
+
* Read a JSON file safely, returning null if not found or invalid
|
|
9
|
+
*/
|
|
10
|
+
export declare function readJsonFile($: BunShell, path: string): Promise<Record<string, unknown> | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Check if a file exists
|
|
13
|
+
*/
|
|
14
|
+
export declare function fileExists($: BunShell, path: string): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Merge two qualityGate config objects.
|
|
17
|
+
*
|
|
18
|
+
* Precedence: `.opencode/opencode.json` overrides `opencode.json` (root).
|
|
19
|
+
* Deep-merges nested objects like `include`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function mergeQualityGateConfig(root: QualityGateConfig | undefined, dotOpencode: QualityGateConfig | undefined): QualityGateConfig;
|
|
22
|
+
/**
|
|
23
|
+
* Load and merge qualityGate config from both config file locations.
|
|
24
|
+
*
|
|
25
|
+
* Reads from:
|
|
26
|
+
* - `${directory}/opencode.json`
|
|
27
|
+
* - `${directory}/.opencode/opencode.json`
|
|
28
|
+
*
|
|
29
|
+
* Merges ONLY the `qualityGate` key. `.opencode/opencode.json` takes precedence.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadQualityGateConfig($: BunShell, directory: string): Promise<QualityGateConfig>;
|
|
32
|
+
/**
|
|
33
|
+
* Check if any explicit commands are configured (lint/build/test).
|
|
34
|
+
* If so, these take priority over ci.sh and discovery.
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasConfiguredCommands(config: QualityGateConfig): boolean;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration values for StopQualityGate
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_QUALITY_GATE = {
|
|
5
|
+
steps: ['lint', 'build', 'test'],
|
|
6
|
+
useCiSh: 'auto',
|
|
7
|
+
packageManager: 'auto',
|
|
8
|
+
cacheSeconds: 30,
|
|
9
|
+
maxOutputLines: 160,
|
|
10
|
+
maxErrorLines: 60,
|
|
11
|
+
include: {
|
|
12
|
+
rustClippy: true,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Read a JSON file safely, returning null if not found or invalid
|
|
17
|
+
*/
|
|
18
|
+
export async function readJsonFile($, path) {
|
|
19
|
+
try {
|
|
20
|
+
const result = await $ `cat ${path}`.quiet();
|
|
21
|
+
return JSON.parse(result.text());
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if a file exists
|
|
29
|
+
*/
|
|
30
|
+
export async function fileExists($, path) {
|
|
31
|
+
try {
|
|
32
|
+
await $ `test -f ${path}`.quiet();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Merge two qualityGate config objects.
|
|
41
|
+
*
|
|
42
|
+
* Precedence: `.opencode/opencode.json` overrides `opencode.json` (root).
|
|
43
|
+
* Deep-merges nested objects like `include`.
|
|
44
|
+
*/
|
|
45
|
+
export function mergeQualityGateConfig(root, dotOpencode) {
|
|
46
|
+
const base = root ?? {};
|
|
47
|
+
const override = dotOpencode ?? {};
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
...override,
|
|
51
|
+
// Deep merge the `include` object
|
|
52
|
+
include: {
|
|
53
|
+
...(base.include ?? {}),
|
|
54
|
+
...(override.include ?? {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Load and merge qualityGate config from both config file locations.
|
|
60
|
+
*
|
|
61
|
+
* Reads from:
|
|
62
|
+
* - `${directory}/opencode.json`
|
|
63
|
+
* - `${directory}/.opencode/opencode.json`
|
|
64
|
+
*
|
|
65
|
+
* Merges ONLY the `qualityGate` key. `.opencode/opencode.json` takes precedence.
|
|
66
|
+
*/
|
|
67
|
+
export async function loadQualityGateConfig($, directory) {
|
|
68
|
+
const rootConfigPath = `${directory}/opencode.json`;
|
|
69
|
+
const dotOpencodeConfigPath = `${directory}/.opencode/opencode.json`;
|
|
70
|
+
const [rootJson, dotOpencodeJson] = await Promise.all([
|
|
71
|
+
readJsonFile($, rootConfigPath),
|
|
72
|
+
readJsonFile($, dotOpencodeConfigPath),
|
|
73
|
+
]);
|
|
74
|
+
const rootQualityGate = rootJson?.qualityGate;
|
|
75
|
+
const dotOpencodeQualityGate = dotOpencodeJson?.qualityGate;
|
|
76
|
+
return mergeQualityGateConfig(rootQualityGate, dotOpencodeQualityGate);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if any explicit commands are configured (lint/build/test).
|
|
80
|
+
* If so, these take priority over ci.sh and discovery.
|
|
81
|
+
*/
|
|
82
|
+
export function hasConfiguredCommands(config) {
|
|
83
|
+
return !!(config.lint || config.build || config.test);
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for quality-gate-config module
|
|
3
|
+
*
|
|
4
|
+
* Tests focus on pure functions:
|
|
5
|
+
* - mergeQualityGateConfig: merges two config objects with deep merge for 'include'
|
|
6
|
+
* - hasConfiguredCommands: checks if explicit commands are configured
|
|
7
|
+
* - DEFAULT_QUALITY_GATE: verify default values
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for quality-gate-config module
|
|
3
|
+
*
|
|
4
|
+
* Tests focus on pure functions:
|
|
5
|
+
* - mergeQualityGateConfig: merges two config objects with deep merge for 'include'
|
|
6
|
+
* - hasConfiguredCommands: checks if explicit commands are configured
|
|
7
|
+
* - DEFAULT_QUALITY_GATE: verify default values
|
|
8
|
+
*/
|
|
9
|
+
import { mergeQualityGateConfig, hasConfiguredCommands, DEFAULT_QUALITY_GATE, } from './quality-gate-config';
|
|
10
|
+
describe('DEFAULT_QUALITY_GATE', () => {
|
|
11
|
+
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);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('mergeQualityGateConfig', () => {
|
|
22
|
+
it('should return empty config when both inputs are undefined', () => {
|
|
23
|
+
const result = mergeQualityGateConfig(undefined, undefined);
|
|
24
|
+
expect(result).toEqual({ include: {} });
|
|
25
|
+
});
|
|
26
|
+
it('should return root config when dotOpencode is undefined', () => {
|
|
27
|
+
const root = {
|
|
28
|
+
lint: 'pnpm lint',
|
|
29
|
+
cacheSeconds: 60,
|
|
30
|
+
};
|
|
31
|
+
const result = mergeQualityGateConfig(root, undefined);
|
|
32
|
+
expect(result).toEqual({ lint: 'pnpm lint', cacheSeconds: 60, include: {} });
|
|
33
|
+
});
|
|
34
|
+
it('should return dotOpencode config when root is undefined', () => {
|
|
35
|
+
const dotOpencode = {
|
|
36
|
+
build: 'npm run build',
|
|
37
|
+
maxOutputLines: 200,
|
|
38
|
+
};
|
|
39
|
+
const result = mergeQualityGateConfig(undefined, dotOpencode);
|
|
40
|
+
expect(result).toEqual({ build: 'npm run build', maxOutputLines: 200, include: {} });
|
|
41
|
+
});
|
|
42
|
+
it('should override root values with dotOpencode values', () => {
|
|
43
|
+
const root = {
|
|
44
|
+
lint: 'pnpm lint',
|
|
45
|
+
build: 'pnpm build',
|
|
46
|
+
cacheSeconds: 30,
|
|
47
|
+
};
|
|
48
|
+
const dotOpencode = {
|
|
49
|
+
lint: 'npm run lint:strict',
|
|
50
|
+
cacheSeconds: 60,
|
|
51
|
+
};
|
|
52
|
+
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
53
|
+
expect(result.lint).toBe('npm run lint:strict');
|
|
54
|
+
expect(result.build).toBe('pnpm build');
|
|
55
|
+
expect(result.cacheSeconds).toBe(60);
|
|
56
|
+
});
|
|
57
|
+
it('should deep merge include objects', () => {
|
|
58
|
+
const root = {
|
|
59
|
+
include: {
|
|
60
|
+
rustClippy: false,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const dotOpencode = {
|
|
64
|
+
include: {
|
|
65
|
+
rustClippy: true,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
69
|
+
expect(result.include?.rustClippy).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it('should preserve root include values not overridden by dotOpencode', () => {
|
|
72
|
+
const root = {
|
|
73
|
+
include: {
|
|
74
|
+
rustClippy: true,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const dotOpencode = {
|
|
78
|
+
lint: 'custom lint',
|
|
79
|
+
// No include specified
|
|
80
|
+
};
|
|
81
|
+
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
82
|
+
expect(result.include?.rustClippy).toBe(true);
|
|
83
|
+
expect(result.lint).toBe('custom lint');
|
|
84
|
+
});
|
|
85
|
+
it('should handle all config properties', () => {
|
|
86
|
+
const root = {
|
|
87
|
+
lint: 'lint-root',
|
|
88
|
+
build: 'build-root',
|
|
89
|
+
test: 'test-root',
|
|
90
|
+
cwd: './root',
|
|
91
|
+
steps: ['lint'],
|
|
92
|
+
useCiSh: 'auto',
|
|
93
|
+
packageManager: 'npm',
|
|
94
|
+
cacheSeconds: 30,
|
|
95
|
+
maxOutputLines: 100,
|
|
96
|
+
maxErrorLines: 50,
|
|
97
|
+
include: { rustClippy: false },
|
|
98
|
+
};
|
|
99
|
+
const dotOpencode = {
|
|
100
|
+
lint: 'lint-override',
|
|
101
|
+
steps: ['lint', 'test'],
|
|
102
|
+
useCiSh: 'never',
|
|
103
|
+
include: { rustClippy: true },
|
|
104
|
+
};
|
|
105
|
+
const result = mergeQualityGateConfig(root, dotOpencode);
|
|
106
|
+
expect(result.lint).toBe('lint-override');
|
|
107
|
+
expect(result.build).toBe('build-root');
|
|
108
|
+
expect(result.test).toBe('test-root');
|
|
109
|
+
expect(result.cwd).toBe('./root');
|
|
110
|
+
expect(result.steps).toEqual(['lint', 'test']);
|
|
111
|
+
expect(result.useCiSh).toBe('never');
|
|
112
|
+
expect(result.packageManager).toBe('npm');
|
|
113
|
+
expect(result.cacheSeconds).toBe(30);
|
|
114
|
+
expect(result.maxOutputLines).toBe(100);
|
|
115
|
+
expect(result.maxErrorLines).toBe(50);
|
|
116
|
+
expect(result.include?.rustClippy).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('hasConfiguredCommands', () => {
|
|
120
|
+
it('should return false for empty config', () => {
|
|
121
|
+
expect(hasConfiguredCommands({})).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('should return false when only non-command properties are set', () => {
|
|
124
|
+
const config = {
|
|
125
|
+
cacheSeconds: 60,
|
|
126
|
+
maxOutputLines: 200,
|
|
127
|
+
useCiSh: 'always',
|
|
128
|
+
packageManager: 'pnpm',
|
|
129
|
+
};
|
|
130
|
+
expect(hasConfiguredCommands(config)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
it('should return true when lint is configured', () => {
|
|
133
|
+
const config = {
|
|
134
|
+
lint: 'pnpm lint',
|
|
135
|
+
};
|
|
136
|
+
expect(hasConfiguredCommands(config)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('should return true when build is configured', () => {
|
|
139
|
+
const config = {
|
|
140
|
+
build: 'pnpm build',
|
|
141
|
+
};
|
|
142
|
+
expect(hasConfiguredCommands(config)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
it('should return true when test is configured', () => {
|
|
145
|
+
const config = {
|
|
146
|
+
test: 'pnpm test',
|
|
147
|
+
};
|
|
148
|
+
expect(hasConfiguredCommands(config)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it('should return true when multiple commands are configured', () => {
|
|
151
|
+
const config = {
|
|
152
|
+
lint: 'pnpm lint',
|
|
153
|
+
build: 'pnpm build',
|
|
154
|
+
test: 'pnpm test',
|
|
155
|
+
};
|
|
156
|
+
expect(hasConfiguredCommands(config)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
it('should return false for empty string commands', () => {
|
|
159
|
+
const config = {
|
|
160
|
+
lint: '',
|
|
161
|
+
};
|
|
162
|
+
expect(hasConfiguredCommands(config)).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PluginInput, Hooks } from '@opencode-ai/plugin';
|
|
2
|
+
export declare const SECRET_PATTERNS: Array<{
|
|
3
|
+
pattern: RegExp;
|
|
4
|
+
replacement: string;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Sanitizes CI output by redacting common secret patterns before sending to the LLM.
|
|
8
|
+
* This helps prevent accidental exposure of sensitive information in prompts.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sanitizeOutput(output: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Truncates CI output to the last N lines to reduce prompt size and focus on relevant errors.
|
|
13
|
+
* Adds a header indicating truncation if the output was longer than the limit.
|
|
14
|
+
*/
|
|
15
|
+
export declare function truncateOutput(output: string, maxLines?: number): string;
|
|
16
|
+
export declare function createQualityGateHooks(input: PluginInput): Promise<Partial<Hooks>>;
|