feat(skills): wire opt-in execution runner selection
This commit is contained in:
+82
-2
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user