Files
rtu_v5/.planning/codebase/TESTING.md
2026-03-12 00:56:57 +08:00

7.6 KiB

Testing Patterns

Analysis Date: 2026-03-12

Test Framework

Mixed approach detected:

Primary: Node.js built-in node:test

  • Used in bin/gsd-tools.test.cjs
  • Version: Node.js built-in (no package needed)
  • Config: None detected

Secondary: Vitest

  • Used in bin/test/*.test.cjs files
  • Version: ^3.2.4 (from package.json)
  • Config: None detected (uses defaults)

Run Commands:

npm test              # Run all tests (vitest run)
npm run test:watch   # Watch mode (vitest)

Test File Organization

Location: bin/test/

Naming: {command-name}.test.cjs

Example files:

  • bin/test/get-profile.test.cjs
  • bin/test/set-profile.test.cjs
  • bin/test/allow-read-config.test.cjs
  • bin/test/oc-profile-config.test.cjs
  • bin/test/pivot-profile.test.cjs

Test Structure

From bin/test/get-profile.test.cjs:

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';

describe('get-profile.cjs', () => {
  let testDir;
  let planningDir;
  let configPath;
  let capturedLog;
  let capturedError;
  let exitCode;

  beforeEach(() => {
    // Create isolated test directory
    testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'get-profile-test-'));
    planningDir = path.join(testDir, '.planning');
    configPath = path.join(planningDir, 'oc_config.json');
    fs.mkdirSync(planningDir, { recursive: true });

    // Reset captured output
    capturedLog = null;
    capturedError = null;
    exitCode = null;

    // Mock console.log to capture all output
    console.log = (msg) => {
      allLogs.push(msg);
      capturedLog = msg;
    };
    console.error = (msg) => {
      allErrors.push(msg);
      capturedError = msg;
    };
    process.exit = (code) => {
      exitCode = code;
      throw new Error(`process.exit(${code})`);
    };
  });

  afterEach(() => {
    // Restore original functions
    console.log = originalLog;
    console.error = originalError;
    process.exit = originalExit;

    // Cleanup test directory
    try {
      fs.rmSync(testDir, { recursive: true, force: true });
    } catch (err) {
      // Ignore cleanup errors
    }
  });

  it('returns current profile when current_oc_profile is set', () => {
    fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG_WITH_CURRENT, null, 2));
    const getProfile = importGetProfile();
    
    try {
      getProfile(testDir, []);
    } catch (err) {
      // Expected to throw due to process.exit mock
    }

    expect(exitCode).toBe(0);
    const output = JSON.parse(capturedLog);
    expect(output.success).toBe(true);
  });
});

Node:test Style

From bin/gsd-tools.test.cjs:

const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');

describe('history-digest command', () => {
  let tmpDir;

  beforeEach(() => {
    tmpDir = createTempProject();
  });

  afterEach(() => {
    cleanup(tmpDir);
  });

  test('empty phases directory returns valid schema', () => {
    const result = runGsdTools('history-digest', tmpDir);
    assert.ok(result.success, `Command failed: ${result.error}`);

    const digest = JSON.parse(result.output);
    assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object');
  });
});

Manual Test Runner Style

From bin/test/allow-read-config.test.cjs:

function testCreatePermission() {
  console.log('Test: Create new opencode.json with permission...');
  
  const testDir = createTestDir();
  
  try {
    const result = runCLI(testDir, []);
    
    if (!result.success) {
      throw new Error(`Expected success, got: ${JSON.stringify(result)}`);
    }
    
    // Verify opencode.json was created
    const opencodePath = path.join(testDir, 'opencode.json');
    if (!fs.existsSync(opencodePath)) {
      throw new Error('opencode.json was not created');
    }
    
    console.log('✓ PASS: Create permission\n');
    return true;
  } catch (err) {
    console.error('✗ FAIL:', err.message, '\n');
    return false;
  } finally {
    cleanupTestDir(testDir);
  }
}

function runTests() {
  console.log('Running allow-read-config tests...\n');
  
  const results = [
    testCreatePermission(),
    testIdempotency(),
    testDryRun(),
  ];
  
  const passed = results.filter(r => r).length;
  console.log(`Results: ${passed}/${total} tests passed`);
  
  if (passed === total) {
    process.exit(0);
  } else {
    process.exit(1);
  }
}

runTests();

Mocking Patterns

Console/Process Mocking

Vitest approach:

const originalLog = console.log;
const originalError = console.error;
const originalExit = process.exit;

beforeEach(() => {
  console.log = (msg) => {
    allLogs.push(msg);
    capturedLog = msg;
  };
  console.error = (msg) => {
    allErrors.push(msg);
    capturedError = msg;
  };
  process.exit = (code) => {
    exitCode = code;
    throw new Error(`process.exit(${code})`);
  };
});

afterEach(() => {
  console.log = originalLog;
  console.error = originalError;
  process.exit = originalExit;
});

Require Cache Clearing

const importGetProfile = () => {
  const modulePath = '../gsd-oc-commands/get-profile.cjs';
  delete require.cache[require.resolve(modulePath)];
  return require(modulePath);
};

Test Fixtures

Location: bin/test/fixtures/

Pattern: In-file constants

const VALID_CONFIG_WITH_CURRENT = {
  current_oc_profile: 'smart',
  profiles: {
    presets: {
      simple: {
        planning: 'bailian-coding-plan/qwen3.5-plus',
        execution: 'bailian-coding-plan/qwen3.5-plus',
        verification: 'bailian-coding-plan/qwen3.5-plus'
      },
      smart: { /* ... */ }
    }
  }
};

Test Helpers

CLI Execution

function runCLI(testDir, args) {
  const cmd = `node ${TOOLS_PATH} allow-read-config ${args.join(' ')}`;
  const output = execSync(cmd, { cwd: testDir, encoding: 'utf8' });
  return JSON.parse(output);
}

Temp Directory Creation

function createTestDir() {
  const testDir = path.join(os.tmpdir(), `gsd-oc-test-${Date.now()}`);
  fs.mkdirSync(testDir, { recursive: true });
  return testDir;
}

function cleanupTestDir(testDir) {
  if (fs.existsSync(testDir)) {
    fs.rmSync(testDir, { recursive: true, force: true });
  }
}

Assertion Patterns

Vitest

expect(exitCode).toBe(0);
expect(output.success).toBe(true);
expect(output.data).toHaveProperty('smart');
expect(output.data.smart).toEqual(VALID_CONFIG_WITH_CURRENT.profiles.presets.smart);

Node.js assert

assert.ok(result.success, `Command failed: ${result.error}`);
assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object');

Coverage

No coverage configuration detected. No enforcement of coverage thresholds.

Test Types

Unit Tests

  • Individual function testing
  • Use mocking for dependencies
  • Test isolated behavior

Integration/CLI Tests

  • Execute CLI commands via execSync
  • Test with real temporary directories
  • Verify file creation/state changes

Patterns

  • Test both success and error paths
  • Test flag combinations (--verbose, --dry-run)
  • Test idempotency (running same command twice)

Common Issues

Inconsistent Test Frameworks

  • Some tests use Vitest, others use Node.js built-in node:test
  • Some tests use manual test runner (no framework)
  • Recommendation: Standardize on one framework

Missing Test Config

  • No vitest.config.js found
  • No jest.config.js found
  • Recommendation: Add configuration file

Testing analysis: 2026-03-12