Add /compact skill for manual context compaction (#817)
* feat: add /compact skill for manual context compaction added /compact session command to fight context rot in long-running sessions. Uses Claude Agent SDK's built-in /compact command with auth gating (main-group or is_from_me only). * simplify: remove group-queue modification, streamline denied path confirmed against fresh-clone merge. * refactor: extract handleSessionCommand from index.ts into session-commands.ts Verified: 345/345 tests pass on fresh-clone merge.
This commit is contained in:
188
.claude/skills/add-compact/tests/add-compact.test.ts
Normal file
188
.claude/skills/add-compact/tests/add-compact.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const SKILL_DIR = path.resolve(__dirname, '..');
|
||||
|
||||
describe('add-compact skill package', () => {
|
||||
describe('manifest', () => {
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
content = fs.readFileSync(path.join(SKILL_DIR, 'manifest.yaml'), 'utf-8');
|
||||
});
|
||||
|
||||
it('has a valid manifest.yaml', () => {
|
||||
expect(fs.existsSync(path.join(SKILL_DIR, 'manifest.yaml'))).toBe(true);
|
||||
expect(content).toContain('skill: add-compact');
|
||||
expect(content).toContain('version: 1.0.0');
|
||||
});
|
||||
|
||||
it('has no npm dependencies', () => {
|
||||
expect(content).toContain('npm_dependencies: {}');
|
||||
});
|
||||
|
||||
it('has no env_additions', () => {
|
||||
expect(content).toContain('env_additions: []');
|
||||
});
|
||||
|
||||
it('lists all add files', () => {
|
||||
expect(content).toContain('src/session-commands.ts');
|
||||
expect(content).toContain('src/session-commands.test.ts');
|
||||
});
|
||||
|
||||
it('lists all modify files', () => {
|
||||
expect(content).toContain('src/index.ts');
|
||||
expect(content).toContain('container/agent-runner/src/index.ts');
|
||||
});
|
||||
|
||||
it('has no dependencies', () => {
|
||||
expect(content).toContain('depends: []');
|
||||
});
|
||||
});
|
||||
|
||||
describe('add/ files', () => {
|
||||
it('includes src/session-commands.ts with required exports', () => {
|
||||
const filePath = path.join(SKILL_DIR, 'add', 'src', 'session-commands.ts');
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).toContain('export function extractSessionCommand');
|
||||
expect(content).toContain('export function isSessionCommandAllowed');
|
||||
expect(content).toContain('export async function handleSessionCommand');
|
||||
expect(content).toContain("'/compact'");
|
||||
});
|
||||
|
||||
it('includes src/session-commands.test.ts with test cases', () => {
|
||||
const filePath = path.join(SKILL_DIR, 'add', 'src', 'session-commands.test.ts');
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).toContain('extractSessionCommand');
|
||||
expect(content).toContain('isSessionCommandAllowed');
|
||||
expect(content).toContain('detects bare /compact');
|
||||
expect(content).toContain('denies untrusted sender');
|
||||
});
|
||||
});
|
||||
|
||||
describe('modify/ files exist', () => {
|
||||
const modifyFiles = [
|
||||
'src/index.ts',
|
||||
'container/agent-runner/src/index.ts',
|
||||
];
|
||||
|
||||
for (const file of modifyFiles) {
|
||||
it(`includes modify/${file}`, () => {
|
||||
const filePath = path.join(SKILL_DIR, 'modify', file);
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('intent files exist', () => {
|
||||
const intentFiles = [
|
||||
'src/index.ts.intent.md',
|
||||
'container/agent-runner/src/index.ts.intent.md',
|
||||
];
|
||||
|
||||
for (const file of intentFiles) {
|
||||
it(`includes modify/${file}`, () => {
|
||||
const filePath = path.join(SKILL_DIR, 'modify', file);
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('modify/src/index.ts', () => {
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
content = fs.readFileSync(
|
||||
path.join(SKILL_DIR, 'modify', 'src', 'index.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('imports session command helpers', () => {
|
||||
expect(content).toContain("import { extractSessionCommand, handleSessionCommand, isSessionCommandAllowed } from './session-commands.js'");
|
||||
});
|
||||
|
||||
it('uses const for missedMessages', () => {
|
||||
expect(content).toMatch(/const missedMessages = getMessagesSince/);
|
||||
});
|
||||
|
||||
it('delegates to handleSessionCommand in processGroupMessages', () => {
|
||||
expect(content).toContain('Session command interception (before trigger check)');
|
||||
expect(content).toContain('handleSessionCommand(');
|
||||
expect(content).toContain('cmdResult.handled');
|
||||
expect(content).toContain('cmdResult.success');
|
||||
});
|
||||
|
||||
it('passes deps to handleSessionCommand', () => {
|
||||
expect(content).toContain('sendMessage:');
|
||||
expect(content).toContain('setTyping:');
|
||||
expect(content).toContain('runAgent:');
|
||||
expect(content).toContain('closeStdin:');
|
||||
expect(content).toContain('advanceCursor:');
|
||||
expect(content).toContain('formatMessages');
|
||||
expect(content).toContain('canSenderInteract:');
|
||||
});
|
||||
|
||||
it('has session command interception in startMessageLoop', () => {
|
||||
expect(content).toContain('Session command interception (message loop)');
|
||||
expect(content).toContain('queue.enqueueMessageCheck(chatJid)');
|
||||
});
|
||||
|
||||
it('preserves core index.ts structure', () => {
|
||||
expect(content).toContain('processGroupMessages');
|
||||
expect(content).toContain('startMessageLoop');
|
||||
expect(content).toContain('async function main()');
|
||||
expect(content).toContain('recoverPendingMessages');
|
||||
expect(content).toContain('ensureContainerSystemRunning');
|
||||
});
|
||||
});
|
||||
|
||||
describe('modify/container/agent-runner/src/index.ts', () => {
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
content = fs.readFileSync(
|
||||
path.join(SKILL_DIR, 'modify', 'container', 'agent-runner', 'src', 'index.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('defines KNOWN_SESSION_COMMANDS whitelist', () => {
|
||||
expect(content).toContain("KNOWN_SESSION_COMMANDS");
|
||||
expect(content).toContain("'/compact'");
|
||||
});
|
||||
|
||||
it('uses query() with string prompt for slash commands', () => {
|
||||
expect(content).toContain('prompt: trimmedPrompt');
|
||||
expect(content).toContain('allowedTools: []');
|
||||
});
|
||||
|
||||
it('observes compact_boundary system event', () => {
|
||||
expect(content).toContain('compactBoundarySeen');
|
||||
expect(content).toContain("'compact_boundary'");
|
||||
expect(content).toContain('Compact boundary observed');
|
||||
});
|
||||
|
||||
it('handles error subtypes', () => {
|
||||
expect(content).toContain("resultSubtype?.startsWith('error')");
|
||||
});
|
||||
|
||||
it('registers PreCompact hook for slash commands', () => {
|
||||
expect(content).toContain('createPreCompactHook(containerInput.assistantName)');
|
||||
});
|
||||
|
||||
it('preserves core agent-runner structure', () => {
|
||||
expect(content).toContain('async function runQuery');
|
||||
expect(content).toContain('class MessageStream');
|
||||
expect(content).toContain('function writeOutput');
|
||||
expect(content).toContain('function createPreCompactHook');
|
||||
expect(content).toContain('function createSanitizeBashHook');
|
||||
expect(content).toContain('async function main');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user