diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 76024cc..cb88e1a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,6 +28,7 @@ jobs: node-version: '24' - run: pnpm install --no-frozen-lockfile + - run: pnpm test - run: pnpm build - name: Zip dist directory diff --git a/package.json b/package.json index c9e68e4..794d254 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shsf-cli", - "version": "2.1.5", + "version": "2.1.6", "description": "", "type": "module", "files": [ @@ -12,7 +12,9 @@ "scripts": { "build": "rimraf dist && tsc", "start": "node dist/index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "keywords": [], "author": "", @@ -21,9 +23,11 @@ "devDependencies": { "@types/inquirer": "^9.0.9", "@types/node": "^25.5.0", + "@vitest/coverage-v8": "^4.1.0", "rimraf": "^6.1.3", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.0" }, "dependencies": { "axios": "^1.13.6", diff --git a/src/__tests__/commands.test.ts b/src/__tests__/commands.test.ts new file mode 100644 index 0000000..5f22669 --- /dev/null +++ b/src/__tests__/commands.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, vi } from 'vitest'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('Command Loading', () => { + it('all command files should export a valid definition', async () => { + const commandsDir = path.resolve(__dirname, '../commands'); + + function getCommandFiles(dir: string): string[] { + const files = fs.readdirSync(dir); + let commandFiles: string[] = []; + + for (const file of files) { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + commandFiles = commandFiles.concat(getCommandFiles(fullPath)); + } else if (file.endsWith('.ts') || file.endsWith('.js')) { + commandFiles.push(fullPath); + } + } + + return commandFiles; + } + + const files = getCommandFiles(commandsDir); + expect(files.length).toBeGreaterThan(0); + + for (const file of files) { + // Use dynamic import to check the module + // We convert absolute path to file:// URL for ESM import on Linux/Windows + const module = await import(`file://${file}`); + + const definition = module.default || Object.values(module).find((val: any) => + val && typeof val === 'object' && val.name && typeof val.action === 'function' + ); + + if (!definition) { + throw new Error(`Command file ${file} does not export a valid command definition (needs name and action function)`); + } + + expect(definition).toBeDefined(); + expect(typeof definition.name).toBe('string'); + expect(typeof definition.action).toBe('function'); + + console.log(`✓ Validated command: ${definition.name} (${path.relative(commandsDir, file)})`); + } + }); +}); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts new file mode 100644 index 0000000..84539c7 --- /dev/null +++ b/src/__tests__/config.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect, vi } from 'vitest'; +import path from 'path'; +import os from 'os'; +import { getConfigPath } from '../config.js'; + +describe('config', () => { + it('should return the correct config path', () => { + const expectedPath = path.join(os.homedir(), '.shsf_config'); + expect(getConfigPath()).toBe(expectedPath); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c634ddb --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + }, + }, +});