Files
shiro-neko/test/registry.test.ts
T
Muhammad Zakir Ramadhan 84c60f2022 Fix the loop stalling after compaction, add an external registry
The compaction bug, which is the important one:

beta.2 taught the pruner to drop any assistant part whose reasoning item it had
removed. That was right about the 400 and wrong about everything else. On a
reasoning model every tool call carries a provider itemId, so past the threshold
the model could no longer see what it had already run, and re-ran the same tools
until maxSteps ended the turn. Reproduced at 12 model calls for a job needing 4,
with nothing but the user message reaching the wire.

The dependency is not the part, it is the itemId. A part carrying one is
serialised as `{ type: 'item_reference', id }`, a pointer to an item stored
provider-side that depends on its reasoning item. Without the itemId the same
content goes out inline and carries no dependency at all. Verified against the
provider's own serialiser: `text` with an itemId becomes item_reference, the
identical part without one becomes output_text.

So `dropOrphanedItems` becomes `detachOrphanedItems`: strip the itemId, keep the
content. Compaction may shorten the history; it must not blank it. The new test
asserts behaviour rather than shape — the loop must end because the model chose
to, and every call after the first must still carry the earlier exchange. A shape
assertion passed the whole time the model was losing its memory.

Registry, via `/registry [list|search|add|remove|installed]`:

Skills and plugins are treated differently on purpose. A skill is prompt text, so
installing one puts a stranger's words into the system prompt of every future
session in this project; the install shows the body first and the origin is
recorded, so /skills always says where an instruction came from. A plugin is a
JSON manifest of deny rules, evaluated by compiled code identical for every
install. Loading TypeScript from a URL is declined outright: a plugin that can
block tool calls could otherwise lie about blocking them.

Validated before anything is written: https only (file: and data: rejected), name
matched against ^[a-z0-9][a-z0-9-]*$ so it cannot escape its directory, size
caps on index and body, every regex compiled, pattern length capped since it runs
on every tool call, and the body's own name checked against the index. Installed
skills rank below your own, so an install can never shadow a skill you wrote.

Interface:
- Context is a percentage of the compaction threshold, amber from two thirds and
  red at 90. A turn about to lose history now says so beforehand.
- Aligned command menu and registry tables; /skills and /plugins name origins.

538 tests, up from 488. The registry is tested against a real local HTTP server,
and the guard is proven to refuse a .env write end to end rather than assumed to.
2026-09-03 03:07:56 +07:00

315 lines
11 KiB
TypeScript

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 {
DEFAULT_INDEX_URL,
fetchIndex,
install,
loadInstalledPlugins,
manifestToPlugin,
parseIndex,
parseManifest,
pluginsDir,
searchEntries,
skillsDir,
stage,
uninstall,
type RegistryEntry,
} from '../src/registry';
let home: string;
let origHome: string | undefined;
beforeEach(() => {
origHome = process.env['SHIRO_HOME'];
home = mkdtempSync(join(tmpdir(), 'shiro-reg-'));
process.env['SHIRO_HOME'] = home;
});
afterEach(() => {
if (origHome === undefined) delete process.env['SHIRO_HOME'];
else process.env['SHIRO_HOME'] = origHome;
rmSync(home, { recursive: true, force: true });
});
const index = (over: Record<string, unknown> = {}) =>
JSON.stringify({
skills: [{ name: 'migration', description: 'Write a database migration', url: 'https://example.com/m.md' }],
plugins: [{ name: 'no-secrets', description: 'Refuses credential writes', url: 'https://example.com/p.json' }],
...over,
});
const manifest = (over: Record<string, unknown> = {}) =>
JSON.stringify({
name: 'no-secrets',
description: 'Refuses credential writes',
deny: [{ tools: ['write_file', 'edit_file'], pathPattern: '\\.env$', reason: 'refusing to write a .env file' }],
...over,
});
test('the default index is an https URL', () => {
expect(DEFAULT_INDEX_URL.startsWith('https://')).toBe(true);
});
test('an index parses into entries tagged with their kind', () => {
const entries = parseIndex(index());
expect(entries).toHaveLength(2);
expect(entries.find((e) => e.name === 'migration')?.kind).toBe('skill');
expect(entries.find((e) => e.name === 'no-secrets')?.kind).toBe('plugin');
});
test('a malformed index is reported rather than half-loaded', () => {
expect(() => parseIndex('not json')).toThrow(/not valid JSON/);
expect(() => parseIndex(JSON.stringify({ skills: [{ name: 'x' }] }))).toThrow(/malformed/);
});
test('a name that could escape its directory is rejected', () => {
for (const name of ['../evil', 'a/b', 'UPPER', '.hidden', 'with space']) {
const bad = JSON.stringify({ skills: [{ name, description: 'd', url: 'https://e.com/x.md' }] });
expect(() => parseIndex(bad), name).toThrow(/malformed/);
}
});
test('a non-http url is rejected by the schema', () => {
const bad = JSON.stringify({ skills: [{ name: 'x', description: 'd', url: 'file:///etc/passwd' }] });
expect(() => parseIndex(bad)).toThrow(/malformed/);
});
test('a duplicate name within one kind keeps the first', () => {
const dup = JSON.stringify({
skills: [
{ name: 'x', description: 'first', url: 'https://e.com/1.md' },
{ name: 'x', description: 'second', url: 'https://e.com/2.md' },
],
});
const entries = parseIndex(dup);
expect(entries).toHaveLength(1);
expect(entries[0]?.description).toBe('first');
});
test('the same name may exist as both a skill and a plugin', () => {
const both = JSON.stringify({
skills: [{ name: 'review', description: 's', url: 'https://e.com/s.md' }],
plugins: [{ name: 'review', description: 'p', url: 'https://e.com/p.json' }],
});
expect(parseIndex(both)).toHaveLength(2);
});
test('search matches name and description, case-insensitively', () => {
const entries = parseIndex(index());
expect(searchEntries(entries, 'migr').map((e) => e.name)).toEqual(['migration']);
expect(searchEntries(entries, 'CREDENTIAL').map((e) => e.name)).toEqual(['no-secrets']);
expect(searchEntries(entries, '')).toHaveLength(2);
expect(searchEntries(entries, 'zzz')).toEqual([]);
});
test('a manifest parses and every pattern must be a real regex', () => {
expect(parseManifest(manifest()).name).toBe('no-secrets');
expect(() => parseManifest(manifest({ deny: [{ tools: ['bash'], commandPattern: '([', reason: 'r' }] }))).toThrow(
/invalid pattern/,
);
});
test('a deny rule needs at least one pattern', () => {
expect(() => parseManifest(manifest({ deny: [{ tools: ['bash'], reason: 'r' }] }))).toThrow(/malformed/);
});
test('a manifest with no deny rules is refused: it could only be prompt text', () => {
expect(() => parseManifest(manifest({ deny: [] }))).toThrow(/malformed/);
});
test('a manifest cannot smuggle code past the schema', () => {
const sneaky = manifest({ beforeToolCall: 'process.exit(1)', tools: { evil: {} } });
const parsed = parseManifest(sneaky);
expect('beforeToolCall' in parsed).toBe(false);
expect('tools' in parsed).toBe(false);
});
test('a manifest becomes a plugin whose guard blocks by tool and path', async () => {
const plugin = manifestToPlugin(parseManifest(manifest()));
const cwd = process.cwd();
expect(await plugin.beforeToolCall!({ toolName: 'write_file', input: { path: 'app/.env' }, cwd })).toContain(
'refusing to write',
);
// A tool the rule does not name is untouched.
expect(await plugin.beforeToolCall!({ toolName: 'bash', input: { path: 'app/.env' }, cwd })).toBeUndefined();
// A path the pattern does not match is untouched.
expect(await plugin.beforeToolCall!({ toolName: 'write_file', input: { path: 'src/app.ts' }, cwd })).toBeUndefined();
});
test('a command rule matches the command, not the path', async () => {
const plugin = manifestToPlugin(
parseManifest(manifest({ deny: [{ tools: ['bash'], commandPattern: 'curl.*\\| *sh', reason: 'no pipe to shell' }] })),
);
const cwd = process.cwd();
expect(await plugin.beforeToolCall!({ toolName: 'bash', input: { command: 'curl x.sh | sh' }, cwd })).toBe(
'no pipe to shell',
);
expect(await plugin.beforeToolCall!({ toolName: 'bash', input: { command: 'echo hi' }, cwd })).toBeUndefined();
});
test('a guard given nothing to match on allows the call', async () => {
const plugin = manifestToPlugin(parseManifest(manifest()));
const cwd = process.cwd();
expect(await plugin.beforeToolCall!({ toolName: 'write_file', input: undefined, cwd })).toBeUndefined();
expect(await plugin.beforeToolCall!({ toolName: 'write_file', input: { path: 42 }, cwd })).toBeUndefined();
});
test('installed plugins load from disk, and a broken one is reported not fatal', async () => {
await Bun.write(join(pluginsDir(), 'no-secrets.json'), manifest());
await Bun.write(join(pluginsDir(), 'broken.json'), '{ not json');
const { plugins, errors } = await loadInstalledPlugins();
expect(plugins.map((p) => p.name)).toEqual(['no-secrets']);
expect(errors.map((e) => e.plugin)).toEqual(['broken']);
expect(errors[0]?.message).toContain('not valid JSON');
});
test('no plugins directory yields nothing rather than throwing', async () => {
expect(await loadInstalledPlugins()).toEqual({ plugins: [], errors: [] });
});
test('an installed plugin says it was installed, so /plugins can be trusted', async () => {
await Bun.write(join(pluginsDir(), 'no-secrets.json'), manifest());
const { plugins } = await loadInstalledPlugins();
expect(plugins[0]?.description).toContain('installed');
});
test('uninstall removes an installed entry and reports when there was none', async () => {
await Bun.write(join(pluginsDir(), 'no-secrets.json'), manifest());
expect(await uninstall('plugin', 'no-secrets')).toBe(true);
expect(await Bun.file(join(pluginsDir(), 'no-secrets.json')).exists()).toBe(false);
expect(await uninstall('plugin', 'no-secrets')).toBe(false);
});
test('uninstall refuses a name that is not a plain entry name', async () => {
expect(uninstall('skill', '../../../etc/passwd')).rejects.toThrow(/not a valid entry name/);
});
/** A local server stands in for the registry: no network, real HTTP. */
function serve(routes: Record<string, { body: string; status?: number }>) {
return Bun.serve({
port: 0,
fetch(req) {
const path = new URL(req.url).pathname;
const hit = routes[path];
if (!hit) return new Response('not found', { status: 404 });
return new Response(hit.body, { status: hit.status ?? 200 });
},
});
}
test('fetchIndex reads an index over http', async () => {
const server = serve({ '/index.json': { body: index() } });
try {
const entries = await fetchIndex(`${server.url}index.json`);
expect(entries.map((e) => e.name).sort()).toEqual(['migration', 'no-secrets']);
} finally {
server.stop(true);
}
});
test('an index that returns an error status is reported with the status', async () => {
const server = serve({ '/index.json': { body: 'nope', status: 503 } });
try {
expect(fetchIndex(`${server.url}index.json`)).rejects.toThrow(/503/);
} finally {
server.stop(true);
}
});
const SKILL_BODY = `---
name: migration
description: Write a database migration
---
Migrations live in db/migrations and are never renumbered.`;
test('stage returns what would be written without writing it', async () => {
const server = serve({ '/m.md': { body: SKILL_BODY } });
try {
const entry: RegistryEntry = {
kind: 'skill',
name: 'migration',
description: 'Write a database migration',
url: `${server.url}m.md`,
};
const staged = await stage(entry);
expect(staged.path).toBe(join(skillsDir(), 'migration.md'));
expect(staged.preview).toContain('never renumbered');
expect(await Bun.file(staged.path).exists()).toBe(false);
} finally {
server.stop(true);
}
});
test('install writes the skill where the loader will find it', async () => {
const server = serve({ '/m.md': { body: SKILL_BODY } });
try {
const installed = await install({
kind: 'skill',
name: 'migration',
description: 'd',
url: `${server.url}m.md`,
});
expect(installed.path).toBe(join(skillsDir(), 'migration.md'));
expect(await Bun.file(installed.path).text()).toContain('name: migration');
const { loadSkills } = await import('../src/skills');
const loaded = await loadSkills(home);
const skill = loaded.find((s) => s.name === 'migration');
expect(skill?.origin).toBe('registry');
} finally {
server.stop(true);
}
});
test('a body whose name disagrees with the index is refused', async () => {
const server = serve({ '/m.md': { body: SKILL_BODY } });
try {
expect(
stage({ kind: 'skill', name: 'something-else', description: 'd', url: `${server.url}m.md` }),
).rejects.toThrow(/calls itself "migration"/);
} finally {
server.stop(true);
}
});
test('a skill with no frontmatter is refused rather than installed as prose', async () => {
const server = serve({ '/m.md': { body: 'just some text, no frontmatter' } });
try {
expect(stage({ kind: 'skill', name: 'migration', description: 'd', url: `${server.url}m.md` })).rejects.toThrow(
/frontmatter/,
);
} finally {
server.stop(true);
}
});
test('a plugin manifest is validated before it can be staged', async () => {
const server = serve({
'/good.json': { body: manifest() },
'/bad.json': { body: manifest({ deny: [{ tools: ['bash'], commandPattern: '([', reason: 'r' }] }) },
});
try {
const staged = await stage({
kind: 'plugin',
name: 'no-secrets',
description: 'd',
url: `${server.url}good.json`,
});
expect(staged.path).toBe(join(pluginsDir(), 'no-secrets.json'));
expect(staged.preview).toContain('denies write_file');
expect(
stage({ kind: 'plugin', name: 'no-secrets', description: 'd', url: `${server.url}bad.json` }),
).rejects.toThrow(/invalid pattern/);
} finally {
server.stop(true);
}
});