tlc-claude-code 2.4.10 → 2.6.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/.claude/commands/tlc/autofix.md +34 -1
- package/.claude/commands/tlc/build.md +203 -27
- package/.claude/commands/tlc/ci.md +178 -414
- package/.claude/commands/tlc/coverage.md +34 -0
- package/.claude/commands/tlc/deploy.md +19 -6
- package/.claude/commands/tlc/discuss.md +34 -0
- package/.claude/commands/tlc/docs.md +35 -1
- package/.claude/commands/tlc/e2e.md +300 -0
- package/.claude/commands/tlc/edge-cases.md +35 -1
- package/.claude/commands/tlc/init.md +38 -8
- package/.claude/commands/tlc/issues.md +46 -0
- package/.claude/commands/tlc/new-project.md +46 -4
- package/.claude/commands/tlc/plan.md +76 -0
- package/.claude/commands/tlc/quick.md +33 -0
- package/.claude/commands/tlc/release.md +85 -135
- package/.claude/commands/tlc/restore.md +14 -0
- package/.claude/commands/tlc/review.md +80 -1
- package/.claude/commands/tlc/tlc.md +134 -0
- package/.claude/commands/tlc/verify.md +64 -65
- package/.claude/commands/tlc/watchci.md +10 -0
- package/.claude/hooks/tlc-block-tools.sh +13 -0
- package/.claude/hooks/tlc-session-init.sh +9 -0
- package/CODING-STANDARDS.md +35 -10
- package/package.json +1 -1
- package/server/lib/block-tools-hook.js +23 -0
- package/server/lib/e2e/acceptance-parser.js +132 -0
- package/server/lib/e2e/acceptance-parser.test.js +110 -0
- package/server/lib/e2e/framework-detector.js +47 -0
- package/server/lib/e2e/framework-detector.test.js +94 -0
- package/server/lib/e2e/log-assertions.js +107 -0
- package/server/lib/e2e/log-assertions.test.js +68 -0
- package/server/lib/e2e/test-generator.js +159 -0
- package/server/lib/e2e/test-generator.test.js +121 -0
- package/server/lib/e2e/verify-runner.js +191 -0
- package/server/lib/e2e/verify-runner.test.js +167 -0
- package/server/lib/github/config.js +458 -0
- package/server/lib/github/config.test.js +385 -0
- package/server/lib/github/gh-client.js +303 -0
- package/server/lib/github/gh-client.test.js +499 -0
- package/server/lib/github/gh-projects.js +594 -0
- package/server/lib/github/gh-projects.test.js +583 -0
- package/server/lib/github/index.js +19 -0
- package/server/lib/github/plan-sync.js +456 -0
- package/server/lib/github/plan-sync.test.js +805 -0
- package/server/lib/hooks/block-tools-hook.test.js +54 -0
- package/server/lib/orchestration/cli-dispatch.js +16 -1
- package/server/lib/orchestration/cli-dispatch.test.js +94 -8
- package/server/lib/orchestration/completion-checker.js +101 -0
- package/server/lib/orchestration/completion-checker.test.js +177 -0
- package/server/lib/orchestration/result-verifier.js +143 -0
- package/server/lib/orchestration/result-verifier.test.js +291 -0
- package/server/lib/orchestration/session-dispatcher.js +99 -0
- package/server/lib/orchestration/session-dispatcher.test.js +215 -0
- package/server/lib/orchestration/session-status.js +147 -0
- package/server/lib/orchestration/session-status.test.js +130 -0
- package/server/lib/release/agent-runner-updates.js +24 -0
- package/server/lib/release/agent-runner-updates.test.js +22 -0
- package/server/lib/release/changelog-generator.js +142 -0
- package/server/lib/release/changelog-generator.test.js +113 -0
- package/server/lib/release/ci-watcher.js +83 -0
- package/server/lib/release/ci-watcher.test.js +81 -0
- package/server/lib/release/health-checker.js +111 -0
- package/server/lib/release/health-checker.test.js +121 -0
- package/server/lib/release/release-pipeline.js +187 -0
- package/server/lib/release/release-pipeline.test.js +262 -0
- package/server/lib/release/version-bumper.js +183 -0
- package/server/lib/release/version-bumper.test.js +142 -0
- package/server/lib/routing-preamble.integration.test.js +12 -0
- package/server/lib/routing-preamble.js +13 -2
- package/server/lib/routing-preamble.test.js +49 -0
- package/server/lib/scaffolding/ci-detector.js +139 -0
- package/server/lib/scaffolding/ci-detector.test.js +198 -0
- package/server/lib/scaffolding/ci-scaffolder.js +347 -0
- package/server/lib/scaffolding/ci-scaffolder.test.js +157 -0
- package/server/lib/scaffolding/deploy-detector.js +135 -0
- package/server/lib/scaffolding/deploy-detector.test.js +106 -0
- package/server/lib/scaffolding/health-scaffold.js +374 -0
- package/server/lib/scaffolding/health-scaffold.test.js +99 -0
- package/server/lib/scaffolding/logger-scaffold.js +196 -0
- package/server/lib/scaffolding/logger-scaffold.test.js +146 -0
- package/server/lib/scaffolding/migration-detector.js +78 -0
- package/server/lib/scaffolding/migration-detector.test.js +127 -0
- package/server/lib/scaffolding/snapshot-manager.js +142 -0
- package/server/lib/scaffolding/snapshot-manager.test.js +225 -0
- package/server/lib/task-router-config.js +50 -20
- package/server/lib/task-router-config.test.js +29 -15
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { afterEach, describe, it } from 'vitest';
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const { detectE2eFramework } = require('./framework-detector.js');
|
|
8
|
+
|
|
9
|
+
const tempDirs = [];
|
|
10
|
+
|
|
11
|
+
function createProjectDir() {
|
|
12
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'framework-detector-'));
|
|
13
|
+
tempDirs.push(projectDir);
|
|
14
|
+
return projectDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writePackageJson(projectDir, packageJson) {
|
|
18
|
+
fs.writeFileSync(
|
|
19
|
+
path.join(projectDir, 'package.json'),
|
|
20
|
+
JSON.stringify(packageJson, null, 2)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
while (tempDirs.length > 0) {
|
|
26
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('detectE2eFramework', () => {
|
|
31
|
+
it('returns playwright when a Playwright config is present', () => {
|
|
32
|
+
const projectDir = createProjectDir();
|
|
33
|
+
writePackageJson(projectDir, {
|
|
34
|
+
name: 'playwright-project',
|
|
35
|
+
devDependencies: {},
|
|
36
|
+
});
|
|
37
|
+
fs.writeFileSync(path.join(projectDir, 'playwright.config.ts'), 'export default {};');
|
|
38
|
+
|
|
39
|
+
const result = detectE2eFramework({ projectDir, fs });
|
|
40
|
+
|
|
41
|
+
assert.deepStrictEqual(result, { framework: 'playwright' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns supertest when package.json declares supertest', () => {
|
|
45
|
+
const projectDir = createProjectDir();
|
|
46
|
+
writePackageJson(projectDir, {
|
|
47
|
+
name: 'supertest-project',
|
|
48
|
+
devDependencies: {
|
|
49
|
+
supertest: '^7.0.0',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = detectE2eFramework({ projectDir, fs });
|
|
54
|
+
|
|
55
|
+
assert.deepStrictEqual(result, { framework: 'supertest' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('prefers playwright when both signals are present', () => {
|
|
59
|
+
const projectDir = createProjectDir();
|
|
60
|
+
writePackageJson(projectDir, {
|
|
61
|
+
name: 'mixed-project',
|
|
62
|
+
dependencies: {
|
|
63
|
+
supertest: '^7.0.0',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
fs.writeFileSync(path.join(projectDir, 'playwright.config.ts'), 'export default {};');
|
|
67
|
+
|
|
68
|
+
const result = detectE2eFramework({ projectDir, fs });
|
|
69
|
+
|
|
70
|
+
assert.deepStrictEqual(result, { framework: 'playwright' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns none when neither signal is present', () => {
|
|
74
|
+
const projectDir = createProjectDir();
|
|
75
|
+
writePackageJson(projectDir, {
|
|
76
|
+
name: 'plain-project',
|
|
77
|
+
dependencies: {
|
|
78
|
+
express: '^4.0.0',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const result = detectE2eFramework({ projectDir, fs });
|
|
83
|
+
|
|
84
|
+
assert.deepStrictEqual(result, { framework: 'none' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns none when package.json is missing', () => {
|
|
88
|
+
const projectDir = createProjectDir();
|
|
89
|
+
|
|
90
|
+
const result = detectE2eFramework({ projectDir, fs });
|
|
91
|
+
|
|
92
|
+
assert.deepStrictEqual(result, { framework: 'none' });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function normalizeEntry(entry) {
|
|
4
|
+
if (typeof entry === 'string') {
|
|
5
|
+
const trimmed = entry.trim();
|
|
6
|
+
const upper = trimmed.toUpperCase();
|
|
7
|
+
|
|
8
|
+
if (upper.includes('ERROR')) {
|
|
9
|
+
return { level: 'ERROR', message: trimmed };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (upper.includes('WARN')) {
|
|
13
|
+
return { level: 'WARN', message: trimmed };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!entry || typeof entry !== 'object') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rawLevel = typeof entry.level === 'string'
|
|
24
|
+
? entry.level
|
|
25
|
+
: typeof entry.severity === 'string'
|
|
26
|
+
? entry.severity
|
|
27
|
+
: null;
|
|
28
|
+
|
|
29
|
+
const rawMessage = typeof entry.message === 'string'
|
|
30
|
+
? entry.message
|
|
31
|
+
: typeof entry.msg === 'string'
|
|
32
|
+
? entry.msg
|
|
33
|
+
: typeof entry.content === 'string'
|
|
34
|
+
? entry.content
|
|
35
|
+
: null;
|
|
36
|
+
|
|
37
|
+
if (!rawLevel || !rawMessage) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const normalizedLevel = rawLevel.toUpperCase();
|
|
42
|
+
|
|
43
|
+
if (normalizedLevel === 'WARNING') {
|
|
44
|
+
return { level: 'WARN', message: rawMessage };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (normalizedLevel === 'WARN' || normalizedLevel === 'ERROR') {
|
|
48
|
+
return { level: normalizedLevel, message: rawMessage };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createMemoryCollector(buffer) {
|
|
55
|
+
const startIndex = buffer.length;
|
|
56
|
+
let stopIndex = null;
|
|
57
|
+
|
|
58
|
+
function getEntries(level) {
|
|
59
|
+
const endIndex = stopIndex === null ? buffer.length : stopIndex;
|
|
60
|
+
const entries = buffer.slice(startIndex, endIndex);
|
|
61
|
+
|
|
62
|
+
return entries
|
|
63
|
+
.map(normalizeEntry)
|
|
64
|
+
.filter((entry) => entry && entry.level === level)
|
|
65
|
+
.map((entry) => entry.message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
getErrors() {
|
|
70
|
+
return getEntries('ERROR');
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
getWarnings() {
|
|
74
|
+
return getEntries('WARN');
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
assertClean() {
|
|
78
|
+
const errors = this.getErrors();
|
|
79
|
+
|
|
80
|
+
if (errors.length > 0) {
|
|
81
|
+
throw new Error(`Expected no ERROR logs, but found:\n${errors.join('\n')}`);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
stop() {
|
|
86
|
+
if (stopIndex === null) {
|
|
87
|
+
stopIndex = buffer.length;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createLogCollector(options) {
|
|
94
|
+
const { logSource } = options || {};
|
|
95
|
+
|
|
96
|
+
if (logSource !== 'memory' && !Array.isArray(logSource)) {
|
|
97
|
+
throw new TypeError('createLogCollector requires logSource "memory" or an array buffer');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const buffer = logSource === 'memory' ? [] : logSource;
|
|
101
|
+
|
|
102
|
+
return createMemoryCollector(buffer);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
createLogCollector,
|
|
107
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const { createLogCollector } = require('./log-assertions.js');
|
|
6
|
+
|
|
7
|
+
describe('createLogCollector', () => {
|
|
8
|
+
it('collects warning and error messages from an in-memory buffer', () => {
|
|
9
|
+
const buffer = [];
|
|
10
|
+
const collector = createLogCollector({ logSource: buffer });
|
|
11
|
+
|
|
12
|
+
buffer.push({ level: 'info', message: 'startup complete' });
|
|
13
|
+
buffer.push({ level: 'warn', message: 'slow request' });
|
|
14
|
+
buffer.push({ severity: 'warning', message: 'deprecated config' });
|
|
15
|
+
buffer.push({ level: 'error', message: 'database unavailable' });
|
|
16
|
+
buffer.push('plain ERROR line');
|
|
17
|
+
|
|
18
|
+
expect(collector.getWarnings()).toEqual([
|
|
19
|
+
'slow request',
|
|
20
|
+
'deprecated config',
|
|
21
|
+
]);
|
|
22
|
+
expect(collector.getErrors()).toEqual([
|
|
23
|
+
'database unavailable',
|
|
24
|
+
'plain ERROR line',
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('assertClean passes when only warnings are present', () => {
|
|
29
|
+
const buffer = [];
|
|
30
|
+
const collector = createLogCollector({ logSource: buffer });
|
|
31
|
+
|
|
32
|
+
buffer.push({ level: 'warn', message: 'retrying request' });
|
|
33
|
+
|
|
34
|
+
expect(() => collector.assertClean()).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('assertClean throws with the error contents in the message', () => {
|
|
38
|
+
const buffer = [];
|
|
39
|
+
const collector = createLogCollector({ logSource: buffer });
|
|
40
|
+
|
|
41
|
+
buffer.push({ level: 'error', message: 'first failure' });
|
|
42
|
+
buffer.push('ERROR second failure');
|
|
43
|
+
|
|
44
|
+
expect(() => collector.assertClean()).toThrowError(
|
|
45
|
+
/first failure[\s\S]*ERROR second failure/
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('stop freezes the observed range', () => {
|
|
50
|
+
const buffer = [];
|
|
51
|
+
const collector = createLogCollector({ logSource: buffer });
|
|
52
|
+
|
|
53
|
+
buffer.push({ level: 'warn', message: 'before stop' });
|
|
54
|
+
collector.stop();
|
|
55
|
+
buffer.push({ level: 'error', message: 'after stop' });
|
|
56
|
+
|
|
57
|
+
expect(collector.getWarnings()).toEqual(['before stop']);
|
|
58
|
+
expect(collector.getErrors()).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('supports a standalone memory source for tests', () => {
|
|
62
|
+
const collector = createLogCollector({ logSource: 'memory' });
|
|
63
|
+
|
|
64
|
+
expect(collector.getWarnings()).toEqual([]);
|
|
65
|
+
expect(collector.getErrors()).toEqual([]);
|
|
66
|
+
expect(() => collector.assertClean()).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { parseAcceptanceCriteria } = require('./acceptance-parser.js');
|
|
6
|
+
|
|
7
|
+
function slugify(value) {
|
|
8
|
+
const slug = String(value || '')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
11
|
+
.replace(/\s+/g, '-')
|
|
12
|
+
.replace(/-+/g, '-')
|
|
13
|
+
.trim();
|
|
14
|
+
|
|
15
|
+
return slug || 'scenario';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function escapeSingleQuotes(value) {
|
|
19
|
+
return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveScenarioName(scenario, index) {
|
|
23
|
+
if (typeof scenario === 'string') {
|
|
24
|
+
return `Scenario ${index + 1}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return scenario.title
|
|
28
|
+
|| scenario.name
|
|
29
|
+
|| scenario.slug
|
|
30
|
+
|| scenario.id
|
|
31
|
+
|| `Scenario ${index + 1}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveScenarioSlug(scenario, index) {
|
|
35
|
+
if (scenario && typeof scenario === 'object' && typeof scenario.slug === 'string' && scenario.slug.trim()) {
|
|
36
|
+
return slugify(scenario.slug);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return slugify(resolveScenarioName(scenario, index));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolvePlanContent(scenario) {
|
|
43
|
+
if (typeof scenario === 'string') {
|
|
44
|
+
return scenario;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!scenario || typeof scenario !== 'object') {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return scenario.planContent
|
|
52
|
+
|| scenario.acceptanceCriteria
|
|
53
|
+
|| scenario.content
|
|
54
|
+
|| scenario.description
|
|
55
|
+
|| '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildPlaywrightContent(scenarioName, criteria) {
|
|
59
|
+
const tests = criteria.map(({ criterion }) => ` test('${escapeSingleQuotes(criterion)}', async ({ page }) => {
|
|
60
|
+
// TODO: Implement the browser flow for this acceptance criterion.
|
|
61
|
+
// TODO: Assert application logs remain clean for this flow.
|
|
62
|
+
});
|
|
63
|
+
`).join('\n');
|
|
64
|
+
|
|
65
|
+
return `'use strict';
|
|
66
|
+
|
|
67
|
+
const { test } = require('@playwright/test');
|
|
68
|
+
|
|
69
|
+
test.describe('${escapeSingleQuotes(scenarioName)}', () => {
|
|
70
|
+
test.afterEach(async ({ page }, testInfo) => {
|
|
71
|
+
await page.screenshot({
|
|
72
|
+
path: testInfo.outputPath('screenshot.png'),
|
|
73
|
+
fullPage: true,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
${tests}});
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildSupertestContent(scenarioName, criteria) {
|
|
82
|
+
const tests = criteria.map(({ criterion }) => ` it('${escapeSingleQuotes(criterion)}', async () => {
|
|
83
|
+
// TODO: Implement the API flow with supertest.
|
|
84
|
+
// TODO: Assert application logs remain clean for this flow.
|
|
85
|
+
});
|
|
86
|
+
`).join('\n');
|
|
87
|
+
|
|
88
|
+
return `'use strict';
|
|
89
|
+
|
|
90
|
+
const { afterEach, describe, it } = require('vitest');
|
|
91
|
+
const request = require('supertest');
|
|
92
|
+
|
|
93
|
+
describe('${escapeSingleQuotes(scenarioName)}', () => {
|
|
94
|
+
afterEach(async () => {
|
|
95
|
+
// TODO: Capture a screenshot or response artifact after each test.
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
${tests}});
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildFileContent(scenarioName, criteria, framework) {
|
|
103
|
+
if (framework === 'playwright') {
|
|
104
|
+
return buildPlaywrightContent(scenarioName, criteria);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (framework === 'supertest') {
|
|
108
|
+
return buildSupertestContent(scenarioName, criteria);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error(`Unsupported E2E framework: ${framework}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function generateE2eTests(options) {
|
|
115
|
+
const {
|
|
116
|
+
scenarios = [],
|
|
117
|
+
framework,
|
|
118
|
+
outputDir = '',
|
|
119
|
+
} = options || {};
|
|
120
|
+
|
|
121
|
+
const files = [];
|
|
122
|
+
const skipped = [];
|
|
123
|
+
|
|
124
|
+
for (const [index, scenario] of scenarios.entries()) {
|
|
125
|
+
const scenarioName = resolveScenarioName(scenario, index);
|
|
126
|
+
const slug = resolveScenarioSlug(scenario, index);
|
|
127
|
+
const planContent = resolvePlanContent(scenario);
|
|
128
|
+
const parsedCriteria = parseAcceptanceCriteria({ planContent });
|
|
129
|
+
const actionableCriteria = [];
|
|
130
|
+
|
|
131
|
+
for (const item of parsedCriteria) {
|
|
132
|
+
if (item.type === 'manual') {
|
|
133
|
+
skipped.push({
|
|
134
|
+
slug,
|
|
135
|
+
criterion: item.criterion,
|
|
136
|
+
type: item.type,
|
|
137
|
+
});
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
actionableCriteria.push(item);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (actionableCriteria.length === 0) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
files.push({
|
|
149
|
+
path: path.join(outputDir, 'e2e', `${slug}.test.ts`),
|
|
150
|
+
content: buildFileContent(scenarioName, actionableCriteria, framework),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { files, skipped };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
generateE2eTests,
|
|
159
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { describe, it } from 'vitest';
|
|
4
|
+
const assert = require('node:assert');
|
|
5
|
+
|
|
6
|
+
const { generateE2eTests } = require('./test-generator.js');
|
|
7
|
+
|
|
8
|
+
describe('generateE2eTests', () => {
|
|
9
|
+
it('generates playwright test files from parsed acceptance criteria', () => {
|
|
10
|
+
const result = generateE2eTests({
|
|
11
|
+
scenarios: [
|
|
12
|
+
{
|
|
13
|
+
title: 'User Login',
|
|
14
|
+
planContent: `
|
|
15
|
+
- [ ] User can click the sign in button
|
|
16
|
+
Given the user is on the login page
|
|
17
|
+
When they click the sign in button
|
|
18
|
+
Then the dashboard page should load
|
|
19
|
+
`,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
framework: 'playwright',
|
|
23
|
+
outputDir: 'generated',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
assert.deepStrictEqual(result.skipped, []);
|
|
27
|
+
assert.strictEqual(result.files.length, 1);
|
|
28
|
+
assert.strictEqual(result.files[0].path, 'generated/e2e/user-login.test.ts');
|
|
29
|
+
assert.match(result.files[0].content, /const \{ test \} = require\('@playwright\/test'\);/);
|
|
30
|
+
assert.match(result.files[0].content, /test\.afterEach\(async \(\{ page \}, testInfo\) =>/);
|
|
31
|
+
assert.match(result.files[0].content, /path: testInfo\.outputPath\('screenshot\.png'\)/);
|
|
32
|
+
assert.match(result.files[0].content, /test\('User can click the sign in button', async \(\{ page \}\) =>/);
|
|
33
|
+
assert.match(result.files[0].content, /test\('the dashboard page should load', async \(\{ page \}\) =>/);
|
|
34
|
+
assert.match(result.files[0].content, /Assert application logs remain clean for this flow\./);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('generates supertest test files with an afterEach artifact hook', () => {
|
|
38
|
+
const result = generateE2eTests({
|
|
39
|
+
scenarios: [
|
|
40
|
+
{
|
|
41
|
+
slug: 'health-check',
|
|
42
|
+
acceptanceCriteria: `
|
|
43
|
+
- [ ] GET /health endpoint returns 200
|
|
44
|
+
`,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
framework: 'supertest',
|
|
48
|
+
outputDir: '',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.deepStrictEqual(result.skipped, []);
|
|
52
|
+
assert.strictEqual(result.files.length, 1);
|
|
53
|
+
assert.strictEqual(result.files[0].path, 'e2e/health-check.test.ts');
|
|
54
|
+
assert.match(result.files[0].content, /const \{ afterEach, describe, it \} = require\('vitest'\);/);
|
|
55
|
+
assert.match(result.files[0].content, /const request = require\('supertest'\);/);
|
|
56
|
+
assert.match(result.files[0].content, /afterEach\(async \(\) =>/);
|
|
57
|
+
assert.match(result.files[0].content, /Capture a screenshot or response artifact after each test\./);
|
|
58
|
+
assert.match(result.files[0].content, /it\('GET \/health endpoint returns 200', async \(\) =>/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('skips manual criteria while still generating files for automatable tests', () => {
|
|
62
|
+
const result = generateE2eTests({
|
|
63
|
+
scenarios: [
|
|
64
|
+
{
|
|
65
|
+
title: 'Mixed Coverage',
|
|
66
|
+
planContent: `
|
|
67
|
+
Data is retained for 30 days.
|
|
68
|
+
- [ ] User can click the export button
|
|
69
|
+
`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
framework: 'playwright',
|
|
73
|
+
outputDir: 'artifacts',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assert.deepStrictEqual(result.skipped, [
|
|
77
|
+
{
|
|
78
|
+
slug: 'mixed-coverage',
|
|
79
|
+
criterion: 'Data is retained for 30 days.',
|
|
80
|
+
type: 'manual',
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
assert.strictEqual(result.files.length, 1);
|
|
84
|
+
assert.strictEqual(result.files[0].path, 'artifacts/e2e/mixed-coverage.test.ts');
|
|
85
|
+
assert.match(result.files[0].content, /test\('User can click the export button'/);
|
|
86
|
+
assert.doesNotMatch(result.files[0].content, /Data is retained for 30 days\./);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('omits files for scenarios that only produce manual criteria', () => {
|
|
90
|
+
const result = generateE2eTests({
|
|
91
|
+
scenarios: [
|
|
92
|
+
{
|
|
93
|
+
title: 'Manual Review',
|
|
94
|
+
planContent: 'Data is retained for 30 days.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
framework: 'supertest',
|
|
98
|
+
outputDir: 'tmp',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assert.deepStrictEqual(result.files, []);
|
|
102
|
+
assert.deepStrictEqual(result.skipped, [
|
|
103
|
+
{
|
|
104
|
+
slug: 'manual-review',
|
|
105
|
+
criterion: 'Data is retained for 30 days.',
|
|
106
|
+
type: 'manual',
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('throws for unsupported frameworks', () => {
|
|
112
|
+
assert.throws(
|
|
113
|
+
() => generateE2eTests({
|
|
114
|
+
scenarios: [{ title: 'User Login', planContent: '- [ ] User can click the sign in button' }],
|
|
115
|
+
framework: 'cypress',
|
|
116
|
+
outputDir: 'generated',
|
|
117
|
+
}),
|
|
118
|
+
/Unsupported E2E framework: cypress/
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|