test: Wired scanner and session manager into Daemon with scanProjects()…
- "packages/daemon/src/daemon.ts" - "packages/daemon/src/index.ts" - "packages/daemon/src/daemon.test.ts" GSD-Task: S02/T03
This commit is contained in:
parent
5910c6523e
commit
7732558d04
3 changed files with 178 additions and 4 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, afterEach, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
|
@ -10,6 +10,7 @@ import { dirname } from 'node:path';
|
|||
import { resolveConfigPath, loadConfig, validateConfig } from './config.js';
|
||||
import { Logger } from './logger.js';
|
||||
import { Daemon } from './daemon.js';
|
||||
import { SessionManager } from './session-manager.js';
|
||||
import type { DaemonConfig, LogEntry } from './types.js';
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
|
@ -521,3 +522,139 @@ log:
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- Daemon + SessionManager integration ----------
|
||||
|
||||
describe('Daemon integration', () => {
|
||||
it('getSessionManager() returns SessionManager after start()', async () => {
|
||||
const dir = tmpDir();
|
||||
cleanupDirs.push(dir);
|
||||
const logPath = join(dir, 'daemon-sm.log');
|
||||
|
||||
const config: DaemonConfig = {
|
||||
discord: undefined,
|
||||
projects: { scan_roots: [] },
|
||||
log: { file: logPath, level: 'info', max_size_mb: 50 },
|
||||
};
|
||||
|
||||
const logger = new Logger({ filePath: logPath, level: 'info' });
|
||||
const daemon = new Daemon(config, logger);
|
||||
|
||||
await daemon.start();
|
||||
|
||||
const sm = daemon.getSessionManager();
|
||||
assert.ok(sm instanceof SessionManager);
|
||||
|
||||
// Clean shutdown
|
||||
const origExit = process.exit;
|
||||
// @ts-expect-error — overriding process.exit for test
|
||||
process.exit = () => {};
|
||||
try {
|
||||
await daemon.shutdown();
|
||||
} finally {
|
||||
process.exit = origExit;
|
||||
}
|
||||
});
|
||||
|
||||
it('getSessionManager() throws before start()', async () => {
|
||||
const dir = tmpDir();
|
||||
cleanupDirs.push(dir);
|
||||
const logPath = join(dir, 'daemon-nostart.log');
|
||||
|
||||
const config: DaemonConfig = {
|
||||
discord: undefined,
|
||||
projects: { scan_roots: [] },
|
||||
log: { file: logPath, level: 'info', max_size_mb: 50 },
|
||||
};
|
||||
|
||||
const logger = new Logger({ filePath: logPath, level: 'info' });
|
||||
const daemon = new Daemon(config, logger);
|
||||
|
||||
assert.throws(
|
||||
() => daemon.getSessionManager(),
|
||||
(err: Error) => {
|
||||
assert.ok(err.message.includes('Daemon not started'));
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
// Close logger to prevent async write stream from hitting cleaned-up tmpdir
|
||||
await logger.close();
|
||||
});
|
||||
|
||||
it('scanProjects() delegates to scanForProjects with configured roots', async () => {
|
||||
const dir = tmpDir();
|
||||
cleanupDirs.push(dir);
|
||||
const logPath = join(dir, 'daemon-scan.log');
|
||||
|
||||
// Create a fake project root with a project that has a .git marker
|
||||
const scanRoot = join(dir, 'projects');
|
||||
mkdirSync(scanRoot);
|
||||
const projectDir = join(scanRoot, 'my-project');
|
||||
mkdirSync(projectDir);
|
||||
mkdirSync(join(projectDir, '.git'));
|
||||
|
||||
const config: DaemonConfig = {
|
||||
discord: undefined,
|
||||
projects: { scan_roots: [scanRoot] },
|
||||
log: { file: logPath, level: 'info', max_size_mb: 50 },
|
||||
};
|
||||
|
||||
const logger = new Logger({ filePath: logPath, level: 'info' });
|
||||
const daemon = new Daemon(config, logger);
|
||||
|
||||
await daemon.start();
|
||||
|
||||
const projects = await daemon.scanProjects();
|
||||
assert.ok(projects.length >= 1);
|
||||
const found = projects.find(p => p.name === 'my-project');
|
||||
assert.ok(found);
|
||||
assert.ok(found.markers.includes('git'));
|
||||
|
||||
// Clean shutdown
|
||||
const origExit = process.exit;
|
||||
// @ts-expect-error — overriding process.exit for test
|
||||
process.exit = () => {};
|
||||
try {
|
||||
await daemon.shutdown();
|
||||
} finally {
|
||||
process.exit = origExit;
|
||||
}
|
||||
});
|
||||
|
||||
it('shutdown cleans up sessionManager before closing logger', async () => {
|
||||
const dir = tmpDir();
|
||||
cleanupDirs.push(dir);
|
||||
const logPath = join(dir, 'daemon-cleanup.log');
|
||||
|
||||
const config: DaemonConfig = {
|
||||
discord: undefined,
|
||||
projects: { scan_roots: [] },
|
||||
log: { file: logPath, level: 'info', max_size_mb: 50 },
|
||||
};
|
||||
|
||||
const logger = new Logger({ filePath: logPath, level: 'info' });
|
||||
const daemon = new Daemon(config, logger);
|
||||
|
||||
await daemon.start();
|
||||
|
||||
// Access sessionManager to verify it exists
|
||||
const sm = daemon.getSessionManager();
|
||||
assert.ok(sm);
|
||||
|
||||
// Shutdown — should not throw even though sessionManager has no active sessions
|
||||
const origExit = process.exit;
|
||||
// @ts-expect-error — overriding process.exit for test
|
||||
process.exit = () => {};
|
||||
try {
|
||||
await daemon.shutdown();
|
||||
} finally {
|
||||
process.exit = origExit;
|
||||
}
|
||||
|
||||
// Verify log contains both started and shutting down
|
||||
const content = readFileSync(logPath, 'utf-8');
|
||||
assert.ok(content.includes('daemon started'));
|
||||
assert.ok(content.includes('daemon shutting down'));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { DaemonConfig } from './types.js';
|
||||
import type { DaemonConfig, ProjectInfo } from './types.js';
|
||||
import type { Logger } from './logger.js';
|
||||
import { SessionManager } from './session-manager.js';
|
||||
import { scanForProjects } from './project-scanner.js';
|
||||
|
||||
/**
|
||||
* Core daemon class — ties config + logger together with lifecycle management.
|
||||
|
|
@ -10,6 +12,7 @@ export class Daemon {
|
|||
private keepaliveTimer: ReturnType<typeof setInterval> | undefined;
|
||||
private readonly onSigterm: () => void;
|
||||
private readonly onSigint: () => void;
|
||||
private sessionManager: SessionManager | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly config: DaemonConfig,
|
||||
|
|
@ -21,6 +24,8 @@ export class Daemon {
|
|||
|
||||
/** Start the daemon: log startup info, register signal handlers, start keepalive. */
|
||||
async start(): Promise<void> {
|
||||
this.sessionManager = new SessionManager(this.logger);
|
||||
|
||||
this.logger.info('daemon started', {
|
||||
log_level: this.config.log.level,
|
||||
scan_roots: this.config.projects.scan_roots.length,
|
||||
|
|
@ -35,7 +40,20 @@ export class Daemon {
|
|||
this.keepaliveTimer = setInterval(() => {}, 60_000);
|
||||
}
|
||||
|
||||
/** Idempotent shutdown: log, close logger, exit. */
|
||||
/** Scan configured project roots for project directories. */
|
||||
async scanProjects(): Promise<ProjectInfo[]> {
|
||||
return scanForProjects(this.config.projects.scan_roots);
|
||||
}
|
||||
|
||||
/** Accessor for the session manager (available after start()). */
|
||||
getSessionManager(): SessionManager {
|
||||
if (!this.sessionManager) {
|
||||
throw new Error('Daemon not started — call start() before accessing the session manager');
|
||||
}
|
||||
return this.sessionManager;
|
||||
}
|
||||
|
||||
/** Idempotent shutdown: log, cleanup sessions, close logger, exit. */
|
||||
async shutdown(): Promise<void> {
|
||||
if (this.shuttingDown) return;
|
||||
this.shuttingDown = true;
|
||||
|
|
@ -52,6 +70,11 @@ export class Daemon {
|
|||
this.keepaliveTimer = undefined;
|
||||
}
|
||||
|
||||
// Clean up active sessions before closing logger
|
||||
if (this.sessionManager) {
|
||||
await this.sessionManager.cleanup();
|
||||
}
|
||||
|
||||
await this.logger.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
export type { DaemonConfig, LogLevel, LogEntry } from './types.js';
|
||||
export type {
|
||||
DaemonConfig,
|
||||
LogLevel,
|
||||
LogEntry,
|
||||
SessionStatus,
|
||||
ManagedSession,
|
||||
PendingBlocker,
|
||||
CostAccumulator,
|
||||
ProjectInfo,
|
||||
ProjectMarker,
|
||||
StartSessionOptions,
|
||||
} from './types.js';
|
||||
export { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js';
|
||||
export { resolveConfigPath, loadConfig, validateConfig } from './config.js';
|
||||
export { Logger } from './logger.js';
|
||||
export type { LoggerOptions } from './logger.js';
|
||||
export { Daemon } from './daemon.js';
|
||||
export { scanForProjects } from './project-scanner.js';
|
||||
export { SessionManager } from './session-manager.js';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue