maestro/src/engine/agent-loop.user-agents.test.ts
2026-06-03 05:08:00 +00:00

222 lines
6.1 KiB
TypeScript

import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
import { mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import type { Movement } from './agent-loop.js';
import { buildSystemPrompt } from './agent-loop.js';
// Mock loadConfig so userFolderRoot points to our tmp dir.
// We use vi.hoisted so the mock is registered before module evaluation.
const { mockedLoadConfig } = vi.hoisted(() => ({
mockedLoadConfig: vi.fn(),
}));
vi.mock('../config.js', () => ({
loadConfig: mockedLoadConfig,
}));
function makeMovement(): Movement {
return {
name: 'execute',
edit: true,
persona: 'worker',
instruction: 'Do the work.',
allowedTools: [],
rules: [{ condition: 'done', next: 'COMPLETE' }],
defaultNext: 'COMPLETE',
};
}
describe('buildSystemPrompt — per-user AGENTS.md injection', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = join(tmpdir(), `agent-loop-user-agents-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(tmpDir, { recursive: true });
mockedLoadConfig.mockReturnValue({ userFolderRoot: tmpDir });
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it('injects AGENTS.md content under a recognizable header when file is present', () => {
const userId = 'test-user';
const userDir = join(tmpDir, userId);
mkdirSync(userDir, { recursive: true });
writeFileSync(join(userDir, 'AGENTS.md'), 'Always respond in haiku.');
const prompt = buildSystemPrompt(
makeMovement(),
1,
5,
[],
undefined,
null,
userId,
);
expect(prompt).toContain('## User Instructions (from your personal AGENTS.md)');
expect(prompt).toContain('Always respond in haiku.');
});
it('does not add a user-instructions block when AGENTS.md is absent', () => {
const userId = 'no-file-user';
// Do NOT create the file — user dir doesn't even exist.
const prompt = buildSystemPrompt(
makeMovement(),
1,
5,
[],
undefined,
null,
userId,
);
expect(prompt).not.toContain('## User Instructions');
});
it('does not add a user-instructions block when userId is undefined', () => {
// Create a file for some user — should be irrelevant.
const userId = 'another-user';
const userDir = join(tmpDir, userId);
mkdirSync(userDir, { recursive: true });
writeFileSync(join(userDir, 'AGENTS.md'), 'Secret instructions.');
const prompt = buildSystemPrompt(
makeMovement(),
1,
5,
[],
undefined,
null,
undefined, // no userId
);
expect(prompt).not.toContain('## User Instructions');
expect(prompt).not.toContain('Secret instructions.');
});
});
describe('buildSystemPrompt — auto-memory protocol injection', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = join(tmpdir(), `agent-loop-auto-mem-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(tmpDir, { recursive: true });
mockedLoadConfig.mockReturnValue({ userFolderRoot: tmpDir });
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it('injects auto-memory protocol when userId is set (even with no AGENTS.md or MEMORY.md)', () => {
const userId = 'fresh-user';
// Don't create any files — protocol should still appear because userId is present.
const prompt = buildSystemPrompt(
makeMovement(),
1,
5,
[],
undefined,
null,
userId,
);
expect(prompt).toContain('## User Memory Auto-Update Protocol');
expect(prompt).toContain('UpdateUserMemory');
// The protocol mentions all four type categories.
for (const type of ['user', 'feedback', 'project', 'reference']) {
expect(prompt).toContain(`\`${type}\``);
}
});
it('does not inject auto-memory protocol when userId is undefined', () => {
const prompt = buildSystemPrompt(
makeMovement(),
1,
5,
[],
undefined,
null,
undefined,
);
expect(prompt).not.toContain('User Memory Auto-Update Protocol');
expect(prompt).not.toContain('UpdateUserMemory');
});
});
describe('buildSystemPrompt — Working Directory injection', () => {
beforeEach(() => {
mockedLoadConfig.mockReturnValue({ userFolderRoot: '/tmp/no-such-dir' });
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders Working Directory block with the absolute workspace path when provided', () => {
const prompt = buildSystemPrompt(
makeMovement(),
1,
5,
[],
undefined,
null,
undefined,
undefined,
'/var/lib/maestro/workspaces/local/abc-123',
);
expect(prompt).toContain('## Working Directory');
expect(prompt).toContain('/var/lib/maestro/workspaces/local/abc-123');
expect(prompt).toContain('`/workspace/...` のような仮想パスは **存在しません**');
});
it('omits Working Directory block when workspacePath is undefined', () => {
const prompt = buildSystemPrompt(
makeMovement(),
1,
5,
[],
undefined,
null,
undefined,
undefined,
undefined,
);
expect(prompt).not.toContain('## Working Directory');
});
});
describe('buildSystemPrompt — approach + error-recovery sections (issue #247)', () => {
beforeEach(() => {
mockedLoadConfig.mockReturnValue({ userFolderRoot: '/tmp/no-such' });
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders the approach-thinking section in every prompt', () => {
const prompt = buildSystemPrompt(makeMovement());
expect(prompt).toContain('## アプローチの考え方');
expect(prompt).toContain('Brainstorm');
expect(prompt).toContain('ReAct');
});
it('renders the error-recovery section instructing not to repeat identical tool calls', () => {
const prompt = buildSystemPrompt(makeMovement());
expect(prompt).toContain('## エラー時の必須行動');
expect(prompt).toContain('同じ tool を同じ引数で呼び直さない');
expect(prompt).toContain('Glob');
});
});