release 1.0.0: cost control, 41 tools, 29 skills, custom commands, auto-load
This commit is contained in:
1 parent
a22d8e13b1
commit
ffa9a02c26
97 files changed
+4791
-788
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { afterEach, beforeEach, expect, test } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { parseCommand } from '../src/commands';
|
||||
import { expandCommand, loadCustomCommands, type CustomCommand } from '../src/custom-commands';
|
||||
|
||||
let dir: string;
|
||||
let origCwd: string;
|
||||
let origHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
origCwd = process.cwd();
|
||||
origHome = process.env['SHIRO_HOME'];
|
||||
dir = mkdtempSync(join(tmpdir(), 'shiro-cmd-'));
|
||||
process.chdir(dir);
|
||||
// An isolated home so a real ~/.shiro-neko/commands cannot leak into a test.
|
||||
const home = mkdtempSync(join(tmpdir(), 'shiro-home-'));
|
||||
process.env['SHIRO_HOME'] = home;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(origCwd);
|
||||
if (origHome === undefined) delete process.env['SHIRO_HOME'];
|
||||
else process.env['SHIRO_HOME'] = origHome;
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const cmd = (over: Partial<CustomCommand>): CustomCommand => ({
|
||||
name: 'review-diff',
|
||||
description: 'review the current diff',
|
||||
body: 'Review the diff.',
|
||||
origin: 'project',
|
||||
path: '/x/review-diff.md',
|
||||
...over,
|
||||
});
|
||||
|
||||
test('a project command is discovered from .shiro/commands', async () => {
|
||||
await Bun.write(join('.shiro', 'commands', 'deploy.md'), '---\ndescription: ship it\n---\nDeploy the app.');
|
||||
const cmds = await loadCustomCommands(dir);
|
||||
expect(cmds.map((c) => c.name)).toContain('deploy');
|
||||
expect(cmds.find((c) => c.name === 'deploy')?.origin).toBe('project');
|
||||
});
|
||||
|
||||
test('frontmatter supplies description and agent; the body is the prompt', async () => {
|
||||
await Bun.write(
|
||||
join('.shiro', 'commands', 'fix.md'),
|
||||
'---\ndescription: fix a test\nagent: deep\n---\nFix the failing test.',
|
||||
);
|
||||
const cmds = await loadCustomCommands(dir);
|
||||
const fix = cmds.find((c) => c.name === 'fix');
|
||||
expect(fix?.description).toBe('fix a test');
|
||||
expect(fix?.agent).toBe('deep');
|
||||
expect(fix?.body).toBe('Fix the failing test.');
|
||||
});
|
||||
|
||||
test('a project command shadows a user command of the same name', async () => {
|
||||
const home = process.env['SHIRO_HOME']!;
|
||||
await Bun.write(join(home, '.shiro-neko', 'commands', 'go.md'), 'user version');
|
||||
await Bun.write(join('.shiro', 'commands', 'go.md'), 'project version');
|
||||
const cmds = await loadCustomCommands(dir);
|
||||
const go = cmds.find((c) => c.name === 'go');
|
||||
expect(go?.origin).toBe('project');
|
||||
expect(go?.body).toBe('project version');
|
||||
});
|
||||
|
||||
test('an empty body or a bad filename is skipped', async () => {
|
||||
await Bun.write(join('.shiro', 'commands', 'empty.md'), '---\ndescription: nothing\n---\n ');
|
||||
await Bun.write(join('.shiro', 'commands', 'BAD NAME.md'), 'not a valid command name');
|
||||
const cmds = await loadCustomCommands(dir);
|
||||
expect(cmds.map((c) => c.name)).not.toContain('empty');
|
||||
expect(cmds).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('the parser resolves a custom command and splits its arguments', () => {
|
||||
const action = parseCommand('/review-diff src/auth.ts', [cmd({})]);
|
||||
expect(action).toEqual({ type: 'custom', command: expect.objectContaining({ name: 'review-diff' }), args: ['src/auth.ts'] });
|
||||
});
|
||||
|
||||
test('a custom command never shadows a built-in', () => {
|
||||
const action = parseCommand('/cost', [cmd({ name: 'cost', body: 'hijack' })]);
|
||||
expect(action.type).toBe('cost');
|
||||
});
|
||||
|
||||
test('$ARGUMENTS and positionals expand', async () => {
|
||||
const c = cmd({ body: 'Review $1 against $2. All: $ARGUMENTS' });
|
||||
const out = await expandCommand(c, ['a.ts', 'b.ts']);
|
||||
expect(out).toBe('Review a.ts against b.ts. All: a.ts b.ts');
|
||||
});
|
||||
|
||||
test('a missing positional expands to nothing', async () => {
|
||||
const c = cmd({ body: 'one $1 two $2 end' });
|
||||
expect(await expandCommand(c, ['only'])).toBe('one only two end');
|
||||
});
|
||||
|
||||
test('a shell substitution inlines its output', async () => {
|
||||
const c = cmd({ body: 'The branch is !`echo feature-x`.' });
|
||||
const out = await expandCommand(c, []);
|
||||
expect(out).toBe('The branch is feature-x.');
|
||||
});
|
||||
|
||||
test('a destructive shell substitution is refused by the guard', async () => {
|
||||
const c = cmd({ body: 'run !`rm -rf /`' });
|
||||
await expect(expandCommand(c, [])).rejects.toThrow(/refused/i);
|
||||
});
|
||||
|
||||
test('a failing substitution reports the command and exit', async () => {
|
||||
const c = cmd({ body: 'value: !`exit 3`' });
|
||||
await expect(expandCommand(c, [])).rejects.toThrow(/exited 3/);
|
||||
});
|
||||
Reference in new issue
Block a user