feat: add diagnostics tool for live command output monitoring
- Implemented diagnostics functionality to start, stop, and monitor background check commands. - Created a DiagnosticsPanel for real-time output display in the UI. - Added support for auto-detecting default diagnostics commands based on project configuration. - Introduced run_checks tool to execute project verification commands and report results. - Enhanced tools-extra with functions to parse AGENTS.md and package.json for check commands. - Added diff review functionality to visualize changes made in the last turn. - Implemented tests for diagnostics and run_checks functionalities to ensure reliability.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { diagStart, diagStop, diagStatus, bootDiagnostics, defaultDiagnosticsCommand, shutdownDiagnostics } from '../src/diagnostics';
|
||||
|
||||
// Diagnostics spawns real processes, so each test gets a fresh state (the module
|
||||
// is a singleton — one diagnostics command at a time).
|
||||
describe('diagnostics module', () => {
|
||||
afterEach(() => {
|
||||
shutdownDiagnostics();
|
||||
});
|
||||
|
||||
it('starts, shows running status, and stops a command', async () => {
|
||||
const started = diagStart('node -e "setInterval(()=>{}, 1000)"');
|
||||
expect(started.started).toBe(true);
|
||||
|
||||
const status = diagStatus();
|
||||
expect(status.running).toBe(true);
|
||||
expect(status.command).toContain('setInterval');
|
||||
|
||||
const stopped = diagStop();
|
||||
expect(stopped.stopped).toBe(true);
|
||||
expect(diagStatus().running).toBe(false);
|
||||
});
|
||||
|
||||
it('reports exit code once the command finishes', async () => {
|
||||
diagStart('node -e "process.exit(3)"');
|
||||
// Wait for the process to actually exit.
|
||||
await Bun.sleep(300);
|
||||
const status = diagStatus();
|
||||
expect(status.running).toBe(false);
|
||||
expect(status.exit).toBe(3);
|
||||
});
|
||||
|
||||
it('captures output into the tail', async () => {
|
||||
diagStart('node -e "console.log(\'hello-diag\')"');
|
||||
await Bun.sleep(300);
|
||||
const status = diagStatus();
|
||||
expect(status.tail).toContain('hello-diag');
|
||||
expect(status.exit).toBe(0);
|
||||
});
|
||||
|
||||
it('diagStop with nothing running is a no-op', () => {
|
||||
expect(diagStop().stopped).toBe(false);
|
||||
expect(diagStatus().running).toBe(false);
|
||||
});
|
||||
|
||||
it('starting replaces a running command and kills the old one', async () => {
|
||||
const oldHandle = diagStart('node -e "setInterval(()=>{}, 1000)"');
|
||||
expect(oldHandle.started).toBe(true);
|
||||
const second = diagStart('node -e "console.log(\'second\')"');
|
||||
expect(second.started).toBe(true);
|
||||
const status = diagStatus();
|
||||
expect(status.command).toContain('second');
|
||||
// old process is gone; status.command reflects the newest start
|
||||
shutdownDiagnostics();
|
||||
});
|
||||
|
||||
it('bootDiagnostics ignores empty config and starts a real one', async () => {
|
||||
bootDiagnostics(undefined);
|
||||
expect(diagStatus().running).toBe(false);
|
||||
bootDiagnostics('node -e "setInterval(()=>{}, 1000)"');
|
||||
expect(diagStatus().running).toBe(true);
|
||||
});
|
||||
|
||||
it('bootDiagnostics never throws on a bad command — keeps boot clean', async () => {
|
||||
expect(() => bootDiagnostics('')).not.toThrow();
|
||||
// A command that cannot spawn (bad binary) still must not throw synchronously.
|
||||
expect(() => bootDiagnostics('/nonexistent/binary')).not.toThrow();
|
||||
await Bun.sleep(50);
|
||||
shutdownDiagnostics();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultDiagnosticsCommand — auto-detection', () => {
|
||||
const tmp = `${Bun.env['TMPDIR'] ?? '/tmp'}/diag-default-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
||||
let savedCwd: string;
|
||||
const { mkdirSync, rmSync, writeFileSync } = require('node:fs') as typeof import('node:fs');
|
||||
|
||||
beforeEach(() => {
|
||||
savedCwd = process.cwd();
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
mkdirSync(tmp, { recursive: true });
|
||||
process.chdir(tmp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(savedCwd);
|
||||
diagStop();
|
||||
});
|
||||
|
||||
it('picks tsc --watch for a bun+ts project', async () => {
|
||||
writeFileSync('bun.lock', '');
|
||||
writeFileSync('package.json', '{}');
|
||||
writeFileSync('tsconfig.json', '{}');
|
||||
expect(await defaultDiagnosticsCommand(process.cwd())).toBe('bun run typecheck --watch');
|
||||
});
|
||||
|
||||
it('falls back to bun test --watch without tsconfig', async () => {
|
||||
writeFileSync('bun.lock', '');
|
||||
writeFileSync('package.json', '{}');
|
||||
expect(await defaultDiagnosticsCommand(process.cwd())).toBe('bun test --watch');
|
||||
});
|
||||
|
||||
it('returns undefined for an unknown project', async () => {
|
||||
expect(await defaultDiagnosticsCommand(process.cwd())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user