This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
||||
rmSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
} from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
@@ -769,3 +771,120 @@ describe('RunUserScript: audit log hook', () => {
|
||||
expect(auditCalls.some(c => c.action === 'user_script_denied')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── ReadUserAgents / UpdateUserAgents ─────────────────────────────────────────
|
||||
|
||||
describe('ReadUserAgents / UpdateUserAgents', () => {
|
||||
const agentsPath = () => join(userFolderRoot, TEST_USER, 'AGENTS.md');
|
||||
|
||||
it('ReadUserAgents returns a friendly note when AGENTS.md does not exist', async () => {
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const r = await executeTool('ReadUserAgents', {}, ctx);
|
||||
expect(r?.isError).toBe(false);
|
||||
expect(r?.output).toContain('does not exist yet');
|
||||
});
|
||||
|
||||
it('append creates AGENTS.md when empty', async () => {
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const r = await executeTool('UpdateUserAgents', { mode: 'append', new_text: '# 方針\n常に日本語で答える' }, ctx);
|
||||
expect(r?.isError).toBe(false);
|
||||
expect(readFileSync(agentsPath(), 'utf-8')).toBe('# 方針\n常に日本語で答える');
|
||||
});
|
||||
|
||||
it('append adds a blank line between existing content and new text', async () => {
|
||||
writeFileSync(agentsPath(), '# 既存\nルール1');
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'ルール2' }, ctx);
|
||||
expect(readFileSync(agentsPath(), 'utf-8')).toBe('# 既存\nルール1\n\nルール2');
|
||||
});
|
||||
|
||||
it('ReadUserAgents returns the full current content', async () => {
|
||||
writeFileSync(agentsPath(), 'line A\nline B');
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const r = await executeTool('ReadUserAgents', {}, ctx);
|
||||
expect(r?.output).toBe('line A\nline B');
|
||||
});
|
||||
|
||||
it('replace swaps an exact unique match', async () => {
|
||||
writeFileSync(agentsPath(), '# 方針\n古い行\nおわり');
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: '古い行', new_text: '新しい行' }, ctx);
|
||||
expect(r?.isError).toBe(false);
|
||||
expect(readFileSync(agentsPath(), 'utf-8')).toBe('# 方針\n新しい行\nおわり');
|
||||
});
|
||||
|
||||
it('replace does not interpret $-patterns in new_text', async () => {
|
||||
writeFileSync(agentsPath(), 'price: PLACEHOLDER yen');
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'PLACEHOLDER', new_text: '$100 & $&' }, ctx);
|
||||
expect(readFileSync(agentsPath(), 'utf-8')).toBe('price: $100 & $& yen');
|
||||
});
|
||||
|
||||
it('replace errors when old_text is not found', async () => {
|
||||
writeFileSync(agentsPath(), 'something');
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'missing', new_text: 'x' }, ctx);
|
||||
expect(r?.isError).toBe(true);
|
||||
expect(r?.output).toContain('not found');
|
||||
});
|
||||
|
||||
it('replace errors when old_text matches more than once', async () => {
|
||||
writeFileSync(agentsPath(), 'dup\ndup');
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'dup', new_text: 'x' }, ctx);
|
||||
expect(r?.isError).toBe(true);
|
||||
expect(r?.output).toContain('matches 2 times');
|
||||
});
|
||||
|
||||
it('replace on an empty file is rejected with a hint to append', async () => {
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const r = await executeTool('UpdateUserAgents', { mode: 'replace', old_text: 'x', new_text: 'y' }, ctx);
|
||||
expect(r?.isError).toBe(true);
|
||||
expect(r?.output).toContain('append');
|
||||
});
|
||||
|
||||
it('snapshots the prior version to trash/agents-history before overwriting', async () => {
|
||||
writeFileSync(agentsPath(), 'original content');
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'more' }, ctx);
|
||||
|
||||
const histDir = join(userFolderRoot, TEST_USER, 'trash', 'agents-history');
|
||||
const backups = readdirSync(histDir).filter((f) => f.startsWith('AGENTS.md.') && f.endsWith('.bak'));
|
||||
expect(backups.length).toBe(1);
|
||||
expect(readFileSync(join(histDir, backups[0]!), 'utf-8')).toBe('original content');
|
||||
});
|
||||
|
||||
it('rejects when result would exceed the 64KB cap', async () => {
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
const big = 'x'.repeat(64 * 1024 + 1);
|
||||
const r = await executeTool('UpdateUserAgents', { mode: 'append', new_text: big }, ctx);
|
||||
expect(r?.isError).toBe(true);
|
||||
expect(r?.output).toContain('exceeds');
|
||||
});
|
||||
|
||||
it('emits a user_agents_updated audit event', async () => {
|
||||
const auditCalls: Array<{ action: string; detail: object }> = [];
|
||||
setUserFolderToolDeps({
|
||||
sessRepo: null as never,
|
||||
masterKeyPath: '',
|
||||
userFolderRoot,
|
||||
auditLog: (action, detail) => auditCalls.push({ action, detail }),
|
||||
});
|
||||
const ctx = buildCtx({ userId: TEST_USER });
|
||||
await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'hello' }, ctx);
|
||||
expect(auditCalls.some((c) => c.action === 'user_agents_updated')).toBe(true);
|
||||
});
|
||||
|
||||
it('both tools require an authenticated user', async () => {
|
||||
const ctx = buildCtx({ userId: undefined });
|
||||
const r1 = await executeTool('ReadUserAgents', {}, ctx);
|
||||
const r2 = await executeTool('UpdateUserAgents', { mode: 'append', new_text: 'x' }, ctx);
|
||||
expect(r1?.isError).toBe(true);
|
||||
expect(r2?.isError).toBe(true);
|
||||
});
|
||||
|
||||
it('registers both tools in TOOL_DEFS', () => {
|
||||
expect(TOOL_DEFS['ReadUserAgents']).toBeDefined();
|
||||
expect(TOOL_DEFS['UpdateUserAgents']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user