bmad-method-test-architecture-enterprise 3.1.0 → 3.3.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 (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/cli/automate.js +7 -0
  3. package/cli/evaluate.js +20 -0
  4. package/cli/examples/gitlab-ci.yaml +1 -1
  5. package/cli/examples/pr-test-review.yml +3 -3
  6. package/cli/lib/agent-adapters.js +46 -0
  7. package/cli/lib/automate-inventory.js +258 -25
  8. package/cli/lib/automate-prompt.js +34 -2
  9. package/cli/lib/automate-result.js +74 -2
  10. package/cli/lib/evaluate/configuration-experiment.js +3401 -0
  11. package/cli/lib/evaluate/experiments.js +1299 -0
  12. package/cli/lib/evaluate/retain-skill-artifacts.js +78 -0
  13. package/cli/lib/evaluate/test-design-corpus/clean/docs/epics/epic-9-last-sync-indicator.md +47 -0
  14. package/cli/lib/evaluate/test-design-corpus/ground-truth.json +222 -0
  15. package/cli/lib/evaluate/test-design-corpus/native-identity.json +4 -0
  16. package/cli/lib/evaluate/test-design-corpus/seeded/docs/epics/epic-7-offline-order-capture.md +75 -0
  17. package/cli/lib/evaluate/test-design-replay.js +452 -0
  18. package/cli/lib/run-agent.js +18 -2
  19. package/cli/lib/test-design-staged-config.js +60 -0
  20. package/cli/lib/test-design-staging.js +229 -0
  21. package/cli/lib/unit-runners.js +219 -0
  22. package/cli/lib/vitest-attempts-reporter.mjs +30 -0
  23. package/cli/test-design-runner.js +245 -11
  24. package/cli/unit-check.js +595 -0
  25. package/package.json +13 -3
  26. package/skills/bmad-tea-automate/red/checklist.md +1 -1
  27. package/skills/bmad-tea-automate/resources/run-and-heal.md +3 -1
  28. package/skills/bmad-tea-evaluate/SKILL.md +11 -0
  29. package/skills/bmad-tea-evaluate/references/configuration-experiments.md +37 -0
  30. package/skills/bmad-tea-evaluate/references/evaluator.md +2 -2
  31. package/skills/bmad-tea-evaluate/references/gaps.md +1 -1
  32. package/skills/bmad-tea-test-review/steps-c/step-03f-aggregate-scores.md +3 -3
  33. package/skills/bmod-tea/bmod.toml +1 -1
  34. package/skills/bmod-tea/help/help.md +1 -1
  35. package/skills/bmod-tea/knowledge/auth-session.md +25 -20
  36. package/skills/bmod-tea/knowledge/test-healing-patterns.md +2 -0
@@ -7,6 +7,7 @@ const { isDeepStrictEqual, stripVTControlCharacters } = require('node:util');
7
7
  const yaml = require('js-yaml');
8
8
  const { projectPath } = require('./workflow-cli');
9
9
  const { generatedInventory, reconcileInventory } = require('./automate-inventory');
10
+ const { UNIT_CHECK_KIND, normalizeUnitReport, unitReportRunner } = require('./unit-runners');
10
11
 
11
12
  const STATUSES = ['passed', 'verified red', 'failed', 'could not measure', 'disabled'];
12
13
  const RESULT_KEYS = [
@@ -64,6 +65,28 @@ function reportCounts(report) {
64
65
  } else if (attempt.status === 'skipped') result.skipped++;
65
66
  else throw new Error(`execution report has unsupported attempt status: ${attempt.status}`);
66
67
  };
68
+ // Unit attempts carry a classified failure: assertion, timeout or other error.
69
+ const addUnit = (attempt) => {
70
+ switch (attempt.status) {
71
+ case 'passed': {
72
+ result.passed++;
73
+ break;
74
+ }
75
+ case 'skipped': {
76
+ result.skipped++;
77
+ break;
78
+ }
79
+ case 'failed': {
80
+ result.failed++;
81
+ if (attempt.failure === 'timeout') result.errors++;
82
+ else if (attempt.failure === 'assertion') result.assertionFailures++;
83
+ break;
84
+ }
85
+ default: {
86
+ throw new Error(`execution report has unsupported attempt status: ${attempt.status}`);
87
+ }
88
+ }
89
+ };
67
90
  if (Array.isArray(report.suites)) {
68
91
  result.errors = (report.errors ?? []).length;
69
92
  const walk = (suite) => {
@@ -71,6 +94,49 @@ function reportCounts(report) {
71
94
  for (const child of suite.suites ?? []) walk(child);
72
95
  };
73
96
  for (const suite of report.suites) walk(suite);
97
+ } else if (unitReportRunner(report)) {
98
+ const normalized = normalizeUnitReport(report);
99
+ result.errors = normalized.errors;
100
+ for (const file of normalized.files) for (const test of file.tests) for (const attempt of test.attempts) addUnit(attempt);
101
+ } else if (report.kind === UNIT_CHECK_KIND) {
102
+ const red = report.mode === 'red';
103
+ if (
104
+ report.schemaVersion !== 1 ||
105
+ !['expand', 'red'].includes(report.mode) ||
106
+ !Array.isArray(report.files) ||
107
+ (red && !Array.isArray(report.productionFilesTouched))
108
+ )
109
+ throw new Error('tea-unit-check report is malformed');
110
+ result.errors = red ? report.productionFilesTouched.length : 0;
111
+ const text = (value) => value === null || (typeof value === 'string' && value.length > 0);
112
+ for (const file of report.files) {
113
+ if (!file || !text(file.loadError) || !text(file.runError) || !(file.exitCode === null || Number.isSafeInteger(file.exitCode)))
114
+ throw new Error('tea-unit-check file result lacks its outcome fields');
115
+ if (file.exitCode === null && !file.loadError && !file.runError)
116
+ throw new Error(`tea-unit-check records no exit code and no error: ${file.file}`);
117
+ if (file.loadError) result.errors++;
118
+ if (file.runError) result.errors++;
119
+ if (!Array.isArray(file.tests)) throw new Error('tea-unit-check file result lacks its tests');
120
+ let failedFinal = false;
121
+ for (const test of file.tests) {
122
+ // Every attempt counts, so a retry cannot hide an earlier wrong-reason failure.
123
+ if (!Array.isArray(test?.attempts) || test.attempts.length === 0) throw new Error('tea-unit-check test lacks its attempts');
124
+ const last = test.attempts.at(-1);
125
+ if (last.status !== test.status || last.failure !== test.failure)
126
+ throw new Error('tea-unit-check test outcome disagrees with its final attempt');
127
+ if (test.status === 'failed') failedFinal = true;
128
+ for (const attempt of test.attempts) {
129
+ if (attempt?.status === 'failed' && !['assertion', 'timeout', 'error'].includes(attempt.failure))
130
+ throw new Error('tea-unit-check failure lacks a classified reason');
131
+ if (attempt?.status !== 'failed' && attempt?.failure !== null)
132
+ throw new Error('tea-unit-check attempt has an unexpected failure reason');
133
+ addUnit(attempt);
134
+ }
135
+ }
136
+ // Vitest and Jest exit nonzero exactly when a test fails, unless a load or run error explains it.
137
+ if (!file.loadError && !file.runError && (file.exitCode !== 0) !== failedFinal)
138
+ throw new Error(`tea-unit-check exit code ${file.exitCode} disagrees with its test results: ${file.file}`);
139
+ }
74
140
  } else if (report.schemaVersion === 1 && Array.isArray(report.files) && Array.isArray(report.productionFilesTouched)) {
75
141
  result.errors = report.productionFilesTouched.length;
76
142
  for (const file of report.files) {
@@ -104,6 +170,7 @@ function parseResult({
104
170
  selectedCheckpoint,
105
171
  checkpointBefore,
106
172
  evidenceDir,
173
+ runners,
107
174
  }) {
108
175
  let result;
109
176
  try {
@@ -181,10 +248,15 @@ function parseResult({
181
248
  const allRecognized = recognizedReports > 0 && recognizedReports === result.executionReports.length;
182
249
  if (successful && !allRecognized) throw new Error('successful execution requires supported native reports for every final scope');
183
250
  if (successful) {
184
- const inventory = generatedInventory(projectRoot, result.generatedFiles);
251
+ // The report family selects the registration grammar; reconciliation then
252
+ // requires that family to match a runner the project declared before the run.
253
+ const unitRunner = nativeReports
254
+ .map((report) => (report.kind === UNIT_CHECK_KIND ? report.runner : unitReportRunner(report)))
255
+ .find(Boolean);
256
+ const inventory = generatedInventory(projectRoot, result.generatedFiles, { runner: unitRunner ?? 'playwright' });
185
257
  if (evidenceDir)
186
258
  fs.writeFileSync(path.join(evidenceDir, 'generated-test-inventory.json'), `${JSON.stringify(inventory, null, 2)}\n`, { flag: 'wx' });
187
- const verified = reconcileInventory(inventory, nativeReports, { mode });
259
+ const verified = reconcileInventory(inventory, nativeReports, { mode, runners });
188
260
  if (evidenceDir)
189
261
  fs.writeFileSync(path.join(evidenceDir, 'validated-test-scopes.json'), `${JSON.stringify(verified, null, 2)}\n`, { flag: 'wx' });
190
262
  }