mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-28 20:01:17 +08:00
feat: add workflow management commands and utilities
- Implemented workflow installation, listing, and syncing commands in `workflow.ts`. - Created utility functions for project root detection and package version retrieval in `project-root.ts`. - Added update checker functionality to notify users of new package versions in `update-checker.ts`. - Developed unit tests for project root utilities and update checker to ensure functionality and version comparison accuracy.
This commit is contained in:
159
ccw/tests/project-root.test.ts
Normal file
159
ccw/tests/project-root.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Unit tests for project-root utility module.
|
||||
*
|
||||
* Tests path resolution logic for finding project root directory.
|
||||
* Note: These tests work with the actual filesystem rather than mocks
|
||||
* because ESM module caching makes fs mocking unreliable.
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Import the actual module
|
||||
import { findProjectRoot, loadPackageInfo, getPackageVersion, getPackageRoot } from '../dist/utils/project-root.js';
|
||||
|
||||
describe('project-root: findProjectRoot', () => {
|
||||
it('should find project root from tests directory', () => {
|
||||
const result = findProjectRoot(__dirname);
|
||||
|
||||
// Should find the actual project root
|
||||
assert.ok(result, 'Should find a project root');
|
||||
|
||||
// Verify it has the expected package.json
|
||||
const pkgPath = join(result, 'package.json');
|
||||
assert.ok(existsSync(pkgPath), 'Should have package.json at root');
|
||||
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
||||
assert.ok(
|
||||
pkg.name === 'claude-code-workflow' || pkg.bin?.ccw,
|
||||
'Should find correct project by name or bin'
|
||||
);
|
||||
});
|
||||
|
||||
it('should find project root from deeply nested directory', () => {
|
||||
const deepDir = join(__dirname, 'integration', 'cli-executor');
|
||||
const result = findProjectRoot(deepDir);
|
||||
|
||||
assert.ok(result, 'Should find project root from deep directory');
|
||||
});
|
||||
|
||||
it('should find project root from src directory', () => {
|
||||
const srcDir = join(__dirname, '..', 'src', 'utils');
|
||||
const result = findProjectRoot(srcDir);
|
||||
|
||||
assert.ok(result, 'Should find project root from src');
|
||||
});
|
||||
|
||||
it('should return consistent result regardless of starting point', () => {
|
||||
const fromTests = findProjectRoot(__dirname);
|
||||
const fromSrc = findProjectRoot(join(__dirname, '..', 'src'));
|
||||
const fromCommands = findProjectRoot(join(__dirname, '..', 'src', 'commands'));
|
||||
|
||||
assert.equal(fromTests, fromSrc, 'Should find same root from tests and src');
|
||||
assert.equal(fromSrc, fromCommands, 'Should find same root from src and commands');
|
||||
});
|
||||
});
|
||||
|
||||
describe('project-root: loadPackageInfo', () => {
|
||||
it('should load package info successfully', () => {
|
||||
const pkg = loadPackageInfo();
|
||||
|
||||
assert.ok(pkg, 'Should return package info');
|
||||
assert.ok(pkg.name, 'Should have name field');
|
||||
assert.ok(pkg.version, 'Should have version field');
|
||||
});
|
||||
|
||||
it('should return correct package name', () => {
|
||||
const pkg = loadPackageInfo();
|
||||
|
||||
assert.ok(
|
||||
pkg?.name === 'claude-code-workflow' || pkg?.bin?.ccw,
|
||||
'Should return the correct project package'
|
||||
);
|
||||
});
|
||||
|
||||
it('should include version field', () => {
|
||||
const pkg = loadPackageInfo();
|
||||
|
||||
assert.ok(pkg?.version, 'Should have version');
|
||||
assert.match(pkg!.version, /^\d+\.\d+\.\d+/, 'Version should be semver format');
|
||||
});
|
||||
});
|
||||
|
||||
describe('project-root: getPackageVersion', () => {
|
||||
it('should return version string', () => {
|
||||
const version = getPackageVersion();
|
||||
|
||||
assert.ok(version, 'Should return a version');
|
||||
assert.equal(typeof version, 'string', 'Version should be a string');
|
||||
});
|
||||
|
||||
it('should return valid semver format', () => {
|
||||
const version = getPackageVersion();
|
||||
|
||||
// Basic semver pattern: X.Y.Z or X.Y.Z-prerelease
|
||||
assert.match(version, /^\d+\.\d+\.\d+/, 'Should be semver format');
|
||||
});
|
||||
|
||||
it('should return consistent version', () => {
|
||||
const v1 = getPackageVersion();
|
||||
const v2 = getPackageVersion();
|
||||
|
||||
assert.equal(v1, v2, 'Should return same version on multiple calls');
|
||||
});
|
||||
});
|
||||
|
||||
describe('project-root: getPackageRoot', () => {
|
||||
it('should return project root path', () => {
|
||||
const root = getPackageRoot();
|
||||
|
||||
assert.ok(root, 'Should return a path');
|
||||
assert.equal(typeof root, 'string', 'Should be a string');
|
||||
});
|
||||
|
||||
it('should return existing directory', () => {
|
||||
const root = getPackageRoot();
|
||||
|
||||
assert.ok(existsSync(root), 'Root directory should exist');
|
||||
});
|
||||
|
||||
it('should contain package.json', () => {
|
||||
const root = getPackageRoot();
|
||||
const pkgPath = join(root, 'package.json');
|
||||
|
||||
assert.ok(existsSync(pkgPath), 'Should have package.json');
|
||||
});
|
||||
|
||||
it('should return absolute path', () => {
|
||||
const root = getPackageRoot();
|
||||
|
||||
assert.equal(root, resolve(root), 'Should be absolute path');
|
||||
});
|
||||
});
|
||||
|
||||
describe('project-root: integration', () => {
|
||||
it('should have consistent data across functions', () => {
|
||||
const root = getPackageRoot();
|
||||
const pkg = loadPackageInfo();
|
||||
const version = getPackageVersion();
|
||||
|
||||
// Read package.json directly for comparison
|
||||
const directPkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
||||
|
||||
assert.equal(pkg?.version, directPkg.version, 'loadPackageInfo should match direct read');
|
||||
assert.equal(version, directPkg.version, 'getPackageVersion should match direct read');
|
||||
});
|
||||
|
||||
it('should find root matching package.json location', () => {
|
||||
const root = getPackageRoot();
|
||||
const foundRoot = findProjectRoot(__dirname);
|
||||
|
||||
assert.equal(root, foundRoot, 'getPackageRoot and findProjectRoot should match');
|
||||
});
|
||||
});
|
||||
225
ccw/tests/update-checker.test.ts
Normal file
225
ccw/tests/update-checker.test.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Unit tests for update-checker utility module.
|
||||
*
|
||||
* Tests version comparison logic with semver support including prerelease versions.
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
// We need to test the compareVersions function which is not exported
|
||||
// So we'll create a standalone copy for testing purposes
|
||||
function parseVersion(version: string): { major: number; minor: number; patch: number; prerelease: string[] } {
|
||||
const cleaned = version.replace(/^v/, '');
|
||||
const [mainPart, prereleasePart] = cleaned.split('-');
|
||||
const parts = mainPart.split('.');
|
||||
const major = parseInt(parts[0], 10) || 0;
|
||||
const minor = parseInt(parts[1], 10) || 0;
|
||||
const patch = parseInt(parts[2], 10) || 0;
|
||||
const prerelease = prereleasePart ? prereleasePart.split('.') : [];
|
||||
|
||||
return { major, minor, patch, prerelease };
|
||||
}
|
||||
|
||||
function compareVersions(a: string, b: string): number {
|
||||
const vA = parseVersion(a);
|
||||
const vB = parseVersion(b);
|
||||
|
||||
// Compare major.minor.patch
|
||||
if (vA.major !== vB.major) return vA.major > vB.major ? 1 : -1;
|
||||
if (vA.minor !== vB.minor) return vA.minor > vB.minor ? 1 : -1;
|
||||
if (vA.patch !== vB.patch) return vA.patch > vB.patch ? 1 : -1;
|
||||
|
||||
// Handle prerelease: no prerelease > has prerelease
|
||||
// e.g., 1.0.0 > 1.0.0-alpha
|
||||
if (vA.prerelease.length === 0 && vB.prerelease.length > 0) return 1;
|
||||
if (vA.prerelease.length > 0 && vB.prerelease.length === 0) return -1;
|
||||
|
||||
// Compare prerelease identifiers
|
||||
const maxLen = Math.max(vA.prerelease.length, vB.prerelease.length);
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const partA = vA.prerelease[i];
|
||||
const partB = vB.prerelease[i];
|
||||
|
||||
// Missing part is less (1.0.0-alpha < 1.0.0-alpha.1)
|
||||
if (partA === undefined) return -1;
|
||||
if (partB === undefined) return 1;
|
||||
|
||||
// Numeric comparison if both are numbers
|
||||
const numA = parseInt(partA, 10);
|
||||
const numB = parseInt(partB, 10);
|
||||
if (!isNaN(numA) && !isNaN(numB)) {
|
||||
if (numA !== numB) return numA > numB ? 1 : -1;
|
||||
} else {
|
||||
// String comparison
|
||||
if (partA !== partB) return partA > partB ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
describe('update-checker: parseVersion', () => {
|
||||
it('should parse basic version', () => {
|
||||
const result = parseVersion('1.2.3');
|
||||
assert.equal(result.major, 1);
|
||||
assert.equal(result.minor, 2);
|
||||
assert.equal(result.patch, 3);
|
||||
assert.deepEqual(result.prerelease, []);
|
||||
});
|
||||
|
||||
it('should parse version with v prefix', () => {
|
||||
const result = parseVersion('v1.2.3');
|
||||
assert.equal(result.major, 1);
|
||||
assert.equal(result.minor, 2);
|
||||
assert.equal(result.patch, 3);
|
||||
});
|
||||
|
||||
it('should parse version with alpha prerelease', () => {
|
||||
const result = parseVersion('1.0.0-alpha');
|
||||
assert.equal(result.major, 1);
|
||||
assert.equal(result.minor, 0);
|
||||
assert.equal(result.patch, 0);
|
||||
assert.deepEqual(result.prerelease, ['alpha']);
|
||||
});
|
||||
|
||||
it('should parse version with numeric prerelease', () => {
|
||||
const result = parseVersion('1.0.0-alpha.1');
|
||||
assert.deepEqual(result.prerelease, ['alpha', '1']);
|
||||
});
|
||||
|
||||
it('should parse version with rc prerelease', () => {
|
||||
const result = parseVersion('2.5.0-rc.3');
|
||||
assert.equal(result.major, 2);
|
||||
assert.equal(result.minor, 5);
|
||||
assert.equal(result.patch, 0);
|
||||
assert.deepEqual(result.prerelease, ['rc', '3']);
|
||||
});
|
||||
|
||||
it('should handle missing patch version', () => {
|
||||
const result = parseVersion('1.2');
|
||||
assert.equal(result.major, 1);
|
||||
assert.equal(result.minor, 2);
|
||||
assert.equal(result.patch, 0);
|
||||
});
|
||||
|
||||
it('should handle missing minor and patch', () => {
|
||||
const result = parseVersion('3');
|
||||
assert.equal(result.major, 3);
|
||||
assert.equal(result.minor, 0);
|
||||
assert.equal(result.patch, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update-checker: compareVersions', () => {
|
||||
describe('major version comparison', () => {
|
||||
it('should return 1 when first major is greater', () => {
|
||||
assert.equal(compareVersions('2.0.0', '1.0.0'), 1);
|
||||
assert.equal(compareVersions('3.5.2', '2.8.9'), 1);
|
||||
});
|
||||
|
||||
it('should return -1 when first major is less', () => {
|
||||
assert.equal(compareVersions('1.0.0', '2.0.0'), -1);
|
||||
assert.equal(compareVersions('1.9.9', '2.0.0'), -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('minor version comparison', () => {
|
||||
it('should return 1 when major equal and first minor is greater', () => {
|
||||
assert.equal(compareVersions('1.2.0', '1.1.0'), 1);
|
||||
assert.equal(compareVersions('3.5.0', '3.4.9'), 1);
|
||||
});
|
||||
|
||||
it('should return -1 when major equal and first minor is less', () => {
|
||||
assert.equal(compareVersions('1.1.0', '1.2.0'), -1);
|
||||
assert.equal(compareVersions('2.3.5', '2.4.0'), -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch version comparison', () => {
|
||||
it('should return 1 when major.minor equal and first patch is greater', () => {
|
||||
assert.equal(compareVersions('1.2.5', '1.2.3'), 1);
|
||||
assert.equal(compareVersions('2.0.1', '2.0.0'), 1);
|
||||
});
|
||||
|
||||
it('should return -1 when major.minor equal and first patch is less', () => {
|
||||
assert.equal(compareVersions('1.2.3', '1.2.5'), -1);
|
||||
assert.equal(compareVersions('3.1.0', '3.1.2'), -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equal versions', () => {
|
||||
it('should return 0 for identical versions', () => {
|
||||
assert.equal(compareVersions('1.2.3', '1.2.3'), 0);
|
||||
assert.equal(compareVersions('v1.2.3', '1.2.3'), 0);
|
||||
});
|
||||
|
||||
it('should return 0 for equal versions with missing parts', () => {
|
||||
assert.equal(compareVersions('1.2', '1.2.0'), 0);
|
||||
assert.equal(compareVersions('2', '2.0.0'), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prerelease version comparison', () => {
|
||||
it('should treat stable version as greater than prerelease', () => {
|
||||
assert.equal(compareVersions('1.0.0', '1.0.0-alpha'), 1);
|
||||
assert.equal(compareVersions('1.0.0', '1.0.0-beta'), 1);
|
||||
assert.equal(compareVersions('1.0.0', '1.0.0-rc.1'), 1);
|
||||
});
|
||||
|
||||
it('should treat prerelease version as less than stable', () => {
|
||||
assert.equal(compareVersions('1.0.0-alpha', '1.0.0'), -1);
|
||||
assert.equal(compareVersions('1.0.0-beta', '1.0.0'), -1);
|
||||
assert.equal(compareVersions('2.5.0-rc.2', '2.5.0'), -1);
|
||||
});
|
||||
|
||||
it('should compare prerelease identifiers alphabetically', () => {
|
||||
assert.equal(compareVersions('1.0.0-beta', '1.0.0-alpha'), 1);
|
||||
assert.equal(compareVersions('1.0.0-alpha', '1.0.0-beta'), -1);
|
||||
assert.equal(compareVersions('1.0.0-rc', '1.0.0-beta'), 1);
|
||||
});
|
||||
|
||||
it('should compare numeric prerelease identifiers numerically', () => {
|
||||
assert.equal(compareVersions('1.0.0-alpha.2', '1.0.0-alpha.1'), 1);
|
||||
assert.equal(compareVersions('1.0.0-alpha.1', '1.0.0-alpha.2'), -1);
|
||||
assert.equal(compareVersions('1.0.0-beta.10', '1.0.0-beta.2'), 1);
|
||||
});
|
||||
|
||||
it('should handle missing prerelease parts', () => {
|
||||
assert.equal(compareVersions('1.0.0-alpha.1', '1.0.0-alpha'), 1);
|
||||
assert.equal(compareVersions('1.0.0-alpha', '1.0.0-alpha.1'), -1);
|
||||
});
|
||||
|
||||
it('should handle complex prerelease comparisons', () => {
|
||||
assert.equal(compareVersions('1.0.0-alpha.1', '1.0.0-alpha.1'), 0);
|
||||
assert.equal(compareVersions('1.0.0-rc.1', '1.0.0-beta.10'), 1);
|
||||
assert.equal(compareVersions('2.0.0-beta.1', '2.0.0-alpha.9'), 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world version scenarios', () => {
|
||||
it('should correctly order common npm package versions', () => {
|
||||
const versions = [
|
||||
'1.0.0-alpha',
|
||||
'1.0.0-alpha.1',
|
||||
'1.0.0-beta',
|
||||
'1.0.0-beta.2',
|
||||
'1.0.0-rc.1',
|
||||
'1.0.0',
|
||||
'1.0.1',
|
||||
'1.1.0',
|
||||
'2.0.0'
|
||||
];
|
||||
|
||||
for (let i = 0; i < versions.length - 1; i++) {
|
||||
const result = compareVersions(versions[i + 1], versions[i]);
|
||||
assert.equal(result, 1, `Expected ${versions[i + 1]} > ${versions[i]}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle version with v prefix correctly', () => {
|
||||
assert.equal(compareVersions('v2.0.0', '1.9.9'), 1);
|
||||
assert.equal(compareVersions('v1.0.0-beta', 'v1.0.0'), -1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user