/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, expect, it } from 'vitest'; import { SkillExtractionAgent } from './skill-extraction-agent.js'; import { EDIT_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, LS_TOOL_NAME, READ_FILE_TOOL_NAME, SHELL_TOOL_NAME, WRITE_FILE_TOOL_NAME, } from '../tools/tool-names.js'; import { PREVIEW_GEMINI_FLASH_MODEL } from '../config/models.js'; describe('SkillExtractionAgent', () => { const skillsDir = '/tmp/skills'; const sessionIndex = '[NEW] Debug login flow (12 user msgs) — /tmp/chats/session-1.json'; const existingSkillsSummary = '## Workspace Skills (.gemini/skills — do NOT duplicate)\n- **existing-skill**: Existing description'; const agent = SkillExtractionAgent( skillsDir, sessionIndex, existingSkillsSummary, ); it('should expose expected metadata, model, and tools', () => { expect(agent.kind).toBe('local'); expect(agent.name).toBe('confucius'); expect(agent.displayName).toBe('Skill Extractor'); expect(agent.modelConfig.model).toBe(PREVIEW_GEMINI_FLASH_MODEL); expect(agent.memoryInboxAccess).toBe(true); expect(agent.autoMemoryExtractionWriteAccess).toBe(true); expect(agent.includeExtensionContext).toBe(false); expect(agent.toolConfig?.tools).toEqual( expect.arrayContaining([ READ_FILE_TOOL_NAME, WRITE_FILE_TOOL_NAME, EDIT_TOOL_NAME, LS_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, ]), ); expect(agent.toolConfig?.tools).not.toContain(SHELL_TOOL_NAME); }); it('should default to no skill unless recurrence and durability are proven', () => { const prompt = agent.promptConfig.systemPrompt; expect(prompt).toContain('Default to NO SKILL.'); expect(prompt).toContain( 'strong evidence this will recur for future agents in this repo/workflow', ); expect(prompt).toContain('broader than a single incident'); expect(prompt).toContain('A skill MUST meet ALL of these criteria:'); expect(prompt).toContain( 'Future agents in this repo/workflow are likely to need it', ); }); it('should explicitly reject one-off incidents and single-session preferences', () => { const prompt = agent.promptConfig.systemPrompt; expect(prompt).toContain('Single-session preferences'); expect(prompt).toContain('One-off incidents'); expect(prompt).toContain('Output-style preferences'); expect(prompt).toContain('cannot survive renaming the specific'); }); it('should require all memory updates to go through .inbox//*.patch for review', () => { const prompt = SkillExtractionAgent( skillsDir, sessionIndex, existingSkillsSummary, '/tmp/memory', ).promptConfig.systemPrompt; expect(prompt).toContain( 'ALL memory updates are expressed as unified diff `.patch` files', ); expect(prompt).toContain('EXACTLY ONE canonical patch file per kind'); expect(prompt).toContain('extraction.patch'); expect(prompt).not.toContain('MEMORY.patch'); expect(prompt).not.toContain('verify-workflow.patch'); expect(prompt).toContain('IMPORTANT — incremental updates'); expect(prompt).toContain( 'REWRITE that file by combining its existing hunks with your new', ); expect(prompt).toContain('private ->'); expect(prompt).toContain('global ->'); expect(prompt).toContain( 'the target MUST be exactly the single global personal memory', ); expect(prompt).toContain('~/.gemini/GEMINI.md'); expect(prompt).not.toContain('memory.md'); expect(prompt).not.toContain('and siblings'); expect(prompt).toContain( 'Project/workspace shared instructions (GEMINI.md and similar files', ); expect(prompt).toContain('MEMORY PATCH FORMAT (STRICT)'); expect(prompt).toContain('--- /dev/null'); expect(prompt).toContain('NEVER directly edit MEMORY.md'); expect(prompt).toContain( 'Every patch you write is held for /memory inbox review.', ); expect(prompt).toContain('the user must approve each patch'); // The MEMORY.md-as-index discipline: sibling creations should pair with // a MEMORY.md update hunk; the inbox apply step auto-bundles a generic // pointer if the agent forgets, but the agent should write its own. expect(prompt).toContain('PRIVATE MEMORY: MEMORY.md IS THE INDEX'); expect(prompt).toContain( 'when you create a new sibling .md file, your patch SHOULD', ); expect(prompt).toContain('a SECOND HUNK that updates MEMORY.md'); expect(prompt).toContain('inbox apply step'); expect(prompt).toContain('auto-bundle a generic pointer'); // Pointer paths must be ABSOLUTE — the runtime agent reads them directly. expect(prompt).toContain('IMPORTANT — pointer paths must be ABSOLUTE'); expect(prompt).toContain('Always write the full path'); // The example pointer in the prompt also uses the absolute path. expect(prompt).toContain(`+- See /tmp/memory/.md for`); }); it('surfaces existing inbox patches in the initial query when present', () => { const pendingInbox = [ '## private (1)', '', '### extraction.patch', '```', '--- /dev/null', '+++ /tmp/memory/MEMORY.md', '@@ -0,0 +1,1 @@', '+- previously-extracted fact', '```', ].join('\n'); const agentWithInbox = SkillExtractionAgent( skillsDir, sessionIndex, existingSkillsSummary, '/tmp/memory', pendingInbox, ); const query = agentWithInbox.promptConfig.query ?? ''; expect(query).toContain('# Pending Memory Inbox'); expect(query).toContain('extraction.patch'); expect(query).toContain('previously-extracted fact'); expect(query).toContain( 'REWRITE that patch (overwrite the same path) with', ); }); it('omits the pending inbox section when nothing is pending', () => { const agentEmpty = SkillExtractionAgent( skillsDir, sessionIndex, existingSkillsSummary, '/tmp/memory', '', ); const query = agentEmpty.promptConfig.query ?? ''; expect(query).not.toContain('# Pending Memory Inbox'); }); it('should warn that session summaries are user-intent summaries, not workflow evidence', () => { const query = agent.promptConfig.query ?? ''; expect(query).toContain(existingSkillsSummary); expect(query).toContain(sessionIndex); expect(query).toContain('optional workflow hint'); expect(query).toContain( 'workflow hints alone is never enough evidence for a reusable skill.', ); expect(query).toContain( 'Session summaries describe user intent; optional workflow hints describe likely procedural traces.', ); expect(query).toContain('Use workflow hints for routing'); expect(query).toContain( 'Only write a skill if the evidence shows a durable, recurring workflow', ); expect(query).toContain( 'Only write memory if it would clearly help a future session.', ); expect(query).toContain( 'If recurrence, durability, or future reuse is unclear, create no artifact and explain why.', ); }); });