mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f869e823af | ||
|
|
8d0b179f94 | ||
|
|
98fa55b7b2 | ||
|
|
c46ac30631 | ||
|
|
dfcc14bfd2 | ||
|
|
a068008409 | ||
|
|
0aa31f100e | ||
|
|
314a160458 | ||
|
|
e7ee5595c5 |
@@ -1,5 +1,33 @@
|
||||
# aicodeman
|
||||
|
||||
## 0.3.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ### Session Resume & History
|
||||
- Add `resumeSessionId` support for conversation resume after reboot
|
||||
- Add history session resume UI and API with route shell sessions routing fix
|
||||
- Improve session resume reliability and persist user settings across refresh
|
||||
- Correct `claudeSessionId` for resumed sessions
|
||||
|
||||
### Terminal & Frontend
|
||||
- Upgrade xterm.js 5.3 → 6.0 with native DEC 2026 synchronized output
|
||||
- Increase terminal scrollback from 5,000 to 20,000 lines
|
||||
- Reduce default font size and persist tab state across refresh
|
||||
- Resolve terminal resize scrollback ghost renders
|
||||
- Hide subagent monitor panel by default
|
||||
|
||||
### Installer
|
||||
- Auto-detect existing install and run update instead of fresh install
|
||||
- Auto-restart codeman-web service after update if running
|
||||
- Show restart command when codeman-web is not a systemd service
|
||||
- Fix one-liner restart command for background processes
|
||||
|
||||
### Codebase Quality
|
||||
- Remove dead code, consolidate imports, extract constants
|
||||
- Repair 15 pre-existing subagent-watcher test failures
|
||||
- Clean up DEC sync dead code
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -52,7 +52,7 @@ When user says "COM":
|
||||
4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json`
|
||||
5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web`
|
||||
|
||||
**Version**: 0.3.10 (must match `package.json`)
|
||||
**Version**: 0.3.11 (must match `package.json`)
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
@@ -35,11 +35,6 @@ codeman web
|
||||
# Open http://localhost:3000 — press Ctrl+Enter to start your first session
|
||||
```
|
||||
|
||||
**Update to latest version:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Ark0N/Codeman/master/install.sh | bash -s update
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>Run as a background service</strong></summary>
|
||||
|
||||
|
||||
Binary file not shown.
+18
-2
@@ -1283,7 +1283,16 @@ update() {
|
||||
npm run build --quiet 2>/dev/null || npm run build
|
||||
success "Updated to $(node -e "console.log(require('./package.json').version)")"
|
||||
echo ""
|
||||
echo -e " ${DIM}Restart codeman web to use the new version.${NC}"
|
||||
|
||||
# Auto-restart systemd service if it's running, otherwise tell the user
|
||||
if systemctl --user is-active codeman-web.service &>/dev/null; then
|
||||
info "Restarting codeman-web service..."
|
||||
systemctl --user restart codeman-web.service
|
||||
success "codeman-web service restarted"
|
||||
else
|
||||
echo -e " ${DIM}Restart codeman web to use the new version:${NC}"
|
||||
echo -e " ${CYAN}pkill -f 'codeman.*web'; codeman web &${NC}"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
@@ -1355,5 +1364,12 @@ uninstall() {
|
||||
case "${1:-}" in
|
||||
update) update ;;
|
||||
uninstall) uninstall ;;
|
||||
*) main "$@" ;;
|
||||
*)
|
||||
if [[ -z "${1:-}" && -d "$INSTALL_DIR/.git" ]]; then
|
||||
print_banner
|
||||
update
|
||||
else
|
||||
main "$@"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aicodeman",
|
||||
"version": "0.3.10",
|
||||
"version": "0.3.11",
|
||||
"description": "The missing control plane for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -28,8 +28,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { getAugmentedPath } from './utils/claude-cli-resolver.js';
|
||||
import { ANSI_ESCAPE_PATTERN_SIMPLE } from './utils/index.js';
|
||||
import { getAugmentedPath, ANSI_ESCAPE_PATTERN_SIMPLE } from './utils/index.js';
|
||||
import { AI_CHECK_MAX_BACKOFF_MS } from './config/ai-defaults.js';
|
||||
|
||||
// ========== Security Validation ==========
|
||||
|
||||
@@ -56,11 +56,6 @@ export const TRIM_TEXT_TO = 768 * 1024; // 768KB
|
||||
*/
|
||||
export const MAX_MESSAGES = 1000;
|
||||
|
||||
/**
|
||||
* Number of messages to keep when trimming (80% of max).
|
||||
*/
|
||||
export const TRIM_MESSAGES_TO = 800;
|
||||
|
||||
// ============================================================================
|
||||
// Line Buffer Limits
|
||||
// ============================================================================
|
||||
@@ -85,3 +80,14 @@ export const MAX_RESPAWN_BUFFER_SIZE = 1 * 1024 * 1024; // 1MB
|
||||
* Size to trim respawn buffer to when max is exceeded.
|
||||
*/
|
||||
export const TRIM_RESPAWN_BUFFER_TO = 512 * 1024; // 512KB
|
||||
|
||||
// ============================================================================
|
||||
// File Peek Limits
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Maximum bytes to read when peeking at the beginning of a file.
|
||||
* Used with `createReadStream({ end })` (inclusive) to read the first 8KB,
|
||||
* which is enough to extract metadata from the first few JSONL lines.
|
||||
*/
|
||||
export const FILE_PEEK_BYTES = 8 * 1024 - 1; // 8KB (inclusive end offset)
|
||||
|
||||
@@ -11,11 +11,5 @@
|
||||
/** Max input length per API request (bytes) */
|
||||
export const MAX_INPUT_LENGTH = 64 * 1024;
|
||||
|
||||
/** Max terminal columns for resize requests */
|
||||
export const MAX_TERMINAL_COLS = 500;
|
||||
|
||||
/** Max terminal rows for resize requests */
|
||||
export const MAX_TERMINAL_ROWS = 200;
|
||||
|
||||
/** Max session name length (chars) */
|
||||
export const MAX_SESSION_NAME_LENGTH = 128;
|
||||
|
||||
@@ -521,7 +521,7 @@ export class PlanOrchestrator {
|
||||
} finally {
|
||||
// Always clean up session and progress interval — centralizing here
|
||||
// prevents the race where cancel() and catch both try to manage the set
|
||||
await session.stop().catch(() => {});
|
||||
await session.stop().catch(() => {}); // Ignore - session cleanup is best-effort in finally block
|
||||
this.runningSessions.delete(session);
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
@@ -651,7 +651,7 @@ export class PlanOrchestrator {
|
||||
} finally {
|
||||
// Always clean up session and progress interval — centralizing here
|
||||
// prevents the race where cancel() and catch both try to manage the set
|
||||
await session.stop().catch(() => {});
|
||||
await session.stop().catch(() => {}); // Ignore - session cleanup is best-effort in finally block
|
||||
this.runningSessions.delete(session);
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@ import { Session } from './session.js';
|
||||
import { AiIdleChecker, type AiCheckResult, type AiCheckState } from './ai-idle-checker.js';
|
||||
import { AiPlanChecker, type AiPlanCheckResult } from './ai-plan-checker.js';
|
||||
import type { TeamWatcher } from './team-watcher.js';
|
||||
import { BufferAccumulator } from './utils/buffer-accumulator.js';
|
||||
import { ANSI_ESCAPE_PATTERN_SIMPLE, assertNever, CleanupManager } from './utils/index.js';
|
||||
import { BufferAccumulator, ANSI_ESCAPE_PATTERN_SIMPLE, assertNever, CleanupManager } from './utils/index.js';
|
||||
import { MAX_RESPAWN_BUFFER_SIZE, TRIM_RESPAWN_BUFFER_TO as RESPAWN_BUFFER_TRIM_SIZE } from './config/buffer-limits.js';
|
||||
import {
|
||||
isCompletionMessage,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { ClaudeMode } from './types.js';
|
||||
import { getAugmentedPath } from './utils/claude-cli-resolver.js';
|
||||
import { getAugmentedPath } from './utils/index.js';
|
||||
|
||||
/**
|
||||
* Build Claude CLI permission flags based on the configured mode.
|
||||
|
||||
+1
-1
@@ -48,8 +48,8 @@ import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
|
||||
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
|
||||
import { RalphTracker } from './ralph-tracker.js';
|
||||
import { BashToolParser } from './bash-tool-parser.js';
|
||||
import { BufferAccumulator } from './utils/buffer-accumulator.js';
|
||||
import {
|
||||
BufferAccumulator,
|
||||
ANSI_ESCAPE_PATTERN_FULL,
|
||||
TOKEN_PATTERN,
|
||||
SPINNER_PATTERN,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* Tracks per-agent: status, token counts, model, description, tool call count, liveness (PID).
|
||||
*
|
||||
* @dependencies config/map-limits (MAX_TRACKED_AGENTS, PENDING_TOOL_CALL_TTL_MS),
|
||||
* utils (CleanupManager, KeyedDebouncer)
|
||||
* config/buffer-limits (FILE_PEEK_BYTES), utils (CleanupManager, KeyedDebouncer)
|
||||
* @consumedby web/server (SSE broadcast), session (subagent-session correlation)
|
||||
* @emits subagent:discovered, subagent:updated, subagent:tool_call, subagent:tool_result,
|
||||
* subagent:progress, subagent:message, subagent:completed
|
||||
@@ -35,6 +35,7 @@ import { execFile } from 'node:child_process';
|
||||
import { readFile, readdir, stat as statAsync } from 'node:fs/promises';
|
||||
import { PENDING_TOOL_CALL_TTL_MS, MAX_PENDING_TOOL_CALLS, MAX_TRACKED_AGENTS } from './config/map-limits.js';
|
||||
import { STALE_DATA_MAX_AGE_MS } from './config/server-timing.js';
|
||||
import { FILE_PEEK_BYTES } from './config/buffer-limits.js';
|
||||
import { CleanupManager, KeyedDebouncer } from './utils/index.js';
|
||||
|
||||
// ========== Types ==========
|
||||
@@ -1009,7 +1010,7 @@ export class SubagentWatcher extends EventEmitter {
|
||||
private async extractDescriptionFromFile(filePath: string): Promise<string | undefined> {
|
||||
try {
|
||||
// Only read the first 8KB — more than enough for 5 JSONL lines
|
||||
const stream = createReadStream(filePath, { end: 8191 });
|
||||
const stream = createReadStream(filePath, { end: FILE_PEEK_BYTES });
|
||||
const rl = createInterface({ input: stream });
|
||||
|
||||
return await new Promise<string | undefined>((resolve) => {
|
||||
@@ -1141,10 +1142,10 @@ export class SubagentWatcher extends EventEmitter {
|
||||
|
||||
if (this.fileAgentContext.has(filePath)) {
|
||||
// Known file — handle content change
|
||||
this.handleFileChange(filePath).catch(() => {});
|
||||
this.handleFileChange(filePath).catch(() => {}); // Ignore - errors logged internally, don't crash watcher callback
|
||||
} else {
|
||||
// New file — register it
|
||||
this.registerAgentFile(filePath, projectHash, sessionId).catch(() => {});
|
||||
this.registerAgentFile(filePath, projectHash, sessionId).catch(() => {}); // Ignore - errors logged internally, don't crash watcher callback
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+5
-5
@@ -61,7 +61,7 @@ export class TeamWatcher extends EventEmitter {
|
||||
persistent: false,
|
||||
});
|
||||
|
||||
const teamsHandler = () => this.pollAsync().catch(() => {});
|
||||
const teamsHandler = () => this.pollAsync().catch(() => {}); // Ignore - poll errors are non-fatal, next poll will retry
|
||||
this.teamsWatcher.on('add', teamsHandler);
|
||||
this.teamsWatcher.on('change', teamsHandler);
|
||||
this.teamsWatcher.on('unlink', teamsHandler);
|
||||
@@ -82,8 +82,8 @@ export class TeamWatcher extends EventEmitter {
|
||||
persistent: false,
|
||||
});
|
||||
|
||||
this.tasksWatcher.on('add', () => this.pollTasks().catch(() => {}));
|
||||
this.tasksWatcher.on('change', () => this.pollTasks().catch(() => {}));
|
||||
this.tasksWatcher.on('add', () => this.pollTasks().catch(() => {})); // Ignore - poll errors are non-fatal, next poll will retry
|
||||
this.tasksWatcher.on('change', () => this.pollTasks().catch(() => {})); // Ignore - poll errors are non-fatal, next poll will retry
|
||||
this.tasksWatcher.on('error', (err) => {
|
||||
console.warn('[TeamWatcher] chokidar tasks watcher error:', err);
|
||||
});
|
||||
@@ -95,11 +95,11 @@ export class TeamWatcher extends EventEmitter {
|
||||
stop(): void {
|
||||
// Close chokidar watchers
|
||||
if (this.teamsWatcher) {
|
||||
this.teamsWatcher.close().catch(() => {});
|
||||
this.teamsWatcher.close().catch(() => {}); // Ignore - watcher cleanup is best-effort during shutdown
|
||||
this.teamsWatcher = null;
|
||||
}
|
||||
if (this.tasksWatcher) {
|
||||
this.tasksWatcher.close().catch(() => {});
|
||||
this.tasksWatcher.close().catch(() => {}); // Ignore - watcher cleanup is best-effort during shutdown
|
||||
this.tasksWatcher = null;
|
||||
}
|
||||
if (this.pollTimer) {
|
||||
|
||||
+1
-7
@@ -40,8 +40,7 @@ import {
|
||||
type SessionMode,
|
||||
type OpenCodeConfig,
|
||||
} from './types.js';
|
||||
import { wrapWithNice } from './utils/nice-wrapper.js';
|
||||
import { SAFE_PATH_PATTERN } from './utils/regex-patterns.js';
|
||||
import { wrapWithNice, SAFE_PATH_PATTERN, findClaudeDir, resolveOpenCodeDir } from './utils/index.js';
|
||||
import type {
|
||||
TerminalMultiplexer,
|
||||
MuxSession,
|
||||
@@ -50,11 +49,6 @@ import type {
|
||||
RespawnPaneOptions,
|
||||
} from './mux-interface.js';
|
||||
|
||||
// Claude CLI PATH resolution — shared utility
|
||||
import { findClaudeDir } from './utils/claude-cli-resolver.js';
|
||||
// OpenCode CLI PATH resolution
|
||||
import { resolveOpenCodeDir } from './utils/opencode-cli-resolver.js';
|
||||
|
||||
// ============================================================================
|
||||
// Timing Constants
|
||||
// ============================================================================
|
||||
|
||||
@@ -6494,7 +6494,7 @@ class CodemanApp {
|
||||
document.getElementById('appSettingsShowMonitor').checked = settings.showMonitor ?? defaults.showMonitor ?? true;
|
||||
document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? defaults.showProjectInsights ?? false;
|
||||
document.getElementById('appSettingsShowFileBrowser').checked = settings.showFileBrowser ?? defaults.showFileBrowser ?? false;
|
||||
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? defaults.showSubagents ?? true;
|
||||
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? defaults.showSubagents ?? false;
|
||||
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? defaults.subagentTrackingEnabled ?? true;
|
||||
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? defaults.subagentActiveTabOnly ?? true;
|
||||
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? defaults.imageWatcherEnabled ?? false;
|
||||
@@ -7662,7 +7662,7 @@ class CodemanApp {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const defaults = this.getDefaultSettings();
|
||||
const showMonitor = settings.showMonitor ?? defaults.showMonitor ?? true;
|
||||
const showSubagents = settings.showSubagents ?? defaults.showSubagents ?? true;
|
||||
const showSubagents = settings.showSubagents ?? defaults.showSubagents ?? false;
|
||||
const showFileBrowser = settings.showFileBrowser ?? defaults.showFileBrowser ?? false;
|
||||
|
||||
const monitorPanel = document.getElementById('monitorPanel');
|
||||
|
||||
@@ -505,7 +505,7 @@ export function registerRalphRoutes(
|
||||
settings.lastUsedCase = caseName;
|
||||
const dir = dirname(SETTINGS_PATH);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
fs.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2)).catch(() => {});
|
||||
fs.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2)).catch(() => {}); // Ignore - persisting lastUsedCase is non-critical
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { SAFE_PATH_PATTERN } from '../utils/regex-patterns.js';
|
||||
import { SAFE_PATH_PATTERN } from '../utils/index.js';
|
||||
|
||||
// ========== Path Validation ==========
|
||||
|
||||
|
||||
+167
-138
@@ -100,11 +100,7 @@ function createAssistantTextEntry(text: string, timestamp?: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
function createToolUseEntry(
|
||||
toolName: string,
|
||||
input: Record<string, unknown>,
|
||||
timestamp?: string
|
||||
): string {
|
||||
function createToolUseEntry(toolName: string, input: Record<string, unknown>, timestamp?: string): string {
|
||||
return JSON.stringify({
|
||||
type: 'assistant',
|
||||
timestamp: timestamp || new Date().toISOString(),
|
||||
@@ -134,6 +130,13 @@ function createToolResultEntry(content: string, timestamp?: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a mock readline interface with .close() method */
|
||||
function createMockRl() {
|
||||
const rl = new EventEmitter() as EventEmitter & { close: ReturnType<typeof vi.fn> };
|
||||
rl.close = vi.fn();
|
||||
return rl;
|
||||
}
|
||||
|
||||
describe('SubagentWatcher', () => {
|
||||
let watcher: SubagentWatcher;
|
||||
let mockExistsSync: Mock;
|
||||
@@ -166,12 +169,12 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
// Default mocks - no projects exist
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
mockStatSync.mockReturnValue({
|
||||
mockStatSync.mockImplementation(() => ({
|
||||
isDirectory: () => true,
|
||||
birthtime: new Date(),
|
||||
mtime: new Date(),
|
||||
size: 0,
|
||||
});
|
||||
}));
|
||||
mockReaddirSync.mockReturnValue([]);
|
||||
mockReadFileSync.mockReturnValue('');
|
||||
mockWatch.mockReturnValue({ close: vi.fn(), on: vi.fn(), off: vi.fn() });
|
||||
@@ -239,9 +242,9 @@ describe('SubagentWatcher', () => {
|
||||
const lines = [validEntry];
|
||||
|
||||
// Setup mock readline interface
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
// Setup file discovery
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
@@ -281,16 +284,11 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
it('should skip malformed JSON lines', async () => {
|
||||
const validEntry = createUserEntry('Valid entry');
|
||||
const malformedLines = [
|
||||
'not json at all',
|
||||
'{"incomplete": true',
|
||||
validEntry,
|
||||
'}{bad json}{',
|
||||
];
|
||||
const malformedLines = ['not json at all', '{"incomplete": true', validEntry, '}{bad json}{'];
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -330,9 +328,9 @@ describe('SubagentWatcher', () => {
|
||||
// Simulate partial write where line is incomplete
|
||||
const partialContent = '{"type": "user", "timestamp": "2024-01-01T00:00:00Z"';
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -369,9 +367,9 @@ describe('SubagentWatcher', () => {
|
||||
const validEntry = createUserEntry('Valid');
|
||||
const contentWithEmptyLines = ['', validEntry, ' ', '', validEntry].join('\n');
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -408,9 +406,9 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
describe('Status Lifecycle', () => {
|
||||
it('should start agents as active', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -442,9 +440,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should transition to idle after IDLE_TIMEOUT_MS (30s)', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -453,12 +451,12 @@ describe('SubagentWatcher', () => {
|
||||
if (path.includes('project1')) return ['session1'];
|
||||
return ['project1'];
|
||||
});
|
||||
mockStatSync.mockReturnValue({
|
||||
mockStatSync.mockImplementation(() => ({
|
||||
isDirectory: () => true,
|
||||
birthtime: new Date(),
|
||||
mtime: new Date(),
|
||||
size: 100,
|
||||
});
|
||||
}));
|
||||
mockReadFileSync.mockReturnValue(createUserEntry('Test subagent task'));
|
||||
|
||||
watcher.start();
|
||||
@@ -481,9 +479,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should reset to active on new activity', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
const mockWatcher = { close: vi.fn(), on: vi.fn(), off: vi.fn() };
|
||||
mockWatch.mockReturnValue(mockWatcher);
|
||||
@@ -495,12 +493,12 @@ describe('SubagentWatcher', () => {
|
||||
if (path.includes('project1')) return ['session1'];
|
||||
return ['project1'];
|
||||
});
|
||||
mockStatSync.mockReturnValue({
|
||||
mockStatSync.mockImplementation(() => ({
|
||||
isDirectory: () => true,
|
||||
birthtime: new Date(),
|
||||
mtime: new Date(),
|
||||
size: 100,
|
||||
});
|
||||
}));
|
||||
mockReadFileSync.mockReturnValue(createUserEntry('Test subagent task'));
|
||||
|
||||
watcher.start();
|
||||
@@ -514,19 +512,20 @@ describe('SubagentWatcher', () => {
|
||||
expect(watcher.getSubagents()[0].status).toBe('idle');
|
||||
|
||||
// Simulate file change event - get the callback from mockWatch
|
||||
const watchCallback = mockWatch.mock.calls.find(
|
||||
(call: unknown[]) => typeof call[1] === 'function'
|
||||
)?.[1];
|
||||
const watchCallback = mockWatch.mock.calls.find((call: unknown[]) => typeof call[1] === 'function')?.[1];
|
||||
|
||||
if (watchCallback) {
|
||||
// Need to reset the readline mock for the new read
|
||||
const newMockRl = new EventEmitter();
|
||||
const newMockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(newMockRl);
|
||||
|
||||
// Trigger file change
|
||||
watchCallback('change', 'agent-reactive.jsonl');
|
||||
|
||||
// Complete the new readline
|
||||
// Advance past the fileDeb debounce (100ms) so handleFileChange runs
|
||||
await vi.advanceTimersByTimeAsync(150);
|
||||
|
||||
// Complete the new readline (tailFile)
|
||||
newMockRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
@@ -536,9 +535,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should transition to completed when file becomes stale', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -588,9 +587,10 @@ describe('SubagentWatcher', () => {
|
||||
it('should extract tool_use entries', async () => {
|
||||
const toolEntry = createToolUseEntry('WebSearch', { query: 'test query' });
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
const descRl = createMockRl();
|
||||
const tailRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValueOnce(descRl).mockReturnValue(tailRl);
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -612,9 +612,14 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', toolEntry);
|
||||
mockRl.emit('close');
|
||||
|
||||
// Resolve extractDescriptionFromFile
|
||||
descRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Now tailFile is set up — emit entries on tailRl
|
||||
tailRl.emit('line', toolEntry);
|
||||
tailRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(toolCallHandler).toHaveBeenCalled();
|
||||
@@ -626,9 +631,10 @@ describe('SubagentWatcher', () => {
|
||||
it('should extract text messages', async () => {
|
||||
const textEntry = createAssistantTextEntry('This is the assistant response');
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
const descRl = createMockRl();
|
||||
const tailRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValueOnce(descRl).mockReturnValue(tailRl);
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -650,9 +656,14 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', textEntry);
|
||||
mockRl.emit('close');
|
||||
|
||||
// Resolve extractDescriptionFromFile
|
||||
descRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Now tailFile is set up — emit entries on tailRl
|
||||
tailRl.emit('line', textEntry);
|
||||
tailRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(messageHandler).toHaveBeenCalled();
|
||||
@@ -682,9 +693,9 @@ describe('SubagentWatcher', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -720,9 +731,10 @@ describe('SubagentWatcher', () => {
|
||||
const longText = 'x'.repeat(1000);
|
||||
const textEntry = createAssistantTextEntry(longText);
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
const descRl = createMockRl();
|
||||
const tailRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValueOnce(descRl).mockReturnValue(tailRl);
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -744,9 +756,14 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', textEntry);
|
||||
mockRl.emit('close');
|
||||
|
||||
// Resolve extractDescriptionFromFile
|
||||
descRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Now tailFile is set up — emit entries on tailRl
|
||||
tailRl.emit('line', textEntry);
|
||||
tailRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(messageHandler).toHaveBeenCalled();
|
||||
@@ -757,9 +774,10 @@ describe('SubagentWatcher', () => {
|
||||
it('should extract progress events', async () => {
|
||||
const progressEntry = createProgressEntry('query_update', { query: 'searching for files' });
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
const descRl = createMockRl();
|
||||
const tailRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValueOnce(descRl).mockReturnValue(tailRl);
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -781,9 +799,14 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', progressEntry);
|
||||
mockRl.emit('close');
|
||||
|
||||
// Resolve extractDescriptionFromFile
|
||||
descRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Now tailFile is set up — emit entries on tailRl
|
||||
tailRl.emit('line', progressEntry);
|
||||
tailRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(progressHandler).toHaveBeenCalled();
|
||||
@@ -795,9 +818,9 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
describe('Memory Management', () => {
|
||||
it('should track agents in agentInfo map', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -832,9 +855,10 @@ describe('SubagentWatcher', () => {
|
||||
const toolEntry1 = createToolUseEntry('Read', { file_path: '/test1.ts' });
|
||||
const toolEntry2 = createToolUseEntry('Write', { file_path: '/test2.ts' });
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
const descRl = createMockRl();
|
||||
const tailRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValueOnce(descRl).mockReturnValue(tailRl);
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -853,10 +877,15 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', toolEntry1);
|
||||
mockRl.emit('line', toolEntry2);
|
||||
mockRl.emit('close');
|
||||
|
||||
// Resolve extractDescriptionFromFile
|
||||
descRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Now tailFile is set up — emit entries on tailRl
|
||||
tailRl.emit('line', toolEntry1);
|
||||
tailRl.emit('line', toolEntry2);
|
||||
tailRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
const agent = watcher.getSubagent('toolcount');
|
||||
@@ -871,9 +900,10 @@ describe('SubagentWatcher', () => {
|
||||
createToolUseEntry('Read', { file_path: '/test.ts' }),
|
||||
];
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
const descRl = createMockRl();
|
||||
const tailRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValueOnce(descRl).mockReturnValue(tailRl);
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -892,11 +922,16 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
for (const entry of entries) {
|
||||
mockRl.emit('line', entry);
|
||||
}
|
||||
mockRl.emit('close');
|
||||
|
||||
// Resolve extractDescriptionFromFile
|
||||
descRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Now tailFile is set up — emit entries on tailRl
|
||||
for (const entry of entries) {
|
||||
tailRl.emit('line', entry);
|
||||
}
|
||||
tailRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
const agent = watcher.getSubagent('entrycount');
|
||||
@@ -907,9 +942,9 @@ describe('SubagentWatcher', () => {
|
||||
// Note: Current implementation has no cleanup/eviction policy
|
||||
// This documents the behavior as a known issue
|
||||
it('should retain all agents indefinitely (no cleanup policy)', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
@@ -949,9 +984,9 @@ describe('SubagentWatcher', () => {
|
||||
createToolUseEntry('Read', { file_path: '/test.ts' }),
|
||||
];
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -979,13 +1014,11 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should limit transcript entries when limit is specified', async () => {
|
||||
const entries = Array.from({ length: 10 }, (_, i) =>
|
||||
createUserEntry(`Message ${i}`)
|
||||
);
|
||||
const entries = Array.from({ length: 10 }, (_, i) => createUserEntry(`Message ${i}`));
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1013,9 +1046,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should return empty array for unknown agent', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockReturnValue([]);
|
||||
@@ -1037,9 +1070,7 @@ describe('SubagentWatcher', () => {
|
||||
sessionId: 'sess1',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'WebSearch', input: { query: 'test query' } },
|
||||
],
|
||||
content: [{ type: 'tool_use', name: 'WebSearch', input: { query: 'test query' } }],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1092,9 +1123,9 @@ describe('SubagentWatcher', () => {
|
||||
it('should extract description from first user message', async () => {
|
||||
const userEntry = createUserEntry('Create comprehensive tests for the module');
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1116,6 +1147,7 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', userEntry);
|
||||
mockRl.emit('close');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
@@ -1134,9 +1166,9 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
const userEntry = createUserEntry(longPrompt);
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1158,6 +1190,7 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', userEntry);
|
||||
mockRl.emit('close');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
@@ -1169,9 +1202,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should emit subagent:updated when description is extracted from processEntry', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1234,7 +1267,7 @@ describe('SubagentWatcher', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
|
||||
// createReadStream now used for parent transcript reading (stream tail)
|
||||
@@ -1287,9 +1320,9 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
describe('getRecentSubagents', () => {
|
||||
it('should return only recent subagents', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1317,9 +1350,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should sort by lastActivityAt descending', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1354,9 +1387,9 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
describe('getSubagentsForSession', () => {
|
||||
it('should filter subagents by working directory', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1392,9 +1425,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should return false for already completed agent', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1428,9 +1461,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should emit completed event when killing active agent', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1485,9 +1518,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should handle readline errors gracefully', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1524,9 +1557,9 @@ describe('SubagentWatcher', () => {
|
||||
// Only directory watchers are created (no per-file watchers)
|
||||
mockWatch.mockReturnValue(mockDirWatcher);
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1556,9 +1589,9 @@ describe('SubagentWatcher', () => {
|
||||
});
|
||||
|
||||
it('should clear idle timers on stop', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1596,9 +1629,9 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
describe('Project Hash Conversion', () => {
|
||||
it('should convert working directory to project hash format', async () => {
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
@@ -1640,9 +1673,7 @@ describe('SubagentWatcher', () => {
|
||||
sessionId: 'sess1',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'WebSearch', input: { query: 'nodejs best practices' } },
|
||||
],
|
||||
content: [{ type: 'tool_use', name: 'WebSearch', input: { query: 'nodejs best practices' } }],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1661,9 +1692,7 @@ describe('SubagentWatcher', () => {
|
||||
sessionId: 'sess1',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'Read', input: { file_path: '/src/index.ts' } },
|
||||
],
|
||||
content: [{ type: 'tool_use', name: 'Read', input: { file_path: '/src/index.ts' } }],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1682,9 +1711,7 @@ describe('SubagentWatcher', () => {
|
||||
sessionId: 'sess1',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'Bash', input: { command: 'npm test' } },
|
||||
],
|
||||
content: [{ type: 'tool_use', name: 'Bash', input: { command: 'npm test' } }],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1704,9 +1731,7 @@ describe('SubagentWatcher', () => {
|
||||
sessionId: 'sess1',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'tool_use', name: 'Bash', input: { command: longCommand } },
|
||||
],
|
||||
content: [{ type: 'tool_use', name: 'Bash', input: { command: longCommand } }],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1753,9 +1778,10 @@ describe('SubagentWatcher', () => {
|
||||
it('should emit user messages under 500 chars', async () => {
|
||||
const userEntry = createUserEntry('Short user message');
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
const descRl = createMockRl();
|
||||
const tailRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValueOnce(descRl).mockReturnValue(tailRl);
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1777,9 +1803,14 @@ describe('SubagentWatcher', () => {
|
||||
|
||||
watcher.start();
|
||||
await flushAsyncScan();
|
||||
mockRl.emit('line', userEntry);
|
||||
mockRl.emit('close');
|
||||
|
||||
// Resolve extractDescriptionFromFile
|
||||
descRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Now tailFile is set up — emit entries on tailRl
|
||||
tailRl.emit('line', userEntry);
|
||||
tailRl.emit('close');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(messageHandler).toHaveBeenCalled();
|
||||
@@ -1790,9 +1821,9 @@ describe('SubagentWatcher', () => {
|
||||
it('should not emit long user messages (over 500 chars)', async () => {
|
||||
const longUserEntry = createUserEntry('x'.repeat(600));
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
@@ -1820,9 +1851,7 @@ describe('SubagentWatcher', () => {
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Long user messages are filtered out
|
||||
const userMessages = messageHandler.mock.calls.filter(
|
||||
(call) => (call[0] as SubagentMessage).role === 'user'
|
||||
);
|
||||
const userMessages = messageHandler.mock.calls.filter((call) => (call[0] as SubagentMessage).role === 'user');
|
||||
expect(userMessages.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1831,9 +1860,9 @@ describe('SubagentWatcher', () => {
|
||||
it('should not emit message for empty text content', async () => {
|
||||
const emptyTextEntry = createAssistantTextEntry(' ');
|
||||
|
||||
const mockRl = new EventEmitter();
|
||||
const mockRl = createMockRl();
|
||||
mockCreateInterface.mockReturnValue(mockRl);
|
||||
mockCreateReadStream.mockReturnValue({});
|
||||
mockCreateReadStream.mockReturnValue({ destroy: vi.fn() });
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReaddirSync.mockImplementation((path: string) => {
|
||||
|
||||
Reference in New Issue
Block a user