Files
ccs/tests/unit/delegation/session-manager.test.js
T
Kai (Tam Nhu) TranandGitHub 4df5a7d357 feat!: delegation system overhaul and .claude/ shipping (v4.1.0) (#8)
## 🚀 Release v4.1.0 - Major Update

**Breaking changes from v3.5.0**. This release includes the complete v4.0.0 delegation overhaul plus v4.1.0 architecture improvements.

---

## 🎯 v4.0.0: Delegation System Overhaul

**Complete rewrite of the delegation infrastructure with enhanced decision-making and streaming support.**

### New Delegation Features

**Stream-JSON Communication Protocol:**
- Real-time token streaming with `{type: "content", data: "..."}` format
- Progress indicators during delegation execution
- Clean separation: stdout for data, stderr for errors
- Handles tool calls, thinking blocks, and text content

**Enhanced Decision Framework:**
- `/ccs:glm` and `/ccs:kimi` slash commands with auto-enhancement
- `[AUTO ENHANCE]` prompts for better model understanding
- Context-aware task delegation with clear boundaries
- Continuation support: `/ccs:glm:continue` and `/ccs:kimi:continue`

**Robust Error Handling:**
- Graceful degradation when profiles unconfigured
- Clear error messages with actionable fixes
- Signal handling (SIGINT/SIGTERM) for clean child process termination
- Session state recovery on interruption

**Performance & Reliability:**
- Headless mode (`-p` flag) for background execution
- Slash command detection and auto-routing
- Validation system with `DelegationValidator`
- Profile readiness checks in `ccs --version`

### Delegation Components

**New Files:**
- `bin/delegation/delegation-handler.js` - Core delegation orchestrator
- `bin/delegation/stream-processor.js` - Real-time output handling
- `bin/utils/delegation-validator.js` - Profile validation
- `.claude/commands/ccs/*.md` - Slash command definitions
- `.claude/skills/ccs-delegation/` - AI decision framework
- `.claude/agents/ccs-delegator.md` - Proactive delegation agent

**Documentation:**
- Complete delegation workflows with mermaid diagrams
- Troubleshooting guides for common issues
- Headless execution patterns

---

##  v4.1.0: Selective Symlinking Architecture

**Single source of truth for CCS items with automatic propagation.**

### New Architecture

**Ship .claude/ Directory:**
- CCS items now ship with npm/sh/ps1 packages
- Selective item-level symlinks: `~/.ccs/.claude/` → `~/.claude/`
- Auto-propagation on `npm update` - zero manual sync
- Backward compatible with existing `~/.ccs/shared/` mechanism

**Symlink Chain:**
```
~/.ccs/.claude/ (source) 
    ↓ selective symlinks
~/.claude/ (CCS items installed here)
    ↑ symlinked by
~/.ccs/shared/
    ↑ symlinked by  
profiles (work, personal, team)
```

### New Commands

**Maintenance Tools:**
- `ccs update` - Re-install CCS symlinks to ~/.claude/
- `ccs doctor` - Added Check 9: CCS symlinks health verification

**Safe Installation:**
- Automatic conflict backup before symlinking
- Idempotent operations (safe to run multiple times)
- Health monitoring and recovery

### New Components

- `bin/utils/claude-symlink-manager.js` - Manages selective symlinks
- Updated all 3 installers (npm postinstall, install.sh, install.ps1)
- Enhanced `ccs doctor` with symlink health checks

---

## 💥 Breaking Changes

**From v3.5.0 → v4.x:**

1. **Delegation commands moved**: 
   - Old: User manually created in `~/.claude/commands/`
   - New: Auto-shipped in `~/.ccs/.claude/`, symlinked to `~/.claude/commands/ccs/`

2. **Slash command format**:
   - New: `/ccs:glm`, `/ccs:kimi`, `/ccs:glm:continue`
   - Old custom commands may need migration

3. **Profile validation**:
   - Placeholders (`YOUR_API_KEY_HERE`) now detected and marked invalid
   - Must configure real API keys for delegation to work

4. **Stream output format**:
   - Headless mode (`-p`) now uses stream-JSON protocol
   - Old text output replaced with structured `{type, data}` format
2025-11-16 06:31:55 -05:00

216 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const { SessionManager } = require('../../../bin/delegation/session-manager');
/**
* Test runner
*/
class TestRunner {
constructor() {
this.tests = [];
this.passed = 0;
this.failed = 0;
}
test(name, fn) {
this.tests.push({ name, fn });
}
async run() {
console.log('\n=== Session Manager Tests ===\n');
for (const { name, fn } of this.tests) {
try {
await fn();
console.log(`[OK] ${name}`);
this.passed++;
} catch (error) {
console.error(`[X] ${name}`);
console.error(` Error: ${error.message}`);
this.failed++;
}
}
console.log(`\nResults: ${this.passed} passed, ${this.failed} failed`);
process.exit(this.failed > 0 ? 1 : 0);
}
}
function assert(condition, message) {
if (!condition) throw new Error(message || 'Assertion failed');
}
function assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(message || `Expected ${expected}, got ${actual}`);
}
}
// Test suite
const runner = new TestRunner();
// Cleanup test sessions before/after
const testSessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json');
function cleanupTestSessions() {
if (fs.existsSync(testSessionsPath)) {
fs.unlinkSync(testSessionsPath);
}
}
/**
* Test 1: Store and retrieve session
*/
runner.test('Store new session', () => {
cleanupTestSessions();
const mgr = new SessionManager();
mgr.storeSession('glm', {
sessionId: 'test123',
totalCost: 0.0025,
cwd: '/home/test'
});
const session = mgr.getLastSession('glm');
assert(session, 'Session should exist');
assertEqual(session.sessionId, 'test123', 'Session ID should match');
assertEqual(session.totalCost, 0.0025, 'Cost should match');
assertEqual(session.turns, 1, 'Should have 1 turn initially');
});
/**
* Test 2: Update session
*/
runner.test('Update existing session', () => {
const mgr = new SessionManager();
// Store initial
mgr.storeSession('glm', {
sessionId: 'test456',
totalCost: 0.001,
cwd: '/home/test'
});
// Update
mgr.updateSession('glm', 'test456', {
totalCost: 0.002
});
const session = mgr.getLastSession('glm');
assertEqual(session.totalCost, 0.003, 'Cost should be aggregated (0.001 + 0.002)');
assertEqual(session.turns, 2, 'Should have 2 turns');
});
/**
* Test 3: Multiple profiles
*/
runner.test('Manage multiple profiles', () => {
const mgr = new SessionManager();
mgr.storeSession('glm', {
sessionId: 'glm123',
totalCost: 0.001,
cwd: '/home/test'
});
mgr.storeSession('kimi', {
sessionId: 'kimi123',
totalCost: 0.002,
cwd: '/home/test'
});
const glmSession = mgr.getLastSession('glm');
const kimiSession = mgr.getLastSession('kimi');
assertEqual(glmSession.sessionId, 'glm123', 'GLM session should be separate');
assertEqual(kimiSession.sessionId, 'kimi123', 'Kimi session should be separate');
});
/**
* Test 4: No session for profile
*/
runner.test('Return null for non-existent profile', () => {
const mgr = new SessionManager();
const session = mgr.getLastSession('nonexistent');
assertEqual(session, null, 'Should return null for unknown profile');
});
/**
* Test 5: Clear profile
*/
runner.test('Clear profile sessions', () => {
const mgr = new SessionManager();
mgr.storeSession('glm', {
sessionId: 'test789',
totalCost: 0.001,
cwd: '/home/test'
});
mgr.clearProfile('glm');
const session = mgr.getLastSession('glm');
assertEqual(session, null, 'Session should be cleared');
});
/**
* Test 6: Cleanup expired sessions
*/
runner.test('Cleanup expired sessions', () => {
const mgr = new SessionManager();
// Store session with old timestamp (31 days ago)
const sessions = {};
const oldTime = Date.now() - (31 * 24 * 60 * 60 * 1000);
sessions['glm:latest'] = {
sessionId: 'old123',
profile: 'glm',
startTime: oldTime,
lastTurnTime: oldTime,
totalCost: 0.001,
turns: 1,
cwd: '/home/test'
};
// Save manually
const dir = path.dirname(mgr.sessionsPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(mgr.sessionsPath, JSON.stringify(sessions));
// Cleanup
mgr.cleanupExpired();
const session = mgr.getLastSession('glm');
assertEqual(session, null, 'Expired session should be removed');
});
/**
* Test 7: Don't cleanup recent sessions
*/
runner.test('Keep recent sessions during cleanup', () => {
const mgr = new SessionManager();
mgr.storeSession('glm', {
sessionId: 'recent123',
totalCost: 0.001,
cwd: '/home/test'
});
mgr.cleanupExpired();
const session = mgr.getLastSession('glm');
assert(session, 'Recent session should not be removed');
assertEqual(session.sessionId, 'recent123');
});
// Cleanup after all tests
runner.run().finally(() => {
cleanupTestSessions();
});