import { describe, it, expect } from 'vitest'; import { buildSystemPrompt, buildUserPrompt } from './reflection-prompt.js'; import type { ReflectionInput } from './types.js'; function makeInput(overrides: Partial = {}): 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); }); });