Files
shiro-neko/test/ignore.test.ts
T
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

119 lines
4.1 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 { walk } from '../src/ignore';
let dir: string;
let orig: string;
beforeEach(() => {
orig = process.cwd();
dir = mkdtempSync(join(tmpdir(), 'shiro-ignore-'));
process.chdir(dir);
});
afterEach(() => {
process.chdir(orig);
rmSync(dir, { recursive: true, force: true });
});
const collect = async (opts: Parameters<typeof walk>[0] = {}) => {
const out: string[] = [];
for await (const p of walk(opts)) out.push(p);
return out.sort();
};
test('walks nested files and returns posix relative paths', async () => {
await Bun.write('a.ts', '');
await Bun.write('src/deep/b.ts', '');
expect(await collect()).toEqual(['a.ts', 'src/deep/b.ts']);
});
test('.git and node_modules are always skipped, even with no ignore file', async () => {
await Bun.write('keep.ts', '');
await Bun.write('node_modules/pkg/index.js', '');
mkdirSync(join(dir, '.git'), { recursive: true });
await Bun.write('.git/HEAD', 'ref: refs/heads/main');
expect(await collect()).toEqual(['keep.ts']);
});
test('a plain gitignore entry excludes a file', async () => {
await Bun.write('.gitignore', 'secret.txt\n');
await Bun.write('secret.txt', '');
await Bun.write('public.txt', '');
expect(await collect()).toEqual(['.gitignore', 'public.txt']);
});
test('a directory rule excludes everything beneath it', async () => {
await Bun.write('.gitignore', 'dist/\n');
await Bun.write('dist/app.js', '');
await Bun.write('dist/nested/app.js', '');
await Bun.write('src/app.ts', '');
expect(await collect()).toEqual(['.gitignore', 'src/app.ts']);
});
test('an unanchored name matches at any depth', async () => {
await Bun.write('.gitignore', '*.log\n');
await Bun.write('top.log', '');
await Bun.write('deep/nested/inner.log', '');
await Bun.write('deep/keep.ts', '');
expect(await collect()).toEqual(['.gitignore', 'deep/keep.ts']);
});
test('a leading slash anchors the rule to the root', async () => {
await Bun.write('.gitignore', '/build\n');
await Bun.write('build/out.js', '');
await Bun.write('src/build/out.js', '');
expect(await collect()).toEqual(['.gitignore', 'src/build/out.js']);
});
test('a negation re-includes a file the previous rule excluded', async () => {
await Bun.write('.gitignore', '*.env\n!keep.env\n');
await Bun.write('secret.env', '');
await Bun.write('keep.env', '');
expect(await collect()).toEqual(['.gitignore', 'keep.env']);
});
test('** spans directories', async () => {
await Bun.write('.gitignore', 'a/**/c.ts\n');
await Bun.write('a/b/c.ts', '');
await Bun.write('a/b/d/c.ts', '');
await Bun.write('a/keep.ts', '');
expect(await collect()).toEqual(['.gitignore', 'a/keep.ts']);
});
test('a nested gitignore applies only inside its own directory', async () => {
await Bun.write('pkg/.gitignore', 'out.js\n');
await Bun.write('pkg/out.js', '');
await Bun.write('other/out.js', '');
expect(await collect()).toEqual(['other/out.js', 'pkg/.gitignore']);
});
test('.shiroignore is honoured alongside .gitignore', async () => {
await Bun.write('.shiroignore', 'notes/\n');
await Bun.write('notes/todo.md', '');
await Bun.write('src/app.ts', '');
expect(await collect()).toEqual(['.shiroignore', 'src/app.ts']);
});
test('comments and blank lines are skipped', async () => {
await Bun.write('.gitignore', '# a comment\n\n \nreal.txt\n');
await Bun.write('real.txt', '');
await Bun.write('# a comment', '');
expect(await collect()).toContain('# a comment');
expect(await collect()).not.toContain('real.txt');
});
test('noIgnore returns everything except .git and node_modules', async () => {
await Bun.write('.gitignore', 'dist/\n');
await Bun.write('dist/app.js', '');
await Bun.write('src/app.ts', '');
expect(await collect({ noIgnore: true })).toEqual(['.gitignore', 'dist/app.js', 'src/app.ts']);
});
test('limit stops the walk early', async () => {
for (let i = 0; i < 10; i++) await Bun.write(`f${i}.ts`, '');
expect((await collect({ limit: 3 })).length).toBe(3);
});