sync: update from private repo (2ef0fa6)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-17 05:23:28 +00:00
parent 517142c61d
commit 1602d52510
42 changed files with 3387 additions and 64 deletions
+119
View File
@@ -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();
});
});