wogiflow 1.0.0
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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - PR Test Analyzer Step
|
|
5
|
+
*
|
|
6
|
+
* Analyzes test coverage and quality for modified files.
|
|
7
|
+
* Checks: coverage for modified files, test quality (mocks, edge cases),
|
|
8
|
+
* and ensures tests exist for new functionality.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { getProjectRoot, colors } = require('./flow-utils');
|
|
14
|
+
|
|
15
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run PR test analysis as a workflow step
|
|
19
|
+
*
|
|
20
|
+
* @param {object} options
|
|
21
|
+
* @param {string[]} options.files - Files modified
|
|
22
|
+
* @param {object} options.stepConfig - Step configuration
|
|
23
|
+
* @param {string} options.mode - Step mode (block/warn/prompt/auto)
|
|
24
|
+
* @returns {object} - { passed: boolean, message: string, details?: object }
|
|
25
|
+
*/
|
|
26
|
+
async function run(options = {}) {
|
|
27
|
+
const { files = [], stepConfig = {} } = options;
|
|
28
|
+
const checkCoverage = stepConfig.checkCoverage !== false;
|
|
29
|
+
const checkQuality = stepConfig.checkQuality !== false;
|
|
30
|
+
const minCoverageForModified = stepConfig.minCoverageForModified || 70;
|
|
31
|
+
|
|
32
|
+
// Filter to source files (not test files)
|
|
33
|
+
const sourceExtensions = ['.js', '.ts', '.jsx', '.tsx'];
|
|
34
|
+
const sourceFiles = files.filter(f =>
|
|
35
|
+
sourceExtensions.some(ext => f.endsWith(ext)) &&
|
|
36
|
+
!f.includes('.test.') &&
|
|
37
|
+
!f.includes('.spec.') &&
|
|
38
|
+
!f.includes('.d.ts') &&
|
|
39
|
+
!f.includes('__tests__') &&
|
|
40
|
+
!f.includes('__mocks__')
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (sourceFiles.length === 0) {
|
|
44
|
+
return { passed: true, message: 'No source files to analyze' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const issues = [];
|
|
48
|
+
const coverageReport = {};
|
|
49
|
+
const qualityReport = {};
|
|
50
|
+
|
|
51
|
+
// Check coverage for modified files
|
|
52
|
+
if (checkCoverage) {
|
|
53
|
+
const coverageIssues = await analyzeCoverage(sourceFiles, minCoverageForModified);
|
|
54
|
+
issues.push(...coverageIssues.issues);
|
|
55
|
+
Object.assign(coverageReport, coverageIssues.report);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check test quality
|
|
59
|
+
if (checkQuality) {
|
|
60
|
+
const qualityIssues = await analyzeTestQuality(sourceFiles, files);
|
|
61
|
+
issues.push(...qualityIssues.issues);
|
|
62
|
+
Object.assign(qualityReport, qualityIssues.report);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Report findings
|
|
66
|
+
if (issues.length > 0) {
|
|
67
|
+
console.log(colors.yellow + '\n PR Test Analysis Issues:' + colors.reset);
|
|
68
|
+
for (const issue of issues) {
|
|
69
|
+
const icon = issue.severity === 'high' ? '\u{1F534}' : '\u{1F7E1}';
|
|
70
|
+
console.log(` ${icon} ${issue.file}`);
|
|
71
|
+
console.log(` ${issue.type}: ${issue.message}`);
|
|
72
|
+
if (issue.suggestion) {
|
|
73
|
+
console.log(colors.dim + ` \u{2192} ${issue.suggestion}` + colors.reset);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const highSeverity = issues.filter(i => i.severity === 'high');
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
passed: highSeverity.length === 0,
|
|
82
|
+
message: issues.length === 0
|
|
83
|
+
? `Test analysis passed (${sourceFiles.length} files)`
|
|
84
|
+
: `${issues.length} issue(s) found (${highSeverity.length} high severity)`,
|
|
85
|
+
details: {
|
|
86
|
+
issues,
|
|
87
|
+
coverage: coverageReport,
|
|
88
|
+
quality: qualityReport,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Analyze test coverage for modified files
|
|
95
|
+
*/
|
|
96
|
+
async function analyzeCoverage(sourceFiles, minCoverage) {
|
|
97
|
+
const issues = [];
|
|
98
|
+
const report = { checked: [], missing: [], below: [] };
|
|
99
|
+
|
|
100
|
+
// Check if coverage report exists
|
|
101
|
+
const coveragePath = path.join(PROJECT_ROOT, 'coverage', 'coverage-summary.json');
|
|
102
|
+
let coverageData = null;
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(coveragePath)) {
|
|
105
|
+
try {
|
|
106
|
+
coverageData = JSON.parse(fs.readFileSync(coveragePath, 'utf8'));
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Can't read coverage
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const file of sourceFiles) {
|
|
113
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
114
|
+
if (!fs.existsSync(filePath)) continue;
|
|
115
|
+
|
|
116
|
+
// Find corresponding test file
|
|
117
|
+
const testPatterns = [
|
|
118
|
+
file.replace(/\.(js|ts|jsx|tsx)$/, '.test.$1'),
|
|
119
|
+
file.replace(/\.(js|ts|jsx|tsx)$/, '.spec.$1'),
|
|
120
|
+
file.replace(/src\//, 'src/__tests__/').replace(/\.(js|ts|jsx|tsx)$/, '.test.$1'),
|
|
121
|
+
file.replace(/src\//, 'tests/').replace(/\.(js|ts|jsx|tsx)$/, '.test.$1'),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const hasTest = testPatterns.some(pattern => {
|
|
125
|
+
const testPath = path.join(PROJECT_ROOT, pattern);
|
|
126
|
+
return fs.existsSync(testPath);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!hasTest) {
|
|
130
|
+
// Check if file exports anything testable
|
|
131
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
132
|
+
const hasExports = /export\s+(?:default\s+)?(?:function|class|const|let|var)|module\.exports/.test(content);
|
|
133
|
+
|
|
134
|
+
if (hasExports) {
|
|
135
|
+
issues.push({
|
|
136
|
+
file,
|
|
137
|
+
type: 'Missing Tests',
|
|
138
|
+
severity: 'high',
|
|
139
|
+
message: 'No test file found for source file with exports',
|
|
140
|
+
suggestion: `Create ${testPatterns[0]}`,
|
|
141
|
+
});
|
|
142
|
+
report.missing.push(file);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
report.checked.push(file);
|
|
148
|
+
|
|
149
|
+
// Check coverage if available
|
|
150
|
+
if (coverageData) {
|
|
151
|
+
const absPath = path.resolve(PROJECT_ROOT, file);
|
|
152
|
+
const fileCoverage = coverageData[absPath];
|
|
153
|
+
|
|
154
|
+
if (fileCoverage) {
|
|
155
|
+
const lineCoverage = fileCoverage.lines?.pct || 0;
|
|
156
|
+
const branchCoverage = fileCoverage.branches?.pct || 0;
|
|
157
|
+
const avgCoverage = (lineCoverage + branchCoverage) / 2;
|
|
158
|
+
|
|
159
|
+
if (avgCoverage < minCoverage) {
|
|
160
|
+
issues.push({
|
|
161
|
+
file,
|
|
162
|
+
type: 'Low Coverage',
|
|
163
|
+
severity: avgCoverage < minCoverage / 2 ? 'high' : 'medium',
|
|
164
|
+
message: `Coverage ${avgCoverage.toFixed(1)}% is below ${minCoverage}% threshold`,
|
|
165
|
+
suggestion: 'Add tests for uncovered lines and branches',
|
|
166
|
+
});
|
|
167
|
+
report.below.push({ file, coverage: avgCoverage });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { issues, report };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Analyze test quality for modified files
|
|
178
|
+
*/
|
|
179
|
+
async function analyzeTestQuality(sourceFiles, allFiles) {
|
|
180
|
+
const issues = [];
|
|
181
|
+
const report = { analyzed: [], concerns: [] };
|
|
182
|
+
|
|
183
|
+
// Find test files in the changeset
|
|
184
|
+
const testFiles = allFiles.filter(f =>
|
|
185
|
+
f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__')
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
for (const testFile of testFiles) {
|
|
189
|
+
const testPath = path.join(PROJECT_ROOT, testFile);
|
|
190
|
+
if (!fs.existsSync(testPath)) continue;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const content = fs.readFileSync(testPath, 'utf8');
|
|
194
|
+
report.analyzed.push(testFile);
|
|
195
|
+
|
|
196
|
+
// Check for test quality issues
|
|
197
|
+
const qualityChecks = analyzeTestFile(content, testFile);
|
|
198
|
+
issues.push(...qualityChecks);
|
|
199
|
+
|
|
200
|
+
if (qualityChecks.length > 0) {
|
|
201
|
+
report.concerns.push({ file: testFile, issues: qualityChecks.length });
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// Skip unreadable files
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Also check source files for testability concerns
|
|
209
|
+
for (const sourceFile of sourceFiles) {
|
|
210
|
+
const sourcePath = path.join(PROJECT_ROOT, sourceFile);
|
|
211
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
215
|
+
const testabilityIssues = checkTestability(content, sourceFile);
|
|
216
|
+
issues.push(...testabilityIssues);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
// Skip unreadable files
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { issues, report };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Analyze a test file for quality issues
|
|
227
|
+
*/
|
|
228
|
+
function analyzeTestFile(content, fileName) {
|
|
229
|
+
const issues = [];
|
|
230
|
+
|
|
231
|
+
// Check for empty tests
|
|
232
|
+
const emptyTests = content.match(/(?:it|test)\s*\([^)]+,\s*(?:async\s*)?\(\s*\)\s*=>\s*\{\s*\}\s*\)/g);
|
|
233
|
+
if (emptyTests) {
|
|
234
|
+
issues.push({
|
|
235
|
+
file: fileName,
|
|
236
|
+
type: 'Empty Tests',
|
|
237
|
+
severity: 'high',
|
|
238
|
+
message: `${emptyTests.length} empty test(s) found`,
|
|
239
|
+
suggestion: 'Add assertions to empty tests or remove them',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for skipped tests
|
|
244
|
+
const skippedTests = (content.match(/(?:it|test|describe)\.skip/g) || []).length;
|
|
245
|
+
if (skippedTests > 0) {
|
|
246
|
+
issues.push({
|
|
247
|
+
file: fileName,
|
|
248
|
+
type: 'Skipped Tests',
|
|
249
|
+
severity: 'medium',
|
|
250
|
+
message: `${skippedTests} skipped test(s)`,
|
|
251
|
+
suggestion: 'Fix or remove skipped tests before PR',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check for focused tests (only)
|
|
256
|
+
const focusedTests = (content.match(/(?:it|test|describe)\.only/g) || []).length;
|
|
257
|
+
if (focusedTests > 0) {
|
|
258
|
+
issues.push({
|
|
259
|
+
file: fileName,
|
|
260
|
+
type: 'Focused Tests',
|
|
261
|
+
severity: 'high',
|
|
262
|
+
message: `${focusedTests} focused test(s) - will skip other tests`,
|
|
263
|
+
suggestion: 'Remove .only before committing',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check for proper assertions
|
|
268
|
+
const testCount = (content.match(/(?:it|test)\s*\(/g) || []).length;
|
|
269
|
+
const assertionCount = (content.match(/expect\s*\(|assert\./g) || []).length;
|
|
270
|
+
|
|
271
|
+
if (testCount > 0 && assertionCount < testCount) {
|
|
272
|
+
issues.push({
|
|
273
|
+
file: fileName,
|
|
274
|
+
type: 'Missing Assertions',
|
|
275
|
+
severity: 'medium',
|
|
276
|
+
message: `${testCount} tests but only ${assertionCount} assertions`,
|
|
277
|
+
suggestion: 'Each test should have at least one assertion',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check for excessive mocking
|
|
282
|
+
const mockCount = (content.match(/jest\.mock|vi\.mock|sinon\.stub|\.mockImplementation/g) || []).length;
|
|
283
|
+
if (mockCount > 10) {
|
|
284
|
+
issues.push({
|
|
285
|
+
file: fileName,
|
|
286
|
+
type: 'Excessive Mocking',
|
|
287
|
+
severity: 'medium',
|
|
288
|
+
message: `${mockCount} mocks - tests may be too coupled to implementation`,
|
|
289
|
+
suggestion: 'Consider testing behavior rather than implementation',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check for hardcoded timeouts
|
|
294
|
+
if (/setTimeout\s*\(\s*[^,]+,\s*\d{4,}/.test(content)) {
|
|
295
|
+
issues.push({
|
|
296
|
+
file: fileName,
|
|
297
|
+
type: 'Hardcoded Timeout',
|
|
298
|
+
severity: 'medium',
|
|
299
|
+
message: 'Test uses hardcoded timeout - may cause flaky tests',
|
|
300
|
+
suggestion: 'Use fake timers or waitFor utilities instead',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check for missing error case tests
|
|
305
|
+
const hasErrorTests = /error|throw|reject|fail|catch/i.test(content);
|
|
306
|
+
if (!hasErrorTests && testCount > 3) {
|
|
307
|
+
issues.push({
|
|
308
|
+
file: fileName,
|
|
309
|
+
type: 'Missing Error Tests',
|
|
310
|
+
severity: 'medium',
|
|
311
|
+
message: 'No error/edge case tests found',
|
|
312
|
+
suggestion: 'Add tests for error conditions and edge cases',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return issues;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Check source file for testability concerns
|
|
321
|
+
*/
|
|
322
|
+
function checkTestability(content, fileName) {
|
|
323
|
+
const issues = [];
|
|
324
|
+
|
|
325
|
+
// Check for hard-to-test patterns
|
|
326
|
+
const singletonPattern = /(?:let|var)\s+instance\s*=\s*null.*getInstance/s;
|
|
327
|
+
if (singletonPattern.test(content)) {
|
|
328
|
+
issues.push({
|
|
329
|
+
file: fileName,
|
|
330
|
+
type: 'Singleton Pattern',
|
|
331
|
+
severity: 'medium',
|
|
332
|
+
message: 'Singleton pattern makes testing difficult',
|
|
333
|
+
suggestion: 'Consider dependency injection instead',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check for direct environment access
|
|
338
|
+
const envAccess = (content.match(/process\.env\.\w+/g) || []).length;
|
|
339
|
+
if (envAccess > 3) {
|
|
340
|
+
issues.push({
|
|
341
|
+
file: fileName,
|
|
342
|
+
type: 'Direct Env Access',
|
|
343
|
+
severity: 'medium',
|
|
344
|
+
message: `${envAccess} direct process.env accesses`,
|
|
345
|
+
suggestion: 'Consider using a config module for easier test mocking',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Check for complex constructors
|
|
350
|
+
const complexConstructor = /constructor\s*\([^)]*\)\s*\{[^}]{300,}\}/s;
|
|
351
|
+
if (complexConstructor.test(content)) {
|
|
352
|
+
issues.push({
|
|
353
|
+
file: fileName,
|
|
354
|
+
type: 'Complex Constructor',
|
|
355
|
+
severity: 'medium',
|
|
356
|
+
message: 'Constructor has complex logic - hard to test',
|
|
357
|
+
suggestion: 'Move initialization logic to separate methods',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return issues;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = { run, analyzeCoverage, analyzeTestQuality };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Regression Test Step
|
|
5
|
+
*
|
|
6
|
+
* Workflow step wrapper for regression testing.
|
|
7
|
+
* Tests random completed tasks to catch regressions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { getProjectRoot } = require('./flow-utils');
|
|
13
|
+
|
|
14
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run regression tests as a workflow step
|
|
18
|
+
*
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* @param {string} options.taskId - Current task ID
|
|
21
|
+
* @param {object} options.stepConfig - Step configuration
|
|
22
|
+
* @param {string} options.mode - Step mode (block/warn/prompt/auto)
|
|
23
|
+
* @returns {object} - { passed: boolean, message: string, details?: object }
|
|
24
|
+
*/
|
|
25
|
+
async function run(options = {}) {
|
|
26
|
+
const { stepConfig = {}, mode } = options;
|
|
27
|
+
const sampleSize = stepConfig.sampleSize || 3;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Run regression tests
|
|
31
|
+
const regressionScript = path.join(PROJECT_ROOT, 'scripts', 'flow-regression.js');
|
|
32
|
+
const result = execSync(`node "${regressionScript}" --count ${sampleSize} --json`, {
|
|
33
|
+
cwd: PROJECT_ROOT,
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Parse result
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(result);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// Non-JSON output means success with no issues
|
|
44
|
+
return { passed: true, message: 'Regression tests passed' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (parsed.failures && parsed.failures.length > 0) {
|
|
48
|
+
return {
|
|
49
|
+
passed: false,
|
|
50
|
+
message: `${parsed.failures.length} regression test(s) failed`,
|
|
51
|
+
details: parsed.failures,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
passed: true,
|
|
57
|
+
message: `Tested ${parsed.tested || sampleSize} completed tasks, all passed`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Check if it's a test failure or script error
|
|
62
|
+
if (error.stdout) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(error.stdout);
|
|
65
|
+
if (parsed.failures) {
|
|
66
|
+
return {
|
|
67
|
+
passed: false,
|
|
68
|
+
message: `${parsed.failures.length} regression test(s) failed`,
|
|
69
|
+
details: parsed.failures,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Not JSON, treat as error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// No completed tasks to test is not a failure
|
|
78
|
+
if (error.message && error.message.includes('No completed tasks')) {
|
|
79
|
+
return { passed: true, message: 'No completed tasks to test' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
passed: false,
|
|
84
|
+
message: `Regression test error: ${error.message}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { run };
|