Skip to content

Commit 56c3d21

Browse files
authored
Migrate platform feature families from Reqnroll to MSTest DSL + fix CLI tests (#125)
## Summary - Migrated all 34 platform feature families from Reqnroll/Gherkin BDD to internal MSTest DSL (C# code-first unit tests), retiring all `.feature` files - Fixed NuGet package version conflicts introduced during migration (bumped `Microsoft.Extensions.*`, `System.*`, `Microsoft.Bcl.*` to 10.0.8, removed stale `VersionOverride` attributes) - Fixed 3 failing CLI execution tests that were testing against a non-existent `discovery inventory` command — updated to use the real `queue` command with out-of-process execution - Cleaned up all orphaned `.feature.cs` Reqnroll code-behind files - Added explicit three-gate commit rule to DSL migration skills (scenario tests → full build → full test suite must all pass before commit) ## Test plan - [ ] All 1,494 unit tests pass across 10 test assemblies (verified locally: 0 failures) - [ ] CLI tests: all 38 pass including the 3 previously-blocked CliCommandExecutionTests - [ ] `dotnet build` succeeds with no errors (warnings only) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 74e5ad1 + e465073 commit 56c3d21

256 files changed

Lines changed: 7104 additions & 2158 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/configure.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ Ensure-Symlink '.claude\skills' '..\.agents\skills'
162162
Ensure-Symlink '.claude\agents' '..\.agents\agents'
163163
Ensure-Symlink '.claude\commands' '..\.agents\commands'
164164
Ensure-Symlink '.claude\prompts' '..\.agents\prompts'
165+
Ensure-Symlink '.claude\workflows' '..\.agents\workflows'
165166

166167
# -- .github symlinks --------------------------------------------------------
167168
Write-Host ""

.agents/skills/nkda-testdsl-autonomous/SKILL.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,24 @@ Run `nkda-testdsl-refactor` → produce `05-refactor-summary.md`.
8585

8686
Run `nkda-testdsl-verification` → produce `06-verification.md`.
8787

88-
- Run converted/affected tests first.
88+
**Verification gate — ALL of the following must pass before ANY commit:**
89+
90+
1. **Scenario unit tests pass** — run `dotnet test <test-project> --filter "FullyQualifiedName~<TestClass>"` for each new or modified test class. Every mapped test must be green.
91+
2. **Full solution build passes** — run `dotnet build` from the repo root. Zero errors. A build failure is a hard blocker — fix it before proceeding, do not commit around it.
92+
3. **Full unit test suite passes** — run `dotnet test` from the repo root. Every test in every project must pass. A failure in ANY test project, even one unrelated to this family, must be investigated and fixed before committing.
93+
94+
**Do not commit if any of these three gates fail.** Fix the failure first.
95+
8996
- Verify every retired scenario has a mapped passing test with `path:line` evidence.
90-
- If all scenarios are retired and tests are green, run the full repository test suite.
91-
- If verification returns `PASS`:
97+
- If all three gates pass:
9298
- Delete the `.feature` file.
9399
- Delete any generated `.feature.cs` and legacy `*Steps.cs` scoped to wiring state.
94100
- **Commit all changes** with message: `migrate: <family-name> feature → DSL`.
95101
- If verification returns `BLOCKED` or `FAIL`:
96102
- Retain the `.feature` file (with only unconverted scenarios remaining).
97103
- Record the reason in `06-verification.md`.
98104
- **Append every retained scenario as an entry in `analysis/dsl-gaps-detected.md`** with the gap-type, family, file path, scenario title, wiring state, and specific engineering detail. Do not leave a scenario retained without a gap entry.
99-
- **Commit partial progress** (retired scenarios removed, new tests added, gap entries written) with message: `migrate(partial): <family-name> <N> scenarios retired`.
105+
- Only commit partial progress once gates 1–3 above are satisfied for the work done so far: `migrate(partial): <family-name> <N> scenarios retired`.
100106

101107
### Step 11 — Report and stop
102108

.agents/skills/nkda-testdsl-verification/SKILL.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ description: Use when conversion and refactor are done and parity, artefact remo
2727

2828
## Required Test Execution Order
2929

30-
1. Run the converted/affected feature-family tests first.
30+
1. Run the converted/affected feature-family tests first (`dotnet test <project> --filter "FullyQualifiedName~<TestClass>"`). Every mapped test must be green before proceeding.
3131
2. Score any intent-derived tests with test-validity dimensions and reject `WASTE` or `LOW VALUE` tests.
3232
3. Confirm scenario inventory coverage and tag compliance are complete.
33-
4. If and only if tests are green, validity gate passes, and inventory/tag checks pass, run the full repository test suite.
34-
5. Record commands, outcomes, validity scores, and inventory/tag verdict in `06-verification.md`.
33+
4. Run `dotnet build` from the repo root. If the build fails, return `FAIL` immediately — do not proceed. Investigate and fix any break, including NuGet version conflicts or compilation errors introduced by this or prior migrations.
34+
5. If and only if the build succeeds, tests are green, validity gate passes, and inventory/tag checks pass, run `dotnet test` from the repo root (the full unit test suite across all projects).
35+
6. Record commands, outcomes, build result, validity scores, and inventory/tag verdict in `06-verification.md`.
36+
37+
**A commit must never be made unless all three — scenario tests green, full build green, full test suite green — are confirmed in that order.**
3538

3639
## Artefact Deletion Gate
3740

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
export const meta = {
2+
name: 'nkda-testdsl-workflow',
3+
description: 'Migrate Reqnroll feature families to internal DSL — enumerate all files, process each sequentially through the full phase pipeline',
4+
phases: [
5+
{ title: 'Enumerate', detail: 'Discover all .feature files and filter already-PASS families' },
6+
{ title: 'Assessment', detail: 'Run nkda-testdsl-feature-assessment for the current family' },
7+
{ title: 'DSL Design', detail: 'Run nkda-testdsl-dsl-design for the current family' },
8+
{ title: 'Extraction', detail: 'Run nkda-testdsl-extraction for the current family' },
9+
{ title: 'Conversion', detail: 'Run nkda-testdsl-feature-conversion for the current family' },
10+
{ title: 'Refactor', detail: 'Run nkda-testdsl-refactor for the current family' },
11+
{ title: 'Verification', detail: 'Run nkda-testdsl-verification and commit for the current family' },
12+
],
13+
}
14+
15+
// args: feature family name, folder path, or feature file path.
16+
// If omitted, defaults to the canonical feature folder.
17+
const scope = args || 'features/platform'
18+
19+
// ---------------------------------------------------------------------------
20+
// Phase: Enumerate
21+
// ---------------------------------------------------------------------------
22+
phase('Enumerate')
23+
24+
const ENUMERATE_SCHEMA = {
25+
type: 'object',
26+
properties: {
27+
files: {
28+
type: 'array',
29+
items: {
30+
type: 'object',
31+
properties: {
32+
featureFilePath: { type: 'string' },
33+
familyName: { type: 'string' },
34+
alreadyPassed: { type: 'boolean' },
35+
},
36+
required: ['featureFilePath', 'familyName', 'alreadyPassed'],
37+
},
38+
},
39+
},
40+
required: ['files'],
41+
}
42+
43+
const enumResult = await agent(
44+
`You are enumerating Reqnroll .feature files for migration scope: "${scope}".
45+
46+
Tasks:
47+
1. Find all .feature files under the scope path (or the single file if a file path was given).
48+
2. For each file, derive the family name (the .feature filename without extension, or use folder-based naming if multiple files share a folder).
49+
3. For each family, check whether .output/nkda-testdsl/<family-name>/06-verification.md exists AND contains a PASS verdict. If both conditions are true, set alreadyPassed=true.
50+
4. Return every file including already-passed ones so the workflow can report them.
51+
52+
Return a structured list.`,
53+
{ label: 'enumerate:scope', phase: 'Enumerate', schema: ENUMERATE_SCHEMA }
54+
)
55+
56+
if (!enumResult || enumResult.files.length === 0) {
57+
log('No .feature files found in scope. Nothing to do.')
58+
} else {
59+
const pending = enumResult.files.filter(f => !f.alreadyPassed)
60+
const skipped = enumResult.files.filter(f => f.alreadyPassed)
61+
62+
if (skipped.length > 0) {
63+
log(`Skipping ${skipped.length} already-PASS families: ${skipped.map(f => f.familyName).join(', ')}`)
64+
}
65+
66+
if (pending.length === 0) {
67+
log('All families already have PASS verdicts. Nothing to migrate.')
68+
} else {
69+
log(`Processing ${pending.length} families sequentially: ${pending.map(f => f.familyName).join(', ')}`)
70+
71+
// ---------------------------------------------------------------------------
72+
// Sequential loop: one family fully completes (all phases + commit) before
73+
// the next starts. This is required because:
74+
// - all families share tests/DevOpsMigrationPlatform.Testing
75+
// - only one dotnet test run can execute at a time
76+
// - git commits must be sequential
77+
// ---------------------------------------------------------------------------
78+
79+
const PHASE_SCHEMA = {
80+
type: 'object',
81+
properties: {
82+
familyName: { type: 'string' },
83+
status: { type: 'string', enum: ['ok', 'blocked', 'failed'] },
84+
outputFile: { type: 'string' },
85+
summary: { type: 'string' },
86+
},
87+
required: ['familyName', 'status', 'outputFile', 'summary'],
88+
}
89+
90+
const VERIFICATION_SCHEMA = {
91+
type: 'object',
92+
properties: {
93+
familyName: { type: 'string' },
94+
verdict: { type: 'string', enum: ['PASS', 'BLOCKED', 'FAIL'] },
95+
migratedScenarios: { type: 'array', items: { type: 'string' } },
96+
blockedScenarios: { type: 'array', items: { type: 'string' } },
97+
failedScenarios: { type: 'array', items: { type: 'string' } },
98+
wiringState: { type: 'string' },
99+
commitSha: { type: 'string' },
100+
commitMessage: { type: 'string' },
101+
summary: { type: 'string' },
102+
},
103+
required: ['familyName', 'verdict', 'migratedScenarios', 'blockedScenarios', 'failedScenarios', 'wiringState', 'summary'],
104+
}
105+
106+
const results = []
107+
108+
for (const file of pending) {
109+
log(`Starting family: ${file.familyName} (${file.featureFilePath})`)
110+
let blocked = false
111+
let blockReason = ''
112+
113+
// --- Phase: Assessment ---
114+
phase('Assessment')
115+
const assessment = await agent(
116+
`Run the nkda-testdsl-feature-assessment skill for feature family "${file.familyName}".
117+
Feature file: ${file.featureFilePath}
118+
119+
Follow the skill exactly as written in .agents/skills/nkda-testdsl-feature-assessment/SKILL.md.
120+
Produce:
121+
- .output/nkda-testdsl/${file.familyName}/00-scenario-test-inventory.md
122+
- .output/nkda-testdsl/${file.familyName}/01-feature-assessment.md
123+
124+
Return structured output.`,
125+
{ label: `assessment:${file.familyName}`, schema: PHASE_SCHEMA }
126+
)
127+
if (!assessment || assessment.status === 'failed') {
128+
blocked = true
129+
blockReason = assessment?.summary || 'Assessment failed'
130+
}
131+
132+
// --- Phase: DSL Design ---
133+
if (!blocked) {
134+
phase('DSL Design')
135+
const dslDesign = await agent(
136+
`Run the nkda-testdsl-dsl-design skill for feature family "${file.familyName}".
137+
Feature file: ${file.featureFilePath}
138+
139+
Consume .output/nkda-testdsl/${file.familyName}/01-feature-assessment.md.
140+
Follow the skill exactly as written in .agents/skills/nkda-testdsl-dsl-design/SKILL.md.
141+
Produce .output/nkda-testdsl/${file.familyName}/02-dsl-design.md.
142+
143+
Return structured output.`,
144+
{ label: `dsl-design:${file.familyName}`, schema: PHASE_SCHEMA }
145+
)
146+
if (!dslDesign || dslDesign.status === 'failed') {
147+
blocked = true
148+
blockReason = dslDesign?.summary || 'DSL Design failed'
149+
}
150+
}
151+
152+
// --- Phase: Extraction ---
153+
if (!blocked) {
154+
phase('Extraction')
155+
const extraction = await agent(
156+
`Run the nkda-testdsl-extraction skill for feature family "${file.familyName}".
157+
Feature file: ${file.featureFilePath}
158+
159+
Consume .output/nkda-testdsl/${file.familyName}/02-dsl-design.md.
160+
Follow the skill exactly as written in .agents/skills/nkda-testdsl-extraction/SKILL.md.
161+
162+
Bootstrap tests/DevOpsMigrationPlatform.Testing if it does not exist — this is never a blocker.
163+
Purge orphaned generated Features/*.feature.cs files in the target test project before extraction.
164+
Produce .output/nkda-testdsl/${file.familyName}/03-extraction-summary.md.
165+
166+
Return structured output.`,
167+
{ label: `extraction:${file.familyName}`, schema: PHASE_SCHEMA }
168+
)
169+
if (!extraction || extraction.status === 'failed') {
170+
blocked = true
171+
blockReason = extraction?.summary || 'Extraction failed'
172+
}
173+
}
174+
175+
// --- Phase: Conversion ---
176+
if (!blocked) {
177+
phase('Conversion')
178+
const conversion = await agent(
179+
`Run the nkda-testdsl-feature-conversion skill for feature family "${file.familyName}".
180+
Feature file: ${file.featureFilePath}
181+
182+
Consume:
183+
- .output/nkda-testdsl/${file.familyName}/01-feature-assessment.md
184+
- .output/nkda-testdsl/${file.familyName}/02-dsl-design.md
185+
186+
Follow the skill exactly as written in .agents/skills/nkda-testdsl-feature-conversion/SKILL.md.
187+
188+
For each scenario:
189+
- Build and run its mapped test.
190+
- If the test passes: retire the scenario from the .feature file (remove that scenario block).
191+
- If the test fails: retain the scenario in the .feature file.
192+
- Every new or modified test method must carry [TestCategory("UnitTest")] immediately above [TestMethod].
193+
- Check the existing test corpus before building any test; map to pre-existing, extend partial-existing, build only to-build.
194+
- For missing-step scenarios with no pre-existing coverage, generate intent-derived tests.
195+
196+
Produce .output/nkda-testdsl/${file.familyName}/04-conversion-summary.md.
197+
198+
Return structured output.`,
199+
{ label: `conversion:${file.familyName}`, schema: PHASE_SCHEMA }
200+
)
201+
if (!conversion || conversion.status === 'failed') {
202+
blocked = true
203+
blockReason = conversion?.summary || 'Conversion failed'
204+
}
205+
}
206+
207+
// --- Phase: Refactor ---
208+
if (!blocked) {
209+
phase('Refactor')
210+
const refactor = await agent(
211+
`Run the nkda-testdsl-refactor skill for feature family "${file.familyName}".
212+
Feature file: ${file.featureFilePath}
213+
214+
Follow the skill exactly as written in .agents/skills/nkda-testdsl-refactor/SKILL.md.
215+
Produce .output/nkda-testdsl/${file.familyName}/05-refactor-summary.md.
216+
217+
Return structured output.`,
218+
{ label: `refactor:${file.familyName}`, schema: PHASE_SCHEMA }
219+
)
220+
if (!refactor || refactor.status === 'failed') {
221+
blocked = true
222+
blockReason = refactor?.summary || 'Refactor failed'
223+
}
224+
}
225+
226+
// --- Phase: Verification + Commit ---
227+
phase('Verification')
228+
let verificationResult
229+
if (blocked) {
230+
verificationResult = {
231+
familyName: file.familyName,
232+
verdict: 'BLOCKED',
233+
migratedScenarios: [],
234+
blockedScenarios: [],
235+
failedScenarios: [],
236+
wiringState: 'unknown',
237+
commitSha: '',
238+
commitMessage: '',
239+
summary: blockReason,
240+
}
241+
} else {
242+
verificationResult = await agent(
243+
`Run the nkda-testdsl-verification skill for feature family "${file.familyName}".
244+
Feature file: ${file.featureFilePath}
245+
246+
Follow the skill exactly as written in .agents/skills/nkda-testdsl-verification/SKILL.md.
247+
248+
Required test execution order:
249+
1. Run converted/affected feature-family tests first.
250+
2. Verify every retired scenario has a mapped passing test with path:line evidence.
251+
3. If all scenarios are retired and tests are green, run the full repository test suite.
252+
253+
If verification returns PASS:
254+
- Delete the .feature file.
255+
- Delete any generated .feature.cs and legacy *Steps.cs scoped to wiring state.
256+
- Commit all changes with message: migrate: ${file.familyName} feature → DSL
257+
258+
If verification returns BLOCKED or FAIL:
259+
- Retain the .feature file (with only unconverted scenarios remaining).
260+
- Append every retained scenario as an entry in analysis/dsl-gaps-detected.md.
261+
- Commit partial progress with message: migrate(partial): ${file.familyName} <N> scenarios retired
262+
263+
Produce .output/nkda-testdsl/${file.familyName}/06-verification.md.
264+
265+
Return structured output including the verdict, lists of migrated/blocked/failed scenarios, wiring state, and commit SHA.`,
266+
{ label: `verification:${file.familyName}`, schema: VERIFICATION_SCHEMA }
267+
)
268+
}
269+
270+
results.push(verificationResult)
271+
log(`Completed family: ${file.familyName}${verificationResult?.verdict ?? 'unknown'}`)
272+
}
273+
274+
// ---------------------------------------------------------------------------
275+
// Terminal report
276+
// ---------------------------------------------------------------------------
277+
const verified = results.filter(Boolean)
278+
279+
for (const r of verified) {
280+
const terminalStatus =
281+
r.verdict === 'PASS'
282+
? (r.migratedScenarios?.length > 0 ? 'converted' : 'already-adapted')
283+
: r.verdict === 'BLOCKED'
284+
? 'blocked'
285+
: 'failed'
286+
287+
log(`
288+
**\`${r.familyName}\` — \`${terminalStatus}\`**
289+
290+
| | Count |
291+
|---|---|
292+
| ✅ Migrated & committed | ${r.migratedScenarios?.length ?? 0} |
293+
| 🚧 Blocked (gap) | ${r.blockedScenarios?.length ?? 0} |
294+
| ⚠️ Failed | ${r.failedScenarios?.length ?? 0} |
295+
| Total | ${(r.migratedScenarios?.length ?? 0) + (r.blockedScenarios?.length ?? 0) + (r.failedScenarios?.length ?? 0)} |
296+
297+
**Migrated:**
298+
${(r.migratedScenarios ?? []).map(s => `- ${s} ✅`).join('\n') || '- (none)'}
299+
300+
**Blocked:**
301+
${(r.blockedScenarios ?? []).map(s => `- ${s}`).join('\n') || '- (none)'}
302+
303+
**Failed:**
304+
${(r.failedScenarios ?? []).map(s => `- ${s}`).join('\n') || '- (none)'}
305+
306+
**Wiring state:** \`${r.wiringState}\`
307+
**Commit:** \`${r.commitSha || 'none'}\` — ${r.commitMessage || r.summary}
308+
`)
309+
}
310+
311+
return { processed: verified.length, results: verified }
312+
}
313+
}

.claude/prompts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.agents/prompts

.claude/workflows

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.agents/workflows

0 commit comments

Comments
 (0)