/** * GSD Tools Tests - UAT Audit */ 'use strict'; const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); 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'); }); // Regression: #2273 — bracketed result values [pending], [blocked], [skipped] test('detects UAT items with bracketed result values (#2273)', () => { 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 correctly', 'result: [pending]', '', '### 2. Submit Button', 'expected: Shows loading state', 'result: [blocked]', 'blocked_by: #123', '', '### 3. Error Message', 'expected: Shows validation error', 'result: [skipped]', ].join('\n')); 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, 'all 3 bracketed items should be detected'); assert.strictEqual(output.results[0].items[0].result, 'pending', '[pending] should parse as pending'); assert.strictEqual(output.results[0].items[1].result, 'blocked', '[blocked] should parse as blocked'); assert.strictEqual(output.results[0].items[2].result, 'skipped', '[skipped] should parse as skipped'); }); 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); }); // Regression: #2383 — human_needed items with result: PASS are still reported test('ignores human_verification items with result PASS (regression #2383)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '31-auth'); fs.mkdirSync(phaseDir, { recursive: true }); // This file has status: human_needed in frontmatter but all individual items // have result: "PASS" — they should not be reported as outstanding fs.writeFileSync(path.join(phaseDir, '31-VERIFICATION.md'), [ '---', 'status: human_needed', 'phase: 31-auth', 'gaps_remaining: []', '---', '', '## Human Verification', '', '| # | Item | Result | Evidence |', '|---|------|--------|----------|', '| 1 | Test SSO login with Google | PASS | Verified 2025-01-15 |', '| 2 | Test password reset flow | PASS | Verified 2025-01-15 |', '| 3 | Verify MFA enrollment | PASS | Verified 2025-01-15 |', ].join('\n')); 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, `Expected 0 outstanding items but got ${output.summary.total_items} — resolved PASS items should not be counted`); assert.strictEqual(output.summary.total_files, 0); }); test('ignores human_needed VERIFICATION file when file-level status is passed (regression #2383)', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '31-auth'); fs.mkdirSync(phaseDir, { recursive: true }); // When the frontmatter status is "passed", skip entirely regardless of section content fs.writeFileSync(path.join(phaseDir, '31-VERIFICATION.md'), [ '---', 'status: passed', 'phase: 31-auth', 'gaps_remaining: []', '---', '', '## Human Verification', '', '1. Test SSO login with Google account', '2. Test password reset flow end-to-end', ].join('\n')); 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, `status: passed file should produce 0 outstanding items, got ${output.summary.total_items}`); assert.strictEqual(output.summary.total_files, 0); }); }); describe('uat render-checkpoint', () => { let tmpDir; let uatPath; beforeEach(() => { tmpDir = createTempProject(); const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test-phase'); fs.mkdirSync(phaseDir, { recursive: true }); uatPath = path.join(phaseDir, '01-UAT.md'); }); afterEach(() => { cleanup(tmpDir); }); test('renders the current checkpoint as raw output', () => { fs.writeFileSync(uatPath, `--- status: testing phase: 01-test-phase --- ## Current Test number: 2 name: Submit form validation expected: | Empty submit keeps controls visible. Validation error copy is shown. awaiting: user response `); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir); assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`); assert.ok(result.output.includes('**Test 2: Submit form validation**')); assert.ok(result.output.includes('Empty submit keeps controls visible.')); assert.ok(result.output.includes("Type `pass` or describe what's wrong.")); }); test('strips protocol leak lines from current test copy', () => { fs.writeFileSync(uatPath, `--- status: testing phase: 01-test-phase --- ## Current Test number: 6 name: Locale copy expected: | English strings render correctly. user to=all:final code 彩票平台招商 pass Chinese strings render correctly. awaiting: user response `); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir); assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`); assert.ok(!result.output.includes('user to=all:final code')); assert.ok(!result.output.includes('彩票平台')); assert.ok(result.output.includes('English strings render correctly.')); assert.ok(result.output.includes('Chinese strings render correctly.')); }); test('does not truncate expected text containing the letter Z', () => { fs.writeFileSync(uatPath, `--- status: testing phase: 01-test-phase --- ## Current Test number: 3 name: Timezone display expected: | Timezone abbreviation shows CET. Zero-offset zones display correctly. awaiting: user response `); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir); assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`); assert.ok(result.output.includes('Timezone abbreviation shows CET.'), 'Expected text before Z-containing word should be present'); assert.ok(result.output.includes('Zero-offset zones display correctly.'), 'Expected text starting with Z should not be truncated by \\Z regex bug'); }); test('parses expected block when it is the last field in the section', () => { fs.writeFileSync(uatPath, `--- status: testing phase: 01-test-phase --- ## Current Test number: 4 name: Final field test expected: | This block has no trailing YAML key. It ends at the section boundary. `); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir); assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`); assert.ok(result.output.includes('This block has no trailing YAML key.')); assert.ok(result.output.includes('It ends at the section boundary.')); }); test('resumes paused Current Test placeholder from first pending test (#1300)', () => { fs.writeFileSync(uatPath, [ '---', 'status: partial', 'phase: 01-test-phase', 'started: 2026-06-15T00:00:00Z', 'updated: 2026-06-15T00:00:00Z', '---', '', '## Current Test', '', '[testing paused — 2 items outstanding]', '', '## Tests', '', '### 1. First test', 'expected: something observable', 'result: pass', '', '### 2. Second test', 'expected: another observable thing', 'result: [pending]', '', '## Summary', '', 'total: 2', 'passed: 1', 'issues: 0', 'pending: 1', 'skipped: 0', 'blocked: 0', '', '## Gaps', '', '[none yet]', ].join('\n')); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md'], tmpDir); assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`); const output = JSON.parse(result.output); assert.strictEqual(output.test_number, 2); assert.strictEqual(output.test_name, 'Second test'); assert.strictEqual(output.file_path, '.planning/phases/01-test-phase/01-UAT.md'); }); test('raw checkpoint mode accepts paused Current Test placeholder (#1300)', () => { fs.writeFileSync(uatPath, [ '---', 'status: partial', 'phase: 01-test-phase', '---', '', '## Current Test', '', '[testing paused — 1 item outstanding]', '', '## Tests', '', '### 1. First pending test', 'expected: raw mode checkpoint is available', 'result: [pending]', ].join('\n')); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md', '--raw'], tmpDir); assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`); assert.ok(result.output.length > 0, 'raw mode must emit a checkpoint'); }); test('non-structured Current Test with no pending tests reports actionable resume error (#1300)', () => { fs.writeFileSync(uatPath, [ '---', 'status: partial', 'phase: 01-test-phase', '---', '', '## Current Test', '', '[testing paused — 0 items outstanding]', '', '## Tests', '', '### 1. Already handled test', 'expected: completed behavior', 'result: pass', ].join('\n')); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md'], tmpDir); assert.strictEqual(result.success, false, 'Should fail when a paused placeholder has no pending test to resume'); assert.ok(result.error.includes('no pending UAT test remains')); assert.ok(!result.error.includes('Current Test section is malformed')); }); test('fails when testing is already complete', () => { fs.writeFileSync(uatPath, `--- status: complete phase: 01-test-phase --- ## Current Test [testing complete] `); const result = runGsdTools(['uat', 'render-checkpoint', '--file', '.planning/phases/01-test-phase/01-UAT.md'], tmpDir); assert.strictEqual(result.success, false, 'Should fail when no current test exists'); assert.ok(result.error.includes('already complete')); }); });