feat(skills): wire opt-in execution runner selection

This commit is contained in:
William Valentin
2026-02-12 19:23:30 -08:00
parent 30fcccd05a
commit 3272387eaa
3 changed files with 154 additions and 11 deletions
+82 -2
View File
@@ -24,6 +24,7 @@ import {
runInstallerCommandsWithPolicy,
noOpSkillInstallerCommandRunner,
createShellSkillInstallerCommandRunner,
resolveSkillInstallerCommandRunner,
runSkillInstallAction,
} from './skills.js';
import type { Skill } from '../skills/index.js';
@@ -313,7 +314,7 @@ describe('skills CLI helpers', () => {
});
it('builds blocked step envelopes when confirmation is required', () => {
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false });
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false, executionRequested: true });
const envelopes = toInstallerExecutionStepEnvelopes(
[{ installerType: 'brew', command: 'brew install jq' }],
@@ -332,7 +333,7 @@ describe('skills CLI helpers', () => {
});
it('marks install execution policy as confirmation_required when not confirmed', () => {
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false });
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false, executionRequested: true });
expect(policy.confirmed).toBe(false);
expect(policy.execution_enabled).toBe(false);
@@ -347,6 +348,33 @@ describe('skills CLI helpers', () => {
expect(policy.reason).toBe('execution_disabled');
});
it('enables install execution only when requested and confirmed', () => {
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: true, executionRequested: true });
expect(policy.confirmed).toBe(true);
expect(policy.execution_enabled).toBe(true);
expect(policy.reason).toBe('execution_enabled');
});
it('resolves installer runner mode and validates invalid values', () => {
const noop = resolveSkillInstallerCommandRunner();
expect('error' in noop).toBe(false);
if (!('error' in noop)) {
expect(noop.mode).toBe('noop');
expect(noop.runner).toBe(noOpSkillInstallerCommandRunner);
}
const shell = resolveSkillInstallerCommandRunner('shell');
expect('error' in shell).toBe(false);
if (!('error' in shell)) {
expect(shell.mode).toBe('shell');
expect(typeof shell.runner.run).toBe('function');
}
const invalid = resolveSkillInstallerCommandRunner('invalid');
expect(invalid).toEqual({ error: "Invalid runner 'invalid'. Allowed values: noop, shell." });
});
it('does not invoke command runner when policy disables execution', () => {
const runner = {
run: vi.fn((_commands: string[]) => [{ command: 'should-not-run', status: 'succeeded' as const }]),
@@ -676,6 +704,58 @@ describe('skills CLI helpers', () => {
rmSync(root, { recursive: true, force: true });
});
it('runs installer commands when execution is requested and confirmed', () => {
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
const sourceDir = join(root, 'source-skill');
const managedDir = join(root, 'managed');
mkdirSync(sourceDir, { recursive: true });
writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
writeFileSync(
join(sourceDir, 'manifest.json'),
JSON.stringify({
name: 'install-skill',
description: 'Install',
version: '1.0.0',
installers: [{ type: 'download', url: 'https://example.com/install-exec.tgz' }],
}),
'utf-8',
);
const installer = new SkillInstaller(managedDir);
const runner = {
run: vi.fn((commands: string[]) =>
commands.map((command) => ({ command, status: 'succeeded' as const, reason: 'runner_reported_success' })),
),
};
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
const result = runSkillInstallAction(installer, sourceDir, {
mode: 'install',
asJson: true,
confirmed: true,
executionRequested: true,
commandRunner: runner,
});
expect(result.ok).toBe(true);
expect(runner.run).toHaveBeenCalledTimes(1);
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');
expect(payload.execution.executed).toEqual(['download https://example.com/install-exec.tgz -> <default destination>']);
expect(payload.execution.results).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/install-exec.tgz -> <default destination>',
status: 'succeeded',
reason: 'runner_reported_success',
},
]);
logSpy.mockRestore();
rmSync(root, { recursive: true, force: true });
});
it('requires --yes confirmation for uninstall helper', () => {
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
const installer = new SkillInstaller(join(root, 'managed'));