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.
Files changed (86) hide show
  1. package/.claude/commands/tlc/autofix.md +34 -1
  2. package/.claude/commands/tlc/build.md +203 -27
  3. package/.claude/commands/tlc/ci.md +178 -414
  4. package/.claude/commands/tlc/coverage.md +34 -0
  5. package/.claude/commands/tlc/deploy.md +19 -6
  6. package/.claude/commands/tlc/discuss.md +34 -0
  7. package/.claude/commands/tlc/docs.md +35 -1
  8. package/.claude/commands/tlc/e2e.md +300 -0
  9. package/.claude/commands/tlc/edge-cases.md +35 -1
  10. package/.claude/commands/tlc/init.md +38 -8
  11. package/.claude/commands/tlc/issues.md +46 -0
  12. package/.claude/commands/tlc/new-project.md +46 -4
  13. package/.claude/commands/tlc/plan.md +76 -0
  14. package/.claude/commands/tlc/quick.md +33 -0
  15. package/.claude/commands/tlc/release.md +85 -135
  16. package/.claude/commands/tlc/restore.md +14 -0
  17. package/.claude/commands/tlc/review.md +80 -1
  18. package/.claude/commands/tlc/tlc.md +134 -0
  19. package/.claude/commands/tlc/verify.md +64 -65
  20. package/.claude/commands/tlc/watchci.md +10 -0
  21. package/.claude/hooks/tlc-block-tools.sh +13 -0
  22. package/.claude/hooks/tlc-session-init.sh +9 -0
  23. package/CODING-STANDARDS.md +35 -10
  24. package/package.json +1 -1
  25. package/server/lib/block-tools-hook.js +23 -0
  26. package/server/lib/e2e/acceptance-parser.js +132 -0
  27. package/server/lib/e2e/acceptance-parser.test.js +110 -0
  28. package/server/lib/e2e/framework-detector.js +47 -0
  29. package/server/lib/e2e/framework-detector.test.js +94 -0
  30. package/server/lib/e2e/log-assertions.js +107 -0
  31. package/server/lib/e2e/log-assertions.test.js +68 -0
  32. package/server/lib/e2e/test-generator.js +159 -0
  33. package/server/lib/e2e/test-generator.test.js +121 -0
  34. package/server/lib/e2e/verify-runner.js +191 -0
  35. package/server/lib/e2e/verify-runner.test.js +167 -0
  36. package/server/lib/github/config.js +458 -0
  37. package/server/lib/github/config.test.js +385 -0
  38. package/server/lib/github/gh-client.js +303 -0
  39. package/server/lib/github/gh-client.test.js +499 -0
  40. package/server/lib/github/gh-projects.js +594 -0
  41. package/server/lib/github/gh-projects.test.js +583 -0
  42. package/server/lib/github/index.js +19 -0
  43. package/server/lib/github/plan-sync.js +456 -0
  44. package/server/lib/github/plan-sync.test.js +805 -0
  45. package/server/lib/hooks/block-tools-hook.test.js +54 -0
  46. package/server/lib/orchestration/cli-dispatch.js +16 -1
  47. package/server/lib/orchestration/cli-dispatch.test.js +94 -8
  48. package/server/lib/orchestration/completion-checker.js +101 -0
  49. package/server/lib/orchestration/completion-checker.test.js +177 -0
  50. package/server/lib/orchestration/result-verifier.js +143 -0
  51. package/server/lib/orchestration/result-verifier.test.js +291 -0
  52. package/server/lib/orchestration/session-dispatcher.js +99 -0
  53. package/server/lib/orchestration/session-dispatcher.test.js +215 -0
  54. package/server/lib/orchestration/session-status.js +147 -0
  55. package/server/lib/orchestration/session-status.test.js +130 -0
  56. package/server/lib/release/agent-runner-updates.js +24 -0
  57. package/server/lib/release/agent-runner-updates.test.js +22 -0
  58. package/server/lib/release/changelog-generator.js +142 -0
  59. package/server/lib/release/changelog-generator.test.js +113 -0
  60. package/server/lib/release/ci-watcher.js +83 -0
  61. package/server/lib/release/ci-watcher.test.js +81 -0
  62. package/server/lib/release/health-checker.js +111 -0
  63. package/server/lib/release/health-checker.test.js +121 -0
  64. package/server/lib/release/release-pipeline.js +187 -0
  65. package/server/lib/release/release-pipeline.test.js +262 -0
  66. package/server/lib/release/version-bumper.js +183 -0
  67. package/server/lib/release/version-bumper.test.js +142 -0
  68. package/server/lib/routing-preamble.integration.test.js +12 -0
  69. package/server/lib/routing-preamble.js +13 -2
  70. package/server/lib/routing-preamble.test.js +49 -0
  71. package/server/lib/scaffolding/ci-detector.js +139 -0
  72. package/server/lib/scaffolding/ci-detector.test.js +198 -0
  73. package/server/lib/scaffolding/ci-scaffolder.js +347 -0
  74. package/server/lib/scaffolding/ci-scaffolder.test.js +157 -0
  75. package/server/lib/scaffolding/deploy-detector.js +135 -0
  76. package/server/lib/scaffolding/deploy-detector.test.js +106 -0
  77. package/server/lib/scaffolding/health-scaffold.js +374 -0
  78. package/server/lib/scaffolding/health-scaffold.test.js +99 -0
  79. package/server/lib/scaffolding/logger-scaffold.js +196 -0
  80. package/server/lib/scaffolding/logger-scaffold.test.js +146 -0
  81. package/server/lib/scaffolding/migration-detector.js +78 -0
  82. package/server/lib/scaffolding/migration-detector.test.js +127 -0
  83. package/server/lib/scaffolding/snapshot-manager.js +142 -0
  84. package/server/lib/scaffolding/snapshot-manager.test.js +225 -0
  85. package/server/lib/task-router-config.js +50 -20
  86. package/server/lib/task-router-config.test.js +29 -15
@@ -1,13 +1,13 @@
1
- # /tlc:verify - Human Acceptance Testing
1
+ # /tlc:verify - Acceptance Verification
2
2
 
3
- Verify the phase works as expected with your own eyes.
3
+ Verify the phase automatically from its plan acceptance criteria, then leave only the genuinely manual items for human follow-up.
4
4
 
5
5
  ## What This Does
6
6
 
7
- 1. Runs tests to confirm code works
8
- 2. Walks you through each deliverable
9
- 3. Captures issues for fixing
10
- 4. Marks phase as verified when done
7
+ 1. Reads acceptance criteria from the phase plan
8
+ 2. Runs `server/lib/e2e/verify-runner.js` against the project automatically
9
+ 3. Captures verified, failed, and manual-only criteria
10
+ 4. Marks the phase verified when all automatable criteria pass
11
11
 
12
12
  ## Usage
13
13
 
@@ -19,78 +19,85 @@ If no phase number, auto-detect current phase.
19
19
 
20
20
  ## Process
21
21
 
22
- ### Step 1: Run Tests
22
+ ### Step 1: Load the Phase Plan
23
23
 
24
24
  ```bash
25
- npm test # or pytest, go test, etc.
25
+ cat .planning/phases/{N}-PLAN.md
26
26
  ```
27
27
 
28
- - All pass Continue to human verification
29
- - ❌ Some fail → Report failures, suggest fixing first
28
+ Pass the detected plan path into `server/lib/e2e/verify-runner.js`:
30
29
 
31
- ### Step 2: Load Phase Deliverables
30
+ ```js
31
+ const { runVerification } = require('./server/lib/e2e/verify-runner.js');
32
+ ```
32
33
 
33
- Read from `.planning/phases/{N}-PLAN.md`:
34
- - Extract acceptance criteria from each task
35
- - Build verification checklist
34
+ The runner must be used by default instead of asking the user to validate each criterion manually.
36
35
 
37
- ### Step 3: Walk Through Each Deliverable
36
+ ### Step 2: Run Automated Verification
38
37
 
39
- For each testable feature:
38
+ Call:
40
39
 
40
+ ```js
41
+ await runVerification({
42
+ planPath,
43
+ projectDir,
44
+ exec,
45
+ fs,
46
+ });
41
47
  ```
42
- Phase 1: Authentication
43
48
 
44
- Verifying 3 deliverables:
49
+ Behavior:
50
+ - Parse acceptance criteria from the plan file
51
+ - Detect the available E2E framework
52
+ - Run the matching E2E command automatically
53
+ - Return `{ verified, manual, failed, results }`
45
54
 
46
- [1/3] User login with email/password
55
+ ### Step 3: Interpret Results
47
56
 
48
- Can you:
49
- 1. Go to /login
50
- 2. Enter valid credentials
51
- 3. Get redirected to dashboard
57
+ Report each criterion with its status:
52
58
 
53
- Works as expected? (Y/n/describe issue)
54
- >
55
59
  ```
60
+ verified: 5
61
+ manual: 1
62
+ failed: 0
56
63
 
57
- ### Step 4: Handle Issues
58
-
59
- If user reports issue:
64
+ - User can click the export button → verified
65
+ - GET /health endpoint returns 200 → verified
66
+ - Data is retained for 30 days. → manual
60
67
  ```
61
- > n - login works but no error message for wrong password
62
68
 
63
- Got it. Creating fix task:
69
+ Only criteria returned as `manual` should be handed back to a human for confirmation.
64
70
 
65
- Issue: No error message for wrong password
66
- Location: Login form
67
- Expected: Show "Invalid credentials" message
71
+ ### Step 4: Handle Failures
68
72
 
69
- Add to current phase as fix task? (Y/n)
73
+ If any criteria fail:
74
+ ```
75
+ Verification failed:
76
+ - User login redirects to dashboard → failed
77
+ logErrors: Timeout while waiting for dashboard
70
78
  ```
71
79
 
80
+ Fix the implementation or tests, then rerun `runVerification(...)`.
81
+
72
82
  ### Step 5: Mark Verified or Loop
73
83
 
74
84
  **All verified:**
75
85
  ```
76
86
  ✅ Phase 1 verified
77
87
 
78
- All 3 deliverables confirmed working.
88
+ All automatable acceptance criteria passed.
79
89
 
80
90
  Creating .planning/phases/1-VERIFIED.md
81
-
82
- Ready for next phase? (Y/n)
83
91
  ```
84
92
 
85
93
  **Issues found:**
86
94
  ```
87
95
  Phase 1 verification incomplete
88
96
 
89
- Issues found:
90
- 1. No error message for wrong password
91
- 2. Session doesn't persist on refresh
97
+ Automated verification failed for 2 criteria.
98
+ 1 manual criterion still requires human confirmation.
92
99
 
93
- Fix tasks added to phase. Run /tlc:build 1 to implement fixes.
100
+ Run /tlc:build 1 to implement fixes, then /tlc:verify again.
94
101
  ```
95
102
 
96
103
  ### Step 6: Create Verification Record
@@ -106,7 +113,7 @@ Verified: {date}
106
113
 
107
114
  - [x] User login with email/password
108
115
  - [x] Session persistence
109
- - [x] Logout functionality
116
+ - [ ] Data retention wording confirmed manually
110
117
 
111
118
  ## Issues Found & Fixed
112
119
 
@@ -118,42 +125,34 @@ Verified: {date}
118
125
  {Any additional observations}
119
126
  ```
120
127
 
121
- ## Why Human Verification?
128
+ ## Why This Order?
122
129
 
123
- **Tests verify code works.**
124
- **You verify it works the way you wanted.**
130
+ **Automated verification should handle every criterion it can.**
131
+ **Humans should only validate what cannot be expressed as a reliable automated check.**
125
132
 
126
- Tests catch:
133
+ Automation catches:
127
134
  - Logic errors
128
135
  - Regressions
129
- - Edge cases
136
+ - Broken user flows
130
137
 
131
- You catch:
132
- - "Technically correct but wrong layout"
133
- - "Works but confusing UX"
134
- - "Missing something I forgot to specify"
138
+ Humans catch:
139
+ - Ambiguous qualitative requirements
140
+ - Visual or product judgment not encoded in tests
141
+ - Criteria intentionally marked as manual
135
142
 
136
- Both matter.
143
+ Both still matter, but `/tlc:verify` should not default to a manual checklist when the runner can verify the phase directly.
137
144
 
138
145
  ## Example
139
146
 
140
147
  ```
141
148
  > /tlc:verify 1
142
149
 
143
- Running tests... ✅ 11 passing
150
+ Loading phase plan... ✅
151
+ Running verify-runner... ✅
144
152
 
145
- Phase 1: Authentication
146
-
147
- [1/3] Login with email/password
148
- Works? (Y/n) > y
149
-
150
- [2/3] Session persists across page refresh
151
- Works? (Y/n) > y
152
-
153
- [3/3] Logout clears session
154
- Works? (Y/n) > y
153
+ verified: 3
154
+ manual: 0
155
+ failed: 0
155
156
 
156
157
  ✅ Phase 1 verified!
157
-
158
- Moving to Phase 2: Dashboard
159
158
  ```
@@ -157,3 +157,13 @@ Run #4522 (CI) — in progress...
157
157
  - After `/tlc:build` when you've pushed your work
158
158
  - When CI keeps failing and you want the fix loop automated
159
159
  - Pair with `/tlc:e2e-verify` for full verification after CI is green
160
+
161
+ ## `--detached`
162
+
163
+ `/tlc:watchci --detached` dispatches CI watching through the local orchestrator instead of keeping the current session attached.
164
+
165
+ - Send a `POST` request to `http://localhost:3100`
166
+ - Ask the orchestrator to run the CI watch workflow for the current project/branch
167
+ - Return the dispatched session details so the user can check status later
168
+
169
+ Detached mode changes how the command is launched, not what it does. The orchestrated worker should still follow the same watch/fix loop defined above.
@@ -25,11 +25,24 @@ case "$TOOL" in
25
25
  ExitPlanMode)
26
26
  REASON="BLOCKED by TLC. Plans are approved via /tlc:build, not ExitPlanMode."
27
27
  ;;
28
+ Agent)
29
+ FLAG_FILE=".tlc/.build-routing-active"
30
+ if [[ -f "$FLAG_FILE" ]]; then
31
+ PROVIDER=$(tr -d '[:space:]' < "$FLAG_FILE")
32
+ if [[ -n "$PROVIDER" && "$PROVIDER" != "claude" ]]; then
33
+ REASON="BLOCKED by TLC. Agent is routed to ${PROVIDER} while ${FLAG_FILE} is active."
34
+ fi
35
+ fi
36
+ ;;
28
37
  *)
29
38
  exit 0
30
39
  ;;
31
40
  esac
32
41
 
42
+ if [[ -z "$REASON" ]]; then
43
+ exit 0
44
+ fi
45
+
33
46
  cat <<EOF
34
47
  {
35
48
  "hookSpecificOutput": {
@@ -43,6 +43,15 @@ else
43
43
  done
44
44
  fi
45
45
 
46
+ # --- tlc-core Orchestrator ---
47
+ TLC_CORE_PORT="${TLC_CORE_PORT:-3100}"
48
+ if curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/health" > /dev/null 2>&1; then
49
+ SESSIONS=$(curl -sf --max-time 1 "http://localhost:${TLC_CORE_PORT}/sessions" 2>/dev/null | jq "length" 2>/dev/null || echo "0")
50
+ echo "tlc-core orchestrator: healthy (${SESSIONS} sessions)"
51
+ else
52
+ echo "tlc-core orchestrator: not running. Agent dispatch will fall back to local mode."
53
+ fi
54
+
46
55
  # ─── Memory System Init ─────────────────────────────
47
56
  mkdir -p "$PROJECT_DIR/.tlc/memory/team/decisions" \
48
57
  "$PROJECT_DIR/.tlc/memory/team/gotchas" \
@@ -425,16 +425,41 @@ export class AppError extends Error {
425
425
  }
426
426
  }
427
427
 
428
- export class NotFoundError extends AppError {
429
- constructor(entity: string, id: string) {
430
- super(`${entity} not found: ${id}`, 'NOT_FOUND', 404);
431
- }
432
- }
433
- ```
434
-
435
- ---
436
-
437
- ## 13. Deprecated Re-exports
428
+ export class NotFoundError extends AppError {
429
+ constructor(entity: string, id: string) {
430
+ super(`${entity} not found: ${id}`, 'NOT_FOUND', 404);
431
+ }
432
+ }
433
+ ```
434
+
435
+ ## Observability
436
+
437
+ ### Structured Logging
438
+ - Use structured JSON logging (`pino` for Node.js, `structlog` for Python, `slog` for Go)
439
+ - Log levels: `ERROR` (needs attention), `WARN` (degraded), `INFO` (business events), `DEBUG` (troubleshooting)
440
+ - Every log must include: `level`, `message`, `context` object, `timestamp`
441
+ - No bare `console.log` for errors; use the project logger
442
+ - No sensitive data in logs (`passwords`, `tokens`, `PII`)
443
+
444
+ ### Connection State Logging
445
+ - Every external connection (DB, Redis, WebSocket, Docker, HTTP client, queue) must log: connect, disconnect, reconnect, and failure
446
+ - Connection state changes are `INFO` level, not `DEBUG`
447
+ - Include connection target in log context (`host`, `port`, database name)
448
+
449
+ ### Health Endpoints
450
+ - Every server project must expose a health endpoint
451
+ - Docker Compose: `GET /health` -> `{ status, dependencies, uptime }`
452
+ - Kubernetes: `GET /healthz` + `/ready` + `/startup` with appropriate checks
453
+ - Health checks must have timeouts (never hang)
454
+
455
+ ### Startup Health
456
+ - Log the state of every dependency on startup
457
+ - `"Connected to Postgres"`, `"Docker socket accessible"`, `"Redis: connection refused"`
458
+ - Never start silently; if a dependency is down, log it at `WARN` level
459
+
460
+ ---
461
+
462
+ ## 13. Deprecated Re-exports
438
463
 
439
464
  When migrating, old file locations should become deprecated re-exports:
440
465
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "2.4.10",
3
+ "version": "2.6.0",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc-claude-code": "./bin/install.js",
@@ -0,0 +1,23 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const BUILD_ROUTING_FLAG = path.join('.tlc', '.build-routing-active');
5
+
6
+ function shouldBlockAgent(projectRoot = process.cwd()) {
7
+ const flagPath = path.join(projectRoot, BUILD_ROUTING_FLAG);
8
+ if (!fs.existsSync(flagPath)) {
9
+ return null;
10
+ }
11
+
12
+ const provider = fs.readFileSync(flagPath, 'utf8').trim();
13
+ if (!provider || provider === 'claude') {
14
+ return null;
15
+ }
16
+
17
+ return provider;
18
+ }
19
+
20
+ module.exports = {
21
+ BUILD_ROUTING_FLAG,
22
+ shouldBlockAgent,
23
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Parse acceptance criteria from plan content into normalized E2E records.
3
+ */
4
+
5
+ const CHECKBOX_REGEX = /^\s*[-*]\s*\[[ xX]?\]\s+(.+?)\s*$/;
6
+ const SHOULD_REGEX = /^(?:[-*]\s*)?(?:it\s+)?should\s+(.+)$/i;
7
+ const GIVEN_REGEX = /^\s*(?:[-*]\s*)?(?:\*\*)?given(?:\*\*)?\s*:?\s+(.+?)\s*$/i;
8
+ const WHEN_REGEX = /^\s*(?:[-*]\s*)?(?:\*\*)?when(?:\*\*)?\s*:?\s+(.+?)\s*$/i;
9
+ const THEN_REGEX = /^\s*(?:[-*]\s*)?(?:\*\*)?then(?:\*\*)?\s*:?\s+(.+?)\s*$/i;
10
+
11
+ function detectType(text) {
12
+ if (/\b(post|get|api|endpoint)\b/i.test(text)) {
13
+ return 'api';
14
+ }
15
+
16
+ if (/\b(click|page|button)\b/i.test(text)) {
17
+ return 'ui';
18
+ }
19
+
20
+ return 'manual';
21
+ }
22
+
23
+ function normalizeCriterion(text) {
24
+ return String(text || '')
25
+ .trim()
26
+ .replace(/\s+/g, ' ');
27
+ }
28
+
29
+ function isIgnorableLine(line) {
30
+ const trimmed = line.trim();
31
+
32
+ if (!trimmed) return true;
33
+ if (/^#{1,6}\s/.test(trimmed)) return true;
34
+ if (/^\*\*acceptance criteria:?\*\*$/i.test(trimmed)) return true;
35
+ if (/^acceptance criteria:?$/i.test(trimmed)) return true;
36
+ if (/^-{3,}$/.test(trimmed)) return true;
37
+
38
+ return false;
39
+ }
40
+
41
+ function parseAcceptanceCriteria({ planContent }) {
42
+ if (!planContent || typeof planContent !== 'string') {
43
+ return [];
44
+ }
45
+
46
+ const lines = planContent.replace(/\r\n/g, '\n').split('\n');
47
+ const criteria = [];
48
+ const consumed = new Set();
49
+
50
+ for (let index = 0; index < lines.length; index += 1) {
51
+ const givenMatch = lines[index].match(GIVEN_REGEX);
52
+ if (!givenMatch) continue;
53
+
54
+ let cursor = index + 1;
55
+ while (cursor < lines.length && !lines[cursor].trim()) {
56
+ cursor += 1;
57
+ }
58
+
59
+ const whenMatch = cursor < lines.length ? lines[cursor].match(WHEN_REGEX) : null;
60
+ if (!whenMatch) continue;
61
+
62
+ cursor += 1;
63
+ while (cursor < lines.length && !lines[cursor].trim()) {
64
+ cursor += 1;
65
+ }
66
+
67
+ const thenMatch = cursor < lines.length ? lines[cursor].match(THEN_REGEX) : null;
68
+ if (!thenMatch) continue;
69
+
70
+ const given = normalizeCriterion(givenMatch[1]);
71
+ const when = normalizeCriterion(whenMatch[1]);
72
+ const then = normalizeCriterion(thenMatch[1]);
73
+ const scenario = `Given ${given} When ${when} Then ${then}`;
74
+
75
+ criteria.push({
76
+ criterion: then,
77
+ type: detectType(scenario),
78
+ scenario,
79
+ });
80
+
81
+ consumed.add(index);
82
+ consumed.add(cursor - 1);
83
+ consumed.add(cursor);
84
+ index = cursor;
85
+ }
86
+
87
+ for (let index = 0; index < lines.length; index += 1) {
88
+ if (consumed.has(index)) continue;
89
+
90
+ const line = lines[index];
91
+ if (isIgnorableLine(line)) continue;
92
+
93
+ const checkboxMatch = line.match(CHECKBOX_REGEX);
94
+ if (checkboxMatch) {
95
+ const criterion = normalizeCriterion(checkboxMatch[1]);
96
+ criteria.push({
97
+ criterion,
98
+ type: detectType(criterion),
99
+ scenario: null,
100
+ });
101
+ continue;
102
+ }
103
+
104
+ const shouldMatch = line.match(SHOULD_REGEX);
105
+ if (shouldMatch) {
106
+ const criterion = `should ${normalizeCriterion(shouldMatch[1])}`;
107
+ criteria.push({
108
+ criterion,
109
+ type: detectType(criterion),
110
+ scenario: null,
111
+ });
112
+ continue;
113
+ }
114
+
115
+ const trimmed = line.trim();
116
+ if (/^(?:[-*]\s*)?(?:given|when|then)\b/i.test(trimmed)) {
117
+ continue;
118
+ }
119
+
120
+ criteria.push({
121
+ criterion: normalizeCriterion(trimmed.replace(/^[-*]\s*/, '')),
122
+ type: detectType(trimmed),
123
+ scenario: null,
124
+ });
125
+ }
126
+
127
+ return criteria;
128
+ }
129
+
130
+ module.exports = {
131
+ parseAcceptanceCriteria,
132
+ };
@@ -0,0 +1,110 @@
1
+ import { describe, it } from 'vitest';
2
+ const assert = require('node:assert');
3
+
4
+ const { parseAcceptanceCriteria } = require('./acceptance-parser.js');
5
+
6
+ describe('parseAcceptanceCriteria', () => {
7
+ it('extracts checkbox criteria and infers types', () => {
8
+ const result = parseAcceptanceCriteria({
9
+ planContent: `
10
+ **Acceptance Criteria:**
11
+ - [ ] User can click the save button
12
+ - [x] GET /health endpoint returns 200
13
+ `,
14
+ });
15
+
16
+ assert.deepStrictEqual(result, [
17
+ {
18
+ criterion: 'User can click the save button',
19
+ type: 'ui',
20
+ scenario: null,
21
+ },
22
+ {
23
+ criterion: 'GET /health endpoint returns 200',
24
+ type: 'api',
25
+ scenario: null,
26
+ },
27
+ ]);
28
+ });
29
+
30
+ it('extracts Given/When/Then scenarios', () => {
31
+ const result = parseAcceptanceCriteria({
32
+ planContent: `
33
+ Given the user is on the login page
34
+ When they click the sign in button
35
+ Then the dashboard page should load
36
+ `,
37
+ });
38
+
39
+ assert.deepStrictEqual(result, [
40
+ {
41
+ criterion: 'the dashboard page should load',
42
+ type: 'ui',
43
+ scenario: 'Given the user is on the login page When they click the sign in button Then the dashboard page should load',
44
+ },
45
+ ]);
46
+ });
47
+
48
+ it('extracts should statements from bullets or plain text', () => {
49
+ const result = parseAcceptanceCriteria({
50
+ planContent: `
51
+ should call the API with the updated payload
52
+ - should show the success page
53
+ `,
54
+ });
55
+
56
+ assert.deepStrictEqual(result, [
57
+ {
58
+ criterion: 'should call the API with the updated payload',
59
+ type: 'api',
60
+ scenario: null,
61
+ },
62
+ {
63
+ criterion: 'should show the success page',
64
+ type: 'ui',
65
+ scenario: null,
66
+ },
67
+ ]);
68
+ });
69
+
70
+ it('falls back to free-text and marks ambiguous criteria as manual', () => {
71
+ const result = parseAcceptanceCriteria({
72
+ planContent: `
73
+ Acceptance Criteria:
74
+ Data is retained for 30 days.
75
+ `,
76
+ });
77
+
78
+ assert.deepStrictEqual(result, [
79
+ {
80
+ criterion: 'Data is retained for 30 days.',
81
+ type: 'manual',
82
+ scenario: null,
83
+ },
84
+ ]);
85
+ });
86
+
87
+ it('ignores markdown structure around parsed lines', () => {
88
+ const result = parseAcceptanceCriteria({
89
+ planContent: `
90
+ ## Task 4
91
+
92
+ **Acceptance Criteria:**
93
+
94
+ Given an admin user
95
+ When they POST /users
96
+ Then the API returns 201
97
+
98
+ ---
99
+ `,
100
+ });
101
+
102
+ assert.deepStrictEqual(result, [
103
+ {
104
+ criterion: 'the API returns 201',
105
+ type: 'api',
106
+ scenario: 'Given an admin user When they POST /users Then the API returns 201',
107
+ },
108
+ ]);
109
+ });
110
+ });
@@ -0,0 +1,47 @@
1
+ const path = require('node:path');
2
+
3
+ function hasDependency(packageJson, dependencyName) {
4
+ const dependencyGroups = [
5
+ packageJson.dependencies,
6
+ packageJson.devDependencies,
7
+ packageJson.peerDependencies,
8
+ packageJson.optionalDependencies,
9
+ ];
10
+
11
+ return dependencyGroups.some(
12
+ (dependencies) =>
13
+ dependencies &&
14
+ typeof dependencies === 'object' &&
15
+ Object.prototype.hasOwnProperty.call(dependencies, dependencyName)
16
+ );
17
+ }
18
+
19
+ function readPackageJson(packageJsonPath, fs) {
20
+ try {
21
+ const packageJsonText = fs.readFileSync(packageJsonPath, 'utf8');
22
+ return JSON.parse(packageJsonText);
23
+ } catch (error) {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function detectE2eFramework({ projectDir, fs }) {
29
+ const packageJsonPath = path.join(projectDir, 'package.json');
30
+ const playwrightConfigPath = path.join(projectDir, 'playwright.config.ts');
31
+ const packageJson = readPackageJson(packageJsonPath, fs);
32
+ const hasPlaywrightConfig = fs.existsSync(playwrightConfigPath);
33
+
34
+ if (hasPlaywrightConfig) {
35
+ return { framework: 'playwright' };
36
+ }
37
+
38
+ if (packageJson && hasDependency(packageJson, 'supertest')) {
39
+ return { framework: 'supertest' };
40
+ }
41
+
42
+ return { framework: 'none' };
43
+ }
44
+
45
+ module.exports = {
46
+ detectE2eFramework,
47
+ };