From 81d5c4d730e4e7dac56f58f43654b79cf98f827e Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 12 Feb 2026 19:38:16 -0800 Subject: [PATCH] test(skills): cover install and execute option parsing --- docs/plans/state.json | 12 +++- src/cli/skills.test.ts | 133 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index ba17e60..ddb9d7e 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1478,6 +1478,14 @@ "src/cli/skills.test.ts" ], "test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing" + }, + "skills_cli_option_parsing_integration_tests": { + "status": "completed", + "description": "Added CLI-level integration tests for `skills install`/`skills execute` option parsing and failure modes around `--execute`, `--runner`, and `--confirm`, validating command wiring through commander actions", + "files_modified": [ + "src/cli/skills.test.ts" + ], + "test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing" } } } @@ -1506,7 +1514,7 @@ }, "overall_progress": { - "total_test_count": 1550, + "total_test_count": 1553, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -1526,7 +1534,7 @@ "gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram", "native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback", "remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 2/2 (100%) — component registry, confidence routing. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening", - "next_up": "Skills infrastructure Phase 3: add CLI-level integration tests for `skills install/execute` option parsing (`--execute`, `--runner`, `--confirm`) to lock behavior and failure modes" + "next_up": "Skills infrastructure Phase 3: add explicit user-facing error messaging/tests for invalid `--execute` usage patterns (for example, `--execute` without `--confirm`) and runner/result edge-case receipts" }, "soul_md_and_cron_create": { "date": "2026-02-11", diff --git a/src/cli/skills.test.ts b/src/cli/skills.test.ts index 19472a7..0f131da 100644 --- a/src/cli/skills.test.ts +++ b/src/cli/skills.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest'; import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; +import { Command } from 'commander'; import { SkillInstaller } from '../skills/index.js'; import { toSkillListRows, @@ -27,6 +28,7 @@ import { resolveSkillInstallerCommandRunner, runSkillExecuteAction, runSkillInstallAction, + registerSkillsCommand, } from './skills.js'; import type { Skill } from '../skills/index.js'; @@ -46,6 +48,23 @@ function buildSkill(overrides: Partial): Skill { }; } +function writeSkillsCliConfig(configPath: string, opts: { managedDir: string; bundledDir: string; workspaceDir: string }): void { + writeFileSync( + configPath, + [ + 'models:', + ' default:', + ' provider: ollama', + ' model: test-model', + 'skills:', + ` managed_dir: ${opts.managedDir}`, + ` bundled_dir: ${opts.bundledDir}`, + ` workspace_dir: ${opts.workspaceDir}`, + ].join('\n'), + 'utf-8', + ); +} + describe('skills CLI helpers', () => { it('maps and sorts skill rows', () => { const rows = toSkillListRows([ @@ -870,4 +889,118 @@ describe('skills CLI helpers', () => { rmSync(root, { recursive: true, force: true }); }); + + it('skills install reports invalid runner via CLI option parsing path', async () => { + const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); + const configPath = join(root, 'config.yaml'); + const managedDir = join(root, 'managed'); + const bundledDir = join(root, 'bundled'); + const workspaceDir = join(root, 'workspace'); + mkdirSync(managedDir, { recursive: true }); + mkdirSync(bundledDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir }); + + const program = new Command(); + registerSkillsCommand(program); + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + process.exitCode = undefined; + + await program.parseAsync(['skills', 'install', '/tmp/any-skill', '--runner', 'invalid', '-c', configPath], { from: 'user' }); + + expect(errorSpy).toHaveBeenCalledWith("Invalid runner 'invalid'. Allowed values: noop, shell."); + expect(process.exitCode).toBe(1); + + errorSpy.mockRestore(); + process.exitCode = undefined; + rmSync(root, { recursive: true, force: true }); + }); + + it('skills install parses execute flags and emits execution-enabled JSON receipt', async () => { + const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); + const configPath = join(root, 'config.yaml'); + const sourceDir = join(root, 'source-skill'); + const managedDir = join(root, 'managed'); + const bundledDir = join(root, 'bundled'); + const workspaceDir = join(root, 'workspace'); + mkdirSync(sourceDir, { recursive: true }); + mkdirSync(managedDir, { recursive: true }); + mkdirSync(bundledDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir }); + writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions'); + writeFileSync( + join(sourceDir, 'manifest.json'), + JSON.stringify({ + name: 'cli-install-skill', + description: 'CLI install parse', + version: '1.0.0', + installers: [{ type: 'download', url: 'https://example.com/cli-install.tgz' }], + }), + 'utf-8', + ); + + const program = new Command(); + registerSkillsCommand(program); + + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + process.exitCode = undefined; + + await program.parseAsync( + ['skills', 'install', sourceDir, '--json', '--execute', '--confirm', '--runner', 'noop', '-c', configPath], + { from: 'user' }, + ); + + const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0])); + expect(payload.execution.execution_enabled).toBe(true); + expect(payload.execution.reason).toBe('execution_enabled'); + + logSpy.mockRestore(); + process.exitCode = undefined; + rmSync(root, { recursive: true, force: true }); + }); + + it('skills execute parses execute flags and emits execution-enabled JSON receipt', async () => { + const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); + const configPath = join(root, 'config.yaml'); + const managedDir = join(root, 'managed'); + const bundledDir = join(root, 'bundled'); + const workspaceDir = join(root, 'workspace'); + const skillDir = join(managedDir, 'cli-exec-skill'); + mkdirSync(skillDir, { recursive: true }); + mkdirSync(bundledDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir }); + writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions'); + writeFileSync( + join(skillDir, 'manifest.json'), + JSON.stringify({ + name: 'cli-exec-skill', + description: 'CLI execute parse', + version: '1.0.0', + installers: [{ type: 'download', url: 'https://example.com/cli-exec.tgz' }], + }), + 'utf-8', + ); + + const program = new Command(); + registerSkillsCommand(program); + + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + process.exitCode = undefined; + + await program.parseAsync( + ['skills', 'execute', 'cli-exec-skill', '--json', '--execute', '--confirm', '--runner', 'noop', '-c', configPath], + { from: 'user' }, + ); + + const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0])); + expect(payload.execution_enabled).toBe(true); + expect(payload.reason).toBe('execution_enabled'); + + logSpy.mockRestore(); + process.exitCode = undefined; + rmSync(root, { recursive: true, force: true }); + }); });