Prevent silent loss of UAT/verification items when projects advance. Surfaces outstanding items across all prior phases so nothing is forgotten. New command: - /gsd:audit-uat — cross-phase audit with categorized report and test plan New capabilities: - Cross-phase health check in /gsd:progress (Step 1.6) - status: partial for incomplete UAT sessions - result: blocked with blocked_by tag for dependency-gated tests - human_needed items persisted as trackable HUMAN-UAT.md files - Phase completion and transition warnings for verification debt Files: 4 new, 14 modified (9 feature + 5 docs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
327 lines
9.3 KiB
JavaScript
327 lines
9.3 KiB
JavaScript
/**
|
|
* GSD Tools Tests - UAT Audit
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
|
|
|
describe('audit-uat command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('returns empty results when no UAT files exist', () => {
|
|
// Create a phase directory with no UAT files
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '01-foundation', '.gitkeep'), '');
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.deepStrictEqual(output.results, []);
|
|
assert.strictEqual(output.summary.total_items, 0);
|
|
assert.strictEqual(output.summary.total_files, 0);
|
|
});
|
|
|
|
test('detects UAT with pending items', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(path.join(phaseDir, '01-UAT.md'), `---
|
|
status: testing
|
|
phase: 01-foundation
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. Login Form
|
|
expected: Form displays with email and password fields
|
|
result: pass
|
|
|
|
### 2. Submit Button
|
|
expected: Submitting shows loading state
|
|
result: pending
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.summary.total_items, 1);
|
|
assert.strictEqual(output.results[0].phase, '01');
|
|
assert.strictEqual(output.results[0].items[0].result, 'pending');
|
|
assert.strictEqual(output.results[0].items[0].category, 'pending');
|
|
assert.strictEqual(output.results[0].items[0].name, 'Submit Button');
|
|
});
|
|
|
|
test('detects UAT with blocked items and categorizes blocked_by', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '02-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(path.join(phaseDir, '02-UAT.md'), `---
|
|
status: partial
|
|
phase: 02-api
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. API Health Check
|
|
expected: Returns 200 OK
|
|
result: blocked
|
|
blocked_by: server
|
|
reason: Server not running locally
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.summary.total_items, 1);
|
|
assert.strictEqual(output.results[0].items[0].result, 'blocked');
|
|
assert.strictEqual(output.results[0].items[0].category, 'server_blocked');
|
|
assert.strictEqual(output.results[0].items[0].blocked_by, 'server');
|
|
});
|
|
|
|
test('detects false completion (complete status with pending items)', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-ui');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), `---
|
|
status: complete
|
|
phase: 03-ui
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. Dashboard Layout
|
|
expected: Cards render in grid
|
|
result: pass
|
|
|
|
### 2. Mobile Responsive
|
|
expected: Grid collapses to single column on mobile
|
|
result: pending
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.summary.total_items, 1);
|
|
assert.strictEqual(output.results[0].status, 'complete');
|
|
assert.strictEqual(output.results[0].items[0].result, 'pending');
|
|
});
|
|
|
|
test('extracts human_needed items from VERIFICATION files', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '04-auth');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(path.join(phaseDir, '04-VERIFICATION.md'), `---
|
|
status: human_needed
|
|
phase: 04-auth
|
|
---
|
|
|
|
## Automated Checks
|
|
|
|
All passed.
|
|
|
|
## Human Verification
|
|
|
|
1. Test SSO login with Google account
|
|
2. Test password reset flow end-to-end
|
|
3. Verify MFA enrollment on new device
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.summary.total_items, 3);
|
|
assert.strictEqual(output.results[0].type, 'verification');
|
|
assert.strictEqual(output.results[0].status, 'human_needed');
|
|
assert.strictEqual(output.results[0].items[0].category, 'human_uat');
|
|
assert.strictEqual(output.results[0].items[0].name, 'Test SSO login with Google account');
|
|
});
|
|
|
|
test('scans and aggregates across multiple phases', () => {
|
|
// Phase 1 with pending
|
|
const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phase1, { recursive: true });
|
|
fs.writeFileSync(path.join(phase1, '01-UAT.md'), `---
|
|
status: partial
|
|
phase: 01-foundation
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. Test A
|
|
expected: Works
|
|
result: pending
|
|
`);
|
|
|
|
// Phase 2 with blocked
|
|
const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api');
|
|
fs.mkdirSync(phase2, { recursive: true });
|
|
fs.writeFileSync(path.join(phase2, '02-UAT.md'), `---
|
|
status: partial
|
|
phase: 02-api
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. Test B
|
|
expected: Responds
|
|
result: blocked
|
|
blocked_by: server
|
|
|
|
### 2. Test C
|
|
expected: Returns data
|
|
result: skipped
|
|
reason: device not available
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.summary.total_files, 2);
|
|
assert.strictEqual(output.summary.total_items, 3);
|
|
assert.strictEqual(output.summary.by_phase['01'], 1);
|
|
assert.strictEqual(output.summary.by_phase['02'], 2);
|
|
});
|
|
|
|
test('milestone scoping filters phases to current milestone', () => {
|
|
// Create a ROADMAP.md that only references Phase 2
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), `# Roadmap
|
|
|
|
### Phase 2: API Layer
|
|
**Goal:** Build API
|
|
`);
|
|
|
|
// Phase 1 (not in current milestone) with pending
|
|
const phase1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phase1, { recursive: true });
|
|
fs.writeFileSync(path.join(phase1, '01-UAT.md'), `---
|
|
status: partial
|
|
phase: 01-foundation
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. Old Test
|
|
expected: Old behavior
|
|
result: pending
|
|
`);
|
|
|
|
// Phase 2 (in current milestone) with pending
|
|
const phase2 = path.join(tmpDir, '.planning', 'phases', '02-api');
|
|
fs.mkdirSync(phase2, { recursive: true });
|
|
fs.writeFileSync(path.join(phase2, '02-UAT.md'), `---
|
|
status: partial
|
|
phase: 02-api
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. New Test
|
|
expected: New behavior
|
|
result: pending
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
// Only Phase 2 should be included (Phase 1 not in ROADMAP)
|
|
assert.strictEqual(output.summary.total_files, 1);
|
|
assert.strictEqual(output.results[0].phase, '02');
|
|
});
|
|
|
|
test('summary by_category counts are correct', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '05-billing');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(path.join(phaseDir, '05-UAT.md'), `---
|
|
status: partial
|
|
phase: 05-billing
|
|
started: 2025-01-01T00:00:00Z
|
|
updated: 2025-01-01T00:00:00Z
|
|
---
|
|
|
|
## Tests
|
|
|
|
### 1. Payment Form
|
|
expected: Stripe elements load
|
|
result: pending
|
|
|
|
### 2. Webhook Handler
|
|
expected: Processes payment events
|
|
result: blocked
|
|
blocked_by: third-party Stripe
|
|
|
|
### 3. Invoice PDF
|
|
expected: Generates downloadable PDF
|
|
result: skipped
|
|
reason: needs release build
|
|
|
|
### 4. Refund Flow
|
|
expected: Processes refund
|
|
result: pending
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.summary.total_items, 4);
|
|
assert.strictEqual(output.summary.by_category.pending, 2);
|
|
assert.strictEqual(output.summary.by_category.third_party, 1);
|
|
assert.strictEqual(output.summary.by_category.build_needed, 1);
|
|
});
|
|
|
|
test('ignores VERIFICATION files without human_needed or gaps_found status', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(path.join(phaseDir, '01-VERIFICATION.md'), `---
|
|
status: passed
|
|
phase: 01-foundation
|
|
---
|
|
|
|
## Results
|
|
|
|
All checks passed.
|
|
`);
|
|
|
|
const result = runGsdTools('audit-uat --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.summary.total_items, 0);
|
|
assert.strictEqual(output.summary.total_files, 0);
|
|
});
|
|
});
|