File size: 7,263 Bytes
7a1ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | /**
* @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/<kind>/*.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/<topic>.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.',
);
});
});
|