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.',
    );
  });
});