222 lines
6.1 KiB
TypeScript
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');
|
|
});
|
|
});
|