@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
package/src/discovery.test.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for discovery module
|
|
3
|
-
*
|
|
4
|
-
* Tests focus on the pure function:
|
|
5
|
-
* - buildRunCommand: builds package manager-specific run commands
|
|
6
|
-
*
|
|
7
|
-
* Note: Most functions in discovery.ts require shell access ($) so they're
|
|
8
|
-
* integration-tested elsewhere. This file tests the pure utility functions.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { PackageManager } from './types';
|
|
12
|
-
|
|
13
|
-
// Re-implement buildRunCommand for testing since it's not exported
|
|
14
|
-
function buildRunCommand(pm: PackageManager, script: string): string {
|
|
15
|
-
switch (pm) {
|
|
16
|
-
case 'pnpm':
|
|
17
|
-
return `pnpm -s run ${script}`;
|
|
18
|
-
case 'bun':
|
|
19
|
-
return `bun run ${script}`;
|
|
20
|
-
case 'yarn':
|
|
21
|
-
return `yarn -s ${script}`;
|
|
22
|
-
case 'npm':
|
|
23
|
-
return `npm run -s ${script}`;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('buildRunCommand', () => {
|
|
28
|
-
describe('pnpm', () => {
|
|
29
|
-
it('should build correct pnpm command', () => {
|
|
30
|
-
expect(buildRunCommand('pnpm', 'lint')).toBe('pnpm -s run lint');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should build correct pnpm command for build script', () => {
|
|
34
|
-
expect(buildRunCommand('pnpm', 'build')).toBe('pnpm -s run build');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should build correct pnpm command for test script', () => {
|
|
38
|
-
expect(buildRunCommand('pnpm', 'test')).toBe('pnpm -s run test');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should handle scripts with colons', () => {
|
|
42
|
-
expect(buildRunCommand('pnpm', 'lint:ci')).toBe('pnpm -s run lint:ci');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('bun', () => {
|
|
47
|
-
it('should build correct bun command', () => {
|
|
48
|
-
expect(buildRunCommand('bun', 'lint')).toBe('bun run lint');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should build correct bun command for build script', () => {
|
|
52
|
-
expect(buildRunCommand('bun', 'build')).toBe('bun run build');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should build correct bun command for test script', () => {
|
|
56
|
-
expect(buildRunCommand('bun', 'test')).toBe('bun run test');
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('yarn', () => {
|
|
61
|
-
it('should build correct yarn command', () => {
|
|
62
|
-
expect(buildRunCommand('yarn', 'lint')).toBe('yarn -s lint');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should build correct yarn command for build script', () => {
|
|
66
|
-
expect(buildRunCommand('yarn', 'build')).toBe('yarn -s build');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should build correct yarn command for test script', () => {
|
|
70
|
-
expect(buildRunCommand('yarn', 'test')).toBe('yarn -s test');
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('npm', () => {
|
|
75
|
-
it('should build correct npm command', () => {
|
|
76
|
-
expect(buildRunCommand('npm', 'lint')).toBe('npm run -s lint');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should build correct npm command for build script', () => {
|
|
80
|
-
expect(buildRunCommand('npm', 'build')).toBe('npm run -s build');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should build correct npm command for test script', () => {
|
|
84
|
-
expect(buildRunCommand('npm', 'test')).toBe('npm run -s test');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('edge cases', () => {
|
|
89
|
-
it('should handle script names with hyphens', () => {
|
|
90
|
-
expect(buildRunCommand('npm', 'type-check')).toBe('npm run -s type-check');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should handle script names with underscores', () => {
|
|
94
|
-
expect(buildRunCommand('pnpm', 'lint_fix')).toBe('pnpm -s run lint_fix');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should handle long script names', () => {
|
|
98
|
-
expect(buildRunCommand('yarn', 'test:unit:coverage')).toBe('yarn -s test:unit:coverage');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* PackageManager type validation tests
|
|
105
|
-
* These ensure the type constraints are working correctly
|
|
106
|
-
*/
|
|
107
|
-
describe('PackageManager type', () => {
|
|
108
|
-
it('should accept valid package manager values', () => {
|
|
109
|
-
const validManagers: PackageManager[] = ['pnpm', 'bun', 'yarn', 'npm'];
|
|
110
|
-
validManagers.forEach((pm) => {
|
|
111
|
-
expect(['pnpm', 'bun', 'yarn', 'npm']).toContain(pm);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
});
|
package/src/discovery.ts
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import type { CommandStep, PackageManager, QualityGateConfig } from './types';
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_QUALITY_GATE,
|
|
4
|
-
fileExists,
|
|
5
|
-
hasConfiguredCommands,
|
|
6
|
-
readJsonFile,
|
|
7
|
-
} from './quality-gate-config';
|
|
8
|
-
|
|
9
|
-
type BunShell = any;
|
|
10
|
-
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Package Manager Detection
|
|
13
|
-
// ============================================================================
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Detect the package manager based on lockfile presence.
|
|
17
|
-
* Priority: pnpm > bun > yarn > npm
|
|
18
|
-
*/
|
|
19
|
-
async function detectPackageManager(
|
|
20
|
-
$: BunShell,
|
|
21
|
-
directory: string,
|
|
22
|
-
override?: string
|
|
23
|
-
): Promise<PackageManager> {
|
|
24
|
-
if (override && override !== 'auto') {
|
|
25
|
-
return override as PackageManager;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Priority order: pnpm > bun > yarn > npm
|
|
29
|
-
if (await fileExists($, `${directory}/pnpm-lock.yaml`)) return 'pnpm';
|
|
30
|
-
if (await fileExists($, `${directory}/bun.lockb`)) return 'bun';
|
|
31
|
-
if (await fileExists($, `${directory}/bun.lock`)) return 'bun';
|
|
32
|
-
if (await fileExists($, `${directory}/yarn.lock`)) return 'yarn';
|
|
33
|
-
if (await fileExists($, `${directory}/package-lock.json`)) return 'npm';
|
|
34
|
-
|
|
35
|
-
return 'npm'; // fallback
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Build the run command for a given package manager and script name
|
|
40
|
-
*/
|
|
41
|
-
function buildRunCommand(pm: PackageManager, script: string): string {
|
|
42
|
-
switch (pm) {
|
|
43
|
-
case 'pnpm':
|
|
44
|
-
return `pnpm -s run ${script}`;
|
|
45
|
-
case 'bun':
|
|
46
|
-
return `bun run ${script}`;
|
|
47
|
-
case 'yarn':
|
|
48
|
-
return `yarn -s ${script}`;
|
|
49
|
-
case 'npm':
|
|
50
|
-
return `npm run -s ${script}`;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ============================================================================
|
|
55
|
-
// Node.js Discovery
|
|
56
|
-
// ============================================================================
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Discover Node.js lint/build/test commands from package.json scripts
|
|
60
|
-
*/
|
|
61
|
-
async function discoverNodeCommands(
|
|
62
|
-
$: BunShell,
|
|
63
|
-
directory: string,
|
|
64
|
-
pm: PackageManager
|
|
65
|
-
): Promise<CommandStep[]> {
|
|
66
|
-
const pkgJson = await readJsonFile($, `${directory}/package.json`);
|
|
67
|
-
if (!pkgJson?.scripts) return [];
|
|
68
|
-
|
|
69
|
-
const scripts = pkgJson.scripts as Record<string, string>;
|
|
70
|
-
const steps: CommandStep[] = [];
|
|
71
|
-
|
|
72
|
-
// Lint: prefer lint:ci, then lint
|
|
73
|
-
const lintScript = scripts['lint:ci'] ? 'lint:ci' : scripts['lint'] ? 'lint' : null;
|
|
74
|
-
if (lintScript) {
|
|
75
|
-
steps.push({ step: 'lint', cmd: buildRunCommand(pm, lintScript) });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Build: prefer build:ci, then build
|
|
79
|
-
const buildScript = scripts['build:ci'] ? 'build:ci' : scripts['build'] ? 'build' : null;
|
|
80
|
-
if (buildScript) {
|
|
81
|
-
steps.push({ step: 'build', cmd: buildRunCommand(pm, buildScript) });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Test: prefer test:ci, then test
|
|
85
|
-
const testScript = scripts['test:ci'] ? 'test:ci' : scripts['test'] ? 'test' : null;
|
|
86
|
-
if (testScript) {
|
|
87
|
-
steps.push({ step: 'test', cmd: buildRunCommand(pm, testScript) });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return steps;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ============================================================================
|
|
94
|
-
// Rust Discovery
|
|
95
|
-
// ============================================================================
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Discover Rust commands from Cargo.toml presence
|
|
99
|
-
*/
|
|
100
|
-
async function discoverRustCommands(
|
|
101
|
-
$: BunShell,
|
|
102
|
-
directory: string,
|
|
103
|
-
includeClippy: boolean
|
|
104
|
-
): Promise<CommandStep[]> {
|
|
105
|
-
if (!(await fileExists($, `${directory}/Cargo.toml`))) return [];
|
|
106
|
-
|
|
107
|
-
const steps: CommandStep[] = [{ step: 'lint (fmt)', cmd: 'cargo fmt --check' }];
|
|
108
|
-
|
|
109
|
-
if (includeClippy) {
|
|
110
|
-
steps.push({
|
|
111
|
-
step: 'lint (clippy)',
|
|
112
|
-
cmd: 'cargo clippy --all-targets --all-features',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
steps.push({ step: 'build', cmd: 'cargo build' });
|
|
117
|
-
steps.push({ step: 'test', cmd: 'cargo test' });
|
|
118
|
-
|
|
119
|
-
return steps;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ============================================================================
|
|
123
|
-
// Go Discovery
|
|
124
|
-
// ============================================================================
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Discover Go commands from go.mod presence
|
|
128
|
-
*/
|
|
129
|
-
async function discoverGoCommands($: BunShell, directory: string): Promise<CommandStep[]> {
|
|
130
|
-
if (!(await fileExists($, `${directory}/go.mod`))) return [];
|
|
131
|
-
|
|
132
|
-
return [{ step: 'test', cmd: 'go test ./...' }];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ============================================================================
|
|
136
|
-
// Python Discovery
|
|
137
|
-
// ============================================================================
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Discover Python test commands with strong signal detection
|
|
141
|
-
*/
|
|
142
|
-
async function discoverPythonCommands($: BunShell, directory: string): Promise<CommandStep[]> {
|
|
143
|
-
// Only add pytest if we have strong signal
|
|
144
|
-
const hasPytestIni = await fileExists($, `${directory}/pytest.ini`);
|
|
145
|
-
const hasPyproject = await fileExists($, `${directory}/pyproject.toml`);
|
|
146
|
-
|
|
147
|
-
if (!hasPytestIni && !hasPyproject) return [];
|
|
148
|
-
|
|
149
|
-
// pytest.ini is strong signal
|
|
150
|
-
if (hasPytestIni) {
|
|
151
|
-
return [{ step: 'test', cmd: 'pytest' }];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Check if pyproject.toml mentions pytest
|
|
155
|
-
if (hasPyproject) {
|
|
156
|
-
try {
|
|
157
|
-
const result = await $`cat ${directory}/pyproject.toml`.quiet();
|
|
158
|
-
const content = result.text();
|
|
159
|
-
if (content.includes('pytest')) {
|
|
160
|
-
return [{ step: 'test', cmd: 'pytest' }];
|
|
161
|
-
}
|
|
162
|
-
} catch {
|
|
163
|
-
// Ignore read errors - return empty array if file can't be read
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return [];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ============================================================================
|
|
171
|
-
// Main Resolution Logic
|
|
172
|
-
// ============================================================================
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Resolve the command plan based on configuration and discovery.
|
|
176
|
-
*
|
|
177
|
-
* Resolution order:
|
|
178
|
-
* 1. Configured commands (qualityGate.lint/build/test) - highest priority
|
|
179
|
-
* 2. management/ci.sh (Feature Factory convention) - only if no configured commands
|
|
180
|
-
* 3. Conventional discovery (Node/Rust/Go/Python) - only if no ci.sh
|
|
181
|
-
*
|
|
182
|
-
* @returns Array of command steps to execute, or empty if nothing found
|
|
183
|
-
*/
|
|
184
|
-
export async function resolveCommands(args: {
|
|
185
|
-
$: BunShell;
|
|
186
|
-
directory: string;
|
|
187
|
-
config: QualityGateConfig;
|
|
188
|
-
}): Promise<CommandStep[]> {
|
|
189
|
-
const { $, directory, config } = args;
|
|
190
|
-
const mergedConfig = { ...DEFAULT_QUALITY_GATE, ...config };
|
|
191
|
-
|
|
192
|
-
// 1. Configured commands take priority (do NOT run ci.sh if these exist)
|
|
193
|
-
if (hasConfiguredCommands(config)) {
|
|
194
|
-
const steps: CommandStep[] = [];
|
|
195
|
-
const order = mergedConfig.steps;
|
|
196
|
-
|
|
197
|
-
for (const stepName of order) {
|
|
198
|
-
const cmd = config[stepName];
|
|
199
|
-
if (cmd) {
|
|
200
|
-
steps.push({ step: stepName, cmd });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return steps;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 2. Feature Factory CI script (only if no configured commands)
|
|
208
|
-
if (mergedConfig.useCiSh !== 'never') {
|
|
209
|
-
const ciShPath = `${directory}/management/ci.sh`;
|
|
210
|
-
if (await fileExists($, ciShPath)) {
|
|
211
|
-
return [{ step: 'ci', cmd: `bash ${ciShPath}` }];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 3. Conventional discovery (only if no ci.sh)
|
|
216
|
-
const pm = await detectPackageManager($, directory, mergedConfig.packageManager);
|
|
217
|
-
|
|
218
|
-
// Try Node first (most common)
|
|
219
|
-
if (await fileExists($, `${directory}/package.json`)) {
|
|
220
|
-
const nodeSteps = await discoverNodeCommands($, directory, pm);
|
|
221
|
-
if (nodeSteps.length > 0) return nodeSteps;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Rust
|
|
225
|
-
const rustSteps = await discoverRustCommands(
|
|
226
|
-
$,
|
|
227
|
-
directory,
|
|
228
|
-
mergedConfig.include?.rustClippy ?? true
|
|
229
|
-
);
|
|
230
|
-
if (rustSteps.length > 0) return rustSteps;
|
|
231
|
-
|
|
232
|
-
// Go
|
|
233
|
-
const goSteps = await discoverGoCommands($, directory);
|
|
234
|
-
if (goSteps.length > 0) return goSteps;
|
|
235
|
-
|
|
236
|
-
// Python
|
|
237
|
-
const pythonSteps = await discoverPythonCommands($, directory);
|
|
238
|
-
if (pythonSteps.length > 0) return pythonSteps;
|
|
239
|
-
|
|
240
|
-
// No commands discovered
|
|
241
|
-
return [];
|
|
242
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { Plugin, Hooks, PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
import { createQualityGateHooks } from './stop-quality-gate';
|
|
3
|
-
|
|
4
|
-
// Export skill and agent paths for programmatic access
|
|
5
|
-
export const SKILL_PATHS = {
|
|
6
|
-
'ff-mini-plan': './skills/ff-mini-plan/SKILL.md',
|
|
7
|
-
'ff-todo-management': './skills/ff-todo-management/SKILL.md',
|
|
8
|
-
'ff-severity-classification': './skills/ff-severity-classification/SKILL.md',
|
|
9
|
-
'ff-report-templates': './skills/ff-report-templates/SKILL.md',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const AGENT_PATHS = {
|
|
13
|
-
'ff-acceptance': './agents/ff-acceptance.md',
|
|
14
|
-
'ff-review': './agents/ff-review.md',
|
|
15
|
-
'ff-security': './agents/ff-security.md',
|
|
16
|
-
'ff-well-architected': './agents/ff-well-architected.md',
|
|
17
|
-
'ff-validate': './agents/ff-validate.md',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const SERVICE_NAME = 'feature-factory';
|
|
21
|
-
|
|
22
|
-
type Client = PluginInput['client'];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Log a message using the OpenCode client's structured logging.
|
|
26
|
-
* Silently fails if logging is unavailable.
|
|
27
|
-
*/
|
|
28
|
-
async function log(
|
|
29
|
-
client: Client,
|
|
30
|
-
level: 'debug' | 'info' | 'warn' | 'error',
|
|
31
|
-
message: string,
|
|
32
|
-
extra?: Record<string, unknown>
|
|
33
|
-
): Promise<void> {
|
|
34
|
-
try {
|
|
35
|
-
await client.app.log({
|
|
36
|
-
body: {
|
|
37
|
-
service: SERVICE_NAME,
|
|
38
|
-
level,
|
|
39
|
-
message,
|
|
40
|
-
extra,
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
} catch {
|
|
44
|
-
// Logging failure should not affect plugin operation
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Determine the project root directory.
|
|
50
|
-
* Prefer worktree (git root) if available, otherwise use directory.
|
|
51
|
-
*/
|
|
52
|
-
function resolveRootDir(input: { worktree: string; directory: string }): string {
|
|
53
|
-
const worktree = (input.worktree ?? '').trim();
|
|
54
|
-
if (worktree.length > 0) return worktree;
|
|
55
|
-
return input.directory;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Stop Quality Gate Plugin
|
|
60
|
-
*
|
|
61
|
-
* Runs quality gate checks when the agent becomes idle after editing files.
|
|
62
|
-
*
|
|
63
|
-
* Behavior:
|
|
64
|
-
* - Runs quality gate on session.idle (when agent finishes working)
|
|
65
|
-
* - Executes management/ci.sh directly (no LLM involvement)
|
|
66
|
-
* - On fast feedback: "Quality gate is running, please stand-by for results ..."
|
|
67
|
-
* - On success: "Quality gate passed"
|
|
68
|
-
* - On failure: passes full CI output to LLM for fix instructions
|
|
69
|
-
* - If management/ci.sh does not exist, quality gate does not run
|
|
70
|
-
*/
|
|
71
|
-
export const StopQualityGatePlugin: Plugin = async (input) => {
|
|
72
|
-
const { worktree, directory, client } = input;
|
|
73
|
-
const rootDir = resolveRootDir({ worktree, directory });
|
|
74
|
-
|
|
75
|
-
// Skip quality gate if no valid directory (e.g., global config with no project)
|
|
76
|
-
if (!rootDir || rootDir === '' || rootDir === '/') {
|
|
77
|
-
return {};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Create quality gate hooks
|
|
81
|
-
let qualityGateHooks: Partial<Hooks> = {};
|
|
82
|
-
try {
|
|
83
|
-
qualityGateHooks = await createQualityGateHooks(input);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
await log(client, 'error', 'quality-gate.init-error', {
|
|
86
|
-
error: String(error),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
...qualityGateHooks,
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Default export for OpenCode plugin discovery
|
|
96
|
-
export default StopQualityGatePlugin;
|
|
97
|
-
|
|
98
|
-
// Export types for consumers who want to use them
|
|
99
|
-
export type {
|
|
100
|
-
QualityGateConfig,
|
|
101
|
-
CommandStep,
|
|
102
|
-
StepResult,
|
|
103
|
-
SessionState,
|
|
104
|
-
PackageManager,
|
|
105
|
-
} from './types';
|
package/src/output.test.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
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
|
-
|
|
9
|
-
import { extractErrorLines, tailLines } from './output';
|
|
10
|
-
|
|
11
|
-
describe('extractErrorLines', () => {
|
|
12
|
-
it('should return empty array for empty input', () => {
|
|
13
|
-
expect(extractErrorLines('', 10)).toEqual([]);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should return empty array when no errors found', () => {
|
|
17
|
-
const output = `
|
|
18
|
-
Build started
|
|
19
|
-
Compiling modules...
|
|
20
|
-
Build completed successfully
|
|
21
|
-
`;
|
|
22
|
-
expect(extractErrorLines(output, 10)).toEqual([]);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should extract lines containing "error"', () => {
|
|
26
|
-
const output = `
|
|
27
|
-
Starting build
|
|
28
|
-
error: Cannot find module 'missing'
|
|
29
|
-
Build failed
|
|
30
|
-
`;
|
|
31
|
-
const result = extractErrorLines(output, 10);
|
|
32
|
-
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should extract lines containing "Error" (case insensitive)', () => {
|
|
36
|
-
const output = `
|
|
37
|
-
Running tests
|
|
38
|
-
TypeError: undefined is not a function
|
|
39
|
-
at test.js:10:5
|
|
40
|
-
`;
|
|
41
|
-
const result = extractErrorLines(output, 10);
|
|
42
|
-
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should extract lines containing "failed"', () => {
|
|
46
|
-
const output = `
|
|
47
|
-
Test suite: MyTests
|
|
48
|
-
Test failed: should work correctly
|
|
49
|
-
1 test failed
|
|
50
|
-
`;
|
|
51
|
-
const result = extractErrorLines(output, 10);
|
|
52
|
-
expect(result.some((line) => line.toLowerCase().includes('failed'))).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should extract lines containing "FAILED"', () => {
|
|
56
|
-
const output = `
|
|
57
|
-
Running: test_function
|
|
58
|
-
FAILED test_function - assertion error
|
|
59
|
-
`;
|
|
60
|
-
const result = extractErrorLines(output, 10);
|
|
61
|
-
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should extract lines containing "panic"', () => {
|
|
65
|
-
const output = `
|
|
66
|
-
thread 'main' panicked at 'assertion failed'
|
|
67
|
-
note: run with RUST_BACKTRACE=1
|
|
68
|
-
`;
|
|
69
|
-
const result = extractErrorLines(output, 10);
|
|
70
|
-
expect(result.some((line) => line.includes('panic'))).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should extract lines containing "exception"', () => {
|
|
74
|
-
const output = `
|
|
75
|
-
Exception in thread "main" java.lang.NullPointerException
|
|
76
|
-
at Main.main(Main.java:5)
|
|
77
|
-
`;
|
|
78
|
-
const result = extractErrorLines(output, 10);
|
|
79
|
-
expect(result.some((line) => line.includes('Exception'))).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should extract lines containing "traceback"', () => {
|
|
83
|
-
const output = `
|
|
84
|
-
Traceback (most recent call last):
|
|
85
|
-
File "test.py", line 10
|
|
86
|
-
NameError: name 'x' is not defined
|
|
87
|
-
`;
|
|
88
|
-
const result = extractErrorLines(output, 10);
|
|
89
|
-
expect(result.some((line) => line.includes('Traceback'))).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should include 2 lines of context after error', () => {
|
|
93
|
-
const output = `line1
|
|
94
|
-
error: something went wrong
|
|
95
|
-
context line 1
|
|
96
|
-
context line 2
|
|
97
|
-
line5`;
|
|
98
|
-
const result = extractErrorLines(output, 10);
|
|
99
|
-
expect(result).toContain('error: something went wrong');
|
|
100
|
-
expect(result).toContain('context line 1');
|
|
101
|
-
expect(result).toContain('context line 2');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should respect maxLines limit', () => {
|
|
105
|
-
const output = `
|
|
106
|
-
error: first error
|
|
107
|
-
context1
|
|
108
|
-
context2
|
|
109
|
-
error: second error
|
|
110
|
-
context3
|
|
111
|
-
context4
|
|
112
|
-
error: third error
|
|
113
|
-
context5
|
|
114
|
-
context6
|
|
115
|
-
`;
|
|
116
|
-
const result = extractErrorLines(output, 5);
|
|
117
|
-
expect(result.length).toBeLessThanOrEqual(5);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should handle error at end of output without enough context lines', () => {
|
|
121
|
-
const output = `line1
|
|
122
|
-
line2
|
|
123
|
-
error: final error`;
|
|
124
|
-
const result = extractErrorLines(output, 10);
|
|
125
|
-
expect(result).toContain('error: final error');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should extract ReferenceError', () => {
|
|
129
|
-
const output = `ReferenceError: x is not defined`;
|
|
130
|
-
const result = extractErrorLines(output, 10);
|
|
131
|
-
expect(result).toContain('ReferenceError: x is not defined');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should extract SyntaxError', () => {
|
|
135
|
-
const output = `SyntaxError: Unexpected token '}'`;
|
|
136
|
-
const result = extractErrorLines(output, 10);
|
|
137
|
-
expect(result).toContain("SyntaxError: Unexpected token '}'");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should extract ERR! (npm style errors)', () => {
|
|
141
|
-
const output = `npm ERR! code ENOENT
|
|
142
|
-
npm ERR! path /app/package.json`;
|
|
143
|
-
const result = extractErrorLines(output, 10);
|
|
144
|
-
expect(result.some((line) => line.includes('ERR!'))).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should extract FAIL (test runner style)', () => {
|
|
148
|
-
const output = `FAIL src/test.ts
|
|
149
|
-
Test suite failed to run`;
|
|
150
|
-
const result = extractErrorLines(output, 10);
|
|
151
|
-
expect(result.some((line) => line.includes('FAIL'))).toBe(true);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should extract multiple error types in same output', () => {
|
|
155
|
-
const output = `
|
|
156
|
-
Starting build...
|
|
157
|
-
error: Build failed
|
|
158
|
-
TypeError: Cannot read property 'x'
|
|
159
|
-
FAILED: test_something
|
|
160
|
-
Build process ended
|
|
161
|
-
`;
|
|
162
|
-
const result = extractErrorLines(output, 20);
|
|
163
|
-
expect(result.some((line) => line.includes('error:'))).toBe(true);
|
|
164
|
-
expect(result.some((line) => line.includes('TypeError'))).toBe(true);
|
|
165
|
-
expect(result.some((line) => line.includes('FAILED'))).toBe(true);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('tailLines', () => {
|
|
170
|
-
it('should return full output when lines count is less than maxLines', () => {
|
|
171
|
-
const output = 'line1\nline2\nline3';
|
|
172
|
-
expect(tailLines(output, 10)).toBe(output);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should return full output when lines count equals maxLines', () => {
|
|
176
|
-
const output = 'line1\nline2\nline3';
|
|
177
|
-
expect(tailLines(output, 3)).toBe(output);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should truncate and add notice when output exceeds maxLines', () => {
|
|
181
|
-
const output = 'line1\nline2\nline3\nline4\nline5';
|
|
182
|
-
const result = tailLines(output, 3);
|
|
183
|
-
expect(result).toContain('... (2 lines truncated)');
|
|
184
|
-
expect(result).toContain('line3');
|
|
185
|
-
expect(result).toContain('line4');
|
|
186
|
-
expect(result).toContain('line5');
|
|
187
|
-
expect(result).not.toContain('line1');
|
|
188
|
-
expect(result).not.toContain('line2');
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should handle single line output', () => {
|
|
192
|
-
const output = 'single line';
|
|
193
|
-
expect(tailLines(output, 5)).toBe('single line');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should handle empty output', () => {
|
|
197
|
-
expect(tailLines('', 5)).toBe('');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should handle maxLines of 1', () => {
|
|
201
|
-
const output = 'line1\nline2\nline3';
|
|
202
|
-
const result = tailLines(output, 1);
|
|
203
|
-
expect(result).toContain('... (2 lines truncated)');
|
|
204
|
-
expect(result).toContain('line3');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should correctly count truncated lines', () => {
|
|
208
|
-
const lines = Array.from({ length: 100 }, (_, i) => `line${i + 1}`);
|
|
209
|
-
const output = lines.join('\n');
|
|
210
|
-
const result = tailLines(output, 10);
|
|
211
|
-
expect(result).toContain('... (90 lines truncated)');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('should preserve line content in tail', () => {
|
|
215
|
-
const output = 'first\nsecond\nthird\nfourth\nfifth';
|
|
216
|
-
const result = tailLines(output, 2);
|
|
217
|
-
const resultLines = result.split('\n');
|
|
218
|
-
expect(resultLines[resultLines.length - 1]).toBe('fifth');
|
|
219
|
-
expect(resultLines[resultLines.length - 2]).toBe('fourth');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should handle output with empty lines', () => {
|
|
223
|
-
const output = 'line1\n\nline3\n\nline5';
|
|
224
|
-
const result = tailLines(output, 3);
|
|
225
|
-
expect(result).toContain('... (2 lines truncated)');
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('should handle output with only newlines', () => {
|
|
229
|
-
const output = '\n\n\n\n';
|
|
230
|
-
const result = tailLines(output, 2);
|
|
231
|
-
expect(result).toContain('... (3 lines truncated)');
|
|
232
|
-
});
|
|
233
|
-
});
|