* 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.
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|