Files
shiro-neko/test/instructions.test.ts
Muhammad Zakir Ramadhan 5b8503fcd9 Initial commit: shiro-neko 0.1.0-beta.1
Agentic coding CLI on Bun, Ink, and the AI SDK.

Core: streamText loop with SDK-level tool approval so a denied call provably never executes; endpoint fallback for OpenAI reasoning models; retry with backoff.

Tools: read/write/edit/glob/grep/bash, path-jailed, gitignore-aware, ripgrep with a JS fallback, binary rejection, live bash streaming.

Agents: five variants crossing thinking level with tool restriction; plan and review withhold mutating tools from the model.

Extensibility: frontmatter skills with on-demand bodies, plugin host with blocking hooks, MCP stdio and HTTP, read-only subagents.

State: durable per-project memory, session task lists, session persistence, compaction that repairs provider-item dependencies.

Distribution: five-platform cross-compiled binaries with checksums, install scripts, CI on three operating systems.

404 tests, typecheck clean.
2026-09-02 17:30:18 +07:00

91 lines
3.5 KiB
TypeScript

import { afterEach, beforeEach, expect, test } from 'bun:test';
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { formatInstructions, INIT_PROMPT, loadInstructions } from '../src/instructions';
import { systemPrompt } from '../src/prompt';
let root: string;
let orig: string;
beforeEach(() => {
orig = process.cwd();
root = mkdtempSync(join(tmpdir(), 'shiro-inst-'));
mkdirSync(join(root, '.git'), { recursive: true });
Bun.write(join(root, '.git', 'HEAD'), 'ref: refs/heads/main');
process.chdir(root);
});
afterEach(() => {
process.chdir(orig);
rmSync(root, { recursive: true, force: true });
});
test('no instruction files yields an empty list and no prompt section', async () => {
const found = await loadInstructions(root);
expect(found).toEqual([]);
expect(formatInstructions(found)).toBe('');
expect(systemPrompt({ cwd: root, instructions: found })).not.toContain('Project instructions');
});
test('AGENTS.md at the root is loaded', async () => {
await Bun.write(join(root, 'AGENTS.md'), '# Rules\nUse tabs, not spaces.\n');
const found = await loadInstructions(root);
expect(found).toHaveLength(1);
expect(found[0]?.text).toContain('Use tabs, not spaces.');
});
test('the instruction text reaches the system prompt', async () => {
await Bun.write(join(root, 'AGENTS.md'), 'Never touch generated/.\n');
const prompt = systemPrompt({ cwd: root, instructions: await loadInstructions(root) });
expect(prompt).toContain('Project instructions');
expect(prompt).toContain('Never touch generated/.');
expect(prompt).toContain('AGENTS.md');
});
test('CLAUDE.md and .shiro.md are recognised too', async () => {
await Bun.write(join(root, 'CLAUDE.md'), 'claude rules\n');
await Bun.write(join(root, '.shiro.md'), 'shiro rules\n');
const names = (await loadInstructions(root)).map((i) => i.path.split(/[\\/]/).at(-1));
expect(names).toEqual(['CLAUDE.md', '.shiro.md']);
});
test('files are collected from the git root down to cwd, outermost first', async () => {
await Bun.write(join(root, 'AGENTS.md'), 'root rules\n');
const nested = join(root, 'packages', 'api');
mkdirSync(nested, { recursive: true });
await Bun.write(join(nested, 'AGENTS.md'), 'api rules\n');
const found = await loadInstructions(nested);
expect(found.map((i) => i.text.trim())).toEqual(['root rules', 'api rules']);
});
test('the walk stops at the git root, ignoring files above it', async () => {
const outside = join(root, '..', `outside-${Date.now()}.md`);
await Bun.write(outside, 'should not be read');
const nested = join(root, 'src');
mkdirSync(nested, { recursive: true });
await Bun.write(join(nested, 'AGENTS.md'), 'nested only\n');
const found = await loadInstructions(nested);
expect(found.map((i) => i.text.trim())).toEqual(['nested only']);
rmSync(outside, { force: true });
});
test('an empty instruction file is skipped', async () => {
await Bun.write(join(root, 'AGENTS.md'), ' \n\n');
expect(await loadInstructions(root)).toEqual([]);
});
test('a huge instruction file is truncated so it cannot crowd out the conversation', async () => {
await Bun.write(join(root, 'AGENTS.md'), 'x'.repeat(50_000));
const found = await loadInstructions(root);
expect(found[0]?.text.length).toBe(12_000);
});
test('the init prompt tells the model to verify rather than guess', () => {
expect(INIT_PROMPT).toContain('AGENTS.md');
expect(INIT_PROMPT).toContain('Investigate first');
expect(INIT_PROMPT).toMatch(/no guesswork|not confirm|guesswork/i);
});