161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { buildSystemPrompt, buildUserPrompt } from './reflection-prompt.js';
|
|
import type { ReflectionInput } from './types.js';
|
|
|
|
function makeInput(overrides: Partial<ReflectionInput> = {}): ReflectionInput {
|
|
return {
|
|
originalJobId: 'job-1',
|
|
userId: 'u-1',
|
|
pieceName: 'chat',
|
|
pieceSource: 'builtin',
|
|
outcome: 'succeeded',
|
|
taskTitle: 'Summarize the report',
|
|
taskBody: 'Please summarize quarterly-report.pdf',
|
|
activityLogSummary: 'ReadPdf -> Write summary.md (2 iterations)',
|
|
postCompletionComments: [],
|
|
feedback: { rating: null, comment: null, tags: [] },
|
|
resultText: 'Wrote output/summary.md',
|
|
observedRevisions: {},
|
|
memoryIndex: '- [fact one](fact_one.md)',
|
|
memoryEntries: [],
|
|
pieceYaml: 'movements:\n - name: execute\n',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('buildSystemPrompt', () => {
|
|
it('forces submit_reflection tool-call-only output', () => {
|
|
const sys = buildSystemPrompt();
|
|
expect(sys).toContain('submit_reflection');
|
|
});
|
|
|
|
it('states the memory rules: non-trivial lessons, type tagging, 3-change cap, abstain path', () => {
|
|
const sys = buildSystemPrompt();
|
|
expect(sys).toContain('非自明');
|
|
expect(sys).toContain('user | feedback | project | reference');
|
|
expect(sys).toContain('最大 3 件');
|
|
expect(sys).toContain('abstain_reason');
|
|
expect(sys).toContain('should_edit=false');
|
|
});
|
|
|
|
it('requires Why/How-to-apply lines for feedback/project entries', () => {
|
|
const sys = buildSystemPrompt();
|
|
expect(sys).toContain('**Why:**');
|
|
expect(sys).toContain('**How to apply:**');
|
|
});
|
|
|
|
it('hardens piece edits: full-YAML replacement and no engine sentinels in rules[].next', () => {
|
|
const sys = buildSystemPrompt();
|
|
expect(sys).toContain('完全置換');
|
|
expect(sys).toContain('COMPLETE / ABORT / ASK');
|
|
expect(sys).toContain('rules[].next');
|
|
});
|
|
|
|
it('is deterministic (no timestamps or randomness)', () => {
|
|
expect(buildSystemPrompt()).toBe(buildSystemPrompt());
|
|
});
|
|
});
|
|
|
|
describe('buildUserPrompt', () => {
|
|
it('includes task title, body, activity log summary, result and outcome', () => {
|
|
const prompt = buildUserPrompt(makeInput());
|
|
expect(prompt).toContain('title: Summarize the report');
|
|
expect(prompt).toContain('body: Please summarize quarterly-report.pdf');
|
|
expect(prompt).toContain('ReadPdf -> Write summary.md (2 iterations)');
|
|
expect(prompt).toContain('status: succeeded');
|
|
expect(prompt).toContain('result: Wrote output/summary.md');
|
|
});
|
|
|
|
it('contains all section headers', () => {
|
|
const prompt = buildUserPrompt(makeInput());
|
|
for (const header of [
|
|
'## 元タスク',
|
|
'## 活動ログ (圧縮済み)',
|
|
'## ジョブ後のユーザーコメント',
|
|
'## 明示フィードバック',
|
|
'## 結果',
|
|
'## 現在の memory スナップショット',
|
|
'## 現在の piece YAML',
|
|
]) {
|
|
expect(prompt).toContain(header);
|
|
}
|
|
});
|
|
|
|
it('renders post-completion comments with timestamp and author', () => {
|
|
const prompt = buildUserPrompt(makeInput({
|
|
postCompletionComments: [
|
|
{ author: 'alice', body: 'wrong file was summarized', createdAt: '2026-06-10T00:00:00Z' },
|
|
{ author: 'bob', body: 'please retry', createdAt: '2026-06-10T01:00:00Z' },
|
|
],
|
|
}));
|
|
expect(prompt).toContain('- [2026-06-10T00:00:00Z] alice: wrong file was summarized');
|
|
expect(prompt).toContain('- [2026-06-10T01:00:00Z] bob: please retry');
|
|
expect(prompt).not.toContain('(なし)');
|
|
});
|
|
|
|
it('shows (なし) when there are no post-completion comments', () => {
|
|
const prompt = buildUserPrompt(makeInput({ postCompletionComments: [] }));
|
|
expect(prompt).toContain('(なし)');
|
|
});
|
|
|
|
it('shows "rating: none" when no explicit feedback exists', () => {
|
|
const prompt = buildUserPrompt(makeInput());
|
|
expect(prompt).toContain('rating: none');
|
|
expect(prompt).not.toContain('rating: good');
|
|
expect(prompt).not.toContain('rating: bad');
|
|
});
|
|
|
|
it('renders rating, comment and tags when feedback exists', () => {
|
|
const prompt = buildUserPrompt(makeInput({
|
|
feedback: { rating: 'good', comment: 'nice work', tags: ['speed', 'quality'] },
|
|
}));
|
|
expect(prompt).toContain('rating: good');
|
|
expect(prompt).toContain('comment: nice work');
|
|
expect(prompt).toContain('tags: speed, quality');
|
|
});
|
|
|
|
it('omits comment/tags lines when feedback has neither', () => {
|
|
const prompt = buildUserPrompt(makeInput({
|
|
feedback: { rating: 'good', comment: null, tags: [] },
|
|
}));
|
|
expect(prompt).toContain('rating: good');
|
|
expect(prompt).not.toContain('comment:');
|
|
expect(prompt).not.toContain('tags:');
|
|
});
|
|
|
|
it('appends the bad-rating investigation directive only for rating=bad', () => {
|
|
const bad = buildUserPrompt(makeInput({
|
|
feedback: { rating: 'bad', comment: null, tags: [] },
|
|
}));
|
|
expect(bad).toContain('**低評価**');
|
|
|
|
const good = buildUserPrompt(makeInput({
|
|
feedback: { rating: 'good', comment: null, tags: [] },
|
|
}));
|
|
expect(good).not.toContain('**低評価**');
|
|
|
|
const none = buildUserPrompt(makeInput());
|
|
expect(none).not.toContain('**低評価**');
|
|
});
|
|
|
|
it('embeds the memory index, or (空) when the user has no memory', () => {
|
|
const withMemory = buildUserPrompt(makeInput());
|
|
expect(withMemory).toContain('- [fact one](fact_one.md)');
|
|
expect(withMemory).not.toContain('(空)');
|
|
|
|
const empty = buildUserPrompt(makeInput({ memoryIndex: '' }));
|
|
expect(empty).toContain('(空)');
|
|
});
|
|
|
|
it('wraps the current piece YAML in a yaml code fence', () => {
|
|
const prompt = buildUserPrompt(makeInput());
|
|
expect(prompt).toContain('```yaml\nmovements:\n - name: execute\n\n```');
|
|
});
|
|
|
|
it('does not truncate the activity log summary (truncation happens upstream in load-inputs)', () => {
|
|
const long = 'x'.repeat(20000);
|
|
const prompt = buildUserPrompt(makeInput({ activityLogSummary: long }));
|
|
expect(prompt).toContain(long);
|
|
});
|
|
});
|