Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ export default function App() {
}
setSelectedSessionId(message.session.id)
addRecentPath(message.session.projectPath)

// Auto-add to filter if filters are active and project isn't included
const { projectFilters, setProjectFilters } = useSettingsStore.getState()
if (projectFilters.length > 0 && !projectFilters.includes(message.session.projectPath)) {
setProjectFilters([...projectFilters, message.session.projectPath])
}
}
if (message.type === 'session-removed') {
// setSessions handles marking removed sessions as exiting for animation
Expand Down
2 changes: 2 additions & 0 deletions src/server/__tests__/agentSessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const baseRecord: AgentSessionRecord = {
currentWindow: 'agentboard:1',
isPinned: false,
lastResumeError: null,
lastKnownLogSize: null,
isCodexExec: false,
}

describe('agentSessions', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/server/__tests__/db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ function makeSession(overrides: Partial<{
currentWindow: string | null
isPinned: boolean
lastResumeError: string | null
lastKnownLogSize: number | null
isCodexExec: boolean
}> = {}) {
return {
sessionId: 'session-abc',
Expand All @@ -33,6 +35,8 @@ function makeSession(overrides: Partial<{
currentWindow: 'agentboard:1',
isPinned: false,
lastResumeError: null,
lastKnownLogSize: null,
isCodexExec: false,
...overrides,
}
}
Expand Down
38 changes: 24 additions & 14 deletions src/server/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
Expand All @@ -16,7 +16,8 @@ const originalDbPath = process.env.AGENTBOARD_DB_PATH
let tempDbPath: string | null = null

const serveCalls: Array<{ port: number }> = []
const importIndex = (suffix: string) => import(`../index?test=${suffix}`)
let importCounter = 0
let spawnSyncImpl: typeof Bun.spawnSync

describe('server entrypoint', () => {
beforeAll(() => {
Expand All @@ -27,32 +28,45 @@ describe('server entrypoint', () => {
process.env.AGENTBOARD_DB_PATH = tempDbPath
})

test('starts server without side effects', async () => {
beforeEach(() => {
serveCalls.length = 0
process.env.AGENTBOARD_LOG_MATCH_WORKER = 'false'
bunAny.spawnSync = () =>

// Default mock: port not in use
spawnSyncImpl = () =>
({
exitCode: 0,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
}) as ReturnType<typeof Bun.spawnSync>

bunAny.spawnSync = ((...args: Parameters<typeof Bun.spawnSync>) =>
spawnSyncImpl(...args)) as typeof Bun.spawnSync
bunAny.serve = ((options: { port?: number }) => {
serveCalls.push({ port: options.port ?? 0 })
return {} as ReturnType<typeof Bun.serve>
}) as unknown as typeof Bun.serve
globalThis.setInterval = (() => 0) as unknown as typeof globalThis.setInterval
})

await importIndex('no-side-effects')
afterEach(() => {
bunAny.serve = originalServe
bunAny.spawnSync = originalSpawnSync
globalThis.setInterval = originalSetInterval
})

test('starts server without side effects', async () => {
importCounter += 1
await import(`../index?test=no-side-effects-${importCounter}`)

const expectedPort = Number(process.env.PORT) || 4040
expect(serveCalls).toHaveLength(1)
expect(serveCalls[0]?.port).toBe(expectedPort)
})

test('starts server when lsof is unavailable', async () => {
serveCalls.length = 0
process.env.AGENTBOARD_LOG_MATCH_WORKER = 'false'
bunAny.spawnSync = ((...args: Parameters<typeof Bun.spawnSync>) => {
// Override spawnSyncImpl to throw for lsof
spawnSyncImpl = ((...args: Parameters<typeof Bun.spawnSync>) => {
const command = Array.isArray(args[0]) ? args[0][0] : ''
if (command === 'lsof') {
throw new Error('missing lsof')
Expand All @@ -63,13 +77,9 @@ describe('server entrypoint', () => {
stderr: Buffer.from(''),
} as ReturnType<typeof Bun.spawnSync>
}) as typeof Bun.spawnSync
bunAny.serve = ((options: { port?: number }) => {
serveCalls.push({ port: options.port ?? 0 })
return {} as ReturnType<typeof Bun.serve>
}) as unknown as typeof Bun.serve
globalThis.setInterval = (() => 0) as unknown as typeof globalThis.setInterval

await importIndex('missing-lsof')
importCounter += 1
await import(`../index?test=missing-lsof-${importCounter}`)

const expectedPort = Number(process.env.PORT) || 4040
expect(serveCalls).toHaveLength(1)
Expand Down
87 changes: 52 additions & 35 deletions src/server/__tests__/indexPortCheck.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, mock } from 'bun:test'
import fs from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'

// Mock logger to suppress expected error output during port conflict tests
mock.module('../logger', () => ({
logger: {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
},
flushLogger: () => {},
closeLogger: () => {},
}))

const bunAny = Bun as typeof Bun & {
serve: typeof Bun.serve
spawnSync: typeof Bun.spawnSync
Expand All @@ -18,6 +30,7 @@ const originalProcessExit = processAny.exit
const originalSetInterval = globalThis.setInterval
const originalDbPath = process.env.AGENTBOARD_DB_PATH
let tempDbPath: string | null = null
let importCounter = 0

beforeAll(() => {
const suffix = `port-check-${process.pid}-${Date.now()}-${Math.random()
Expand All @@ -27,6 +40,42 @@ beforeAll(() => {
process.env.AGENTBOARD_DB_PATH = tempDbPath
})

beforeEach(() => {
// Set up mocks that simulate port already in use
bunAny.spawnSync = ((...args: Parameters<typeof Bun.spawnSync>) => {
const command = Array.isArray(args[0]) ? args[0][0] : ''
if (command === 'lsof') {
return {
exitCode: 0,
stdout: Buffer.from('123\n'),
stderr: Buffer.from(''),
} as ReturnType<typeof Bun.spawnSync>
}
if (command === 'ps') {
return {
exitCode: 0,
stdout: Buffer.from('node\n'),
stderr: Buffer.from(''),
} as ReturnType<typeof Bun.spawnSync>
}
return {
exitCode: 0,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
} as ReturnType<typeof Bun.spawnSync>
}) as typeof Bun.spawnSync

bunAny.serve = ((_options: Parameters<typeof Bun.serve>[0]) => {
return {} as ReturnType<typeof Bun.serve>
}) as typeof Bun.serve

globalThis.setInterval = (() => 0) as unknown as typeof globalThis.setInterval

processAny.exit = ((code?: number) => {
throw new Error(`exit:${code ?? 0}`)
}) as typeof processAny.exit
})

afterEach(() => {
bunAny.serve = originalServe
bunAny.spawnSync = originalSpawnSync
Expand All @@ -47,40 +96,8 @@ afterAll(() => {

describe('port availability', () => {
test('exits when the configured port is already in use', async () => {
const suffix = `port-check-${Date.now()}`

bunAny.spawnSync = ((...args: Parameters<typeof Bun.spawnSync>) => {
const command = Array.isArray(args[0]) ? args[0][0] : ''
if (command === 'lsof') {
return {
exitCode: 0,
stdout: Buffer.from('123\n'),
stderr: Buffer.from(''),
} as ReturnType<typeof Bun.spawnSync>
}
if (command === 'ps') {
return {
exitCode: 0,
stdout: Buffer.from('node\n'),
stderr: Buffer.from(''),
} as ReturnType<typeof Bun.spawnSync>
}
return {
exitCode: 0,
stdout: Buffer.from(''),
stderr: Buffer.from(''),
} as ReturnType<typeof Bun.spawnSync>
}) as typeof Bun.spawnSync

bunAny.serve = ((_options: Parameters<typeof Bun.serve>[0]) => {
return {} as ReturnType<typeof Bun.serve>
}) as typeof Bun.serve

globalThis.setInterval = (() => 0) as unknown as typeof globalThis.setInterval

processAny.exit = ((code?: number) => {
throw new Error(`exit:${code ?? 0}`)
}) as typeof processAny.exit
importCounter += 1
const suffix = `port-check-${importCounter}`

let thrown: Error | null = null
try {
Expand Down
2 changes: 2 additions & 0 deletions src/server/__tests__/isolated/indexHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ function makeRecord(overrides: Partial<AgentSessionRecord> = {}): AgentSessionRe
currentWindow: null,
isPinned: false,
lastResumeError: null,
lastKnownLogSize: null,
isCodexExec: false,
...overrides,
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/server/__tests__/logMatchGate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function makeEntry(
logPath: 'log-1',
mtime: 1_700_000_000_000,
birthtime: 1_699_000_000_000,
size: 1000,
sessionId: 'session-1',
projectPath: '/proj',
agentType: 'claude',
Expand Down Expand Up @@ -49,18 +50,21 @@ describe('logMatchGate', () => {
logFilePath: 'log-stale',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different from entry.size (1000), so needs match
},
{
sessionId: 'session-active',
logFilePath: 'log-active',
currentWindow: '2',
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 1000,
},
{
sessionId: 'session-invalid',
logFilePath: 'log-invalid',
currentWindow: null,
lastActivityAt: 'not-a-date',
lastKnownLogSize: 500, // Different from entry.size (1000), so needs match
},
]

Expand Down Expand Up @@ -133,20 +137,22 @@ describe('logMatchGate', () => {
logFilePath: 'log-orphan-exec',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different size to trigger re-match check
},
{
sessionId: 'session-orphan-normal',
logFilePath: 'log-orphan-normal',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different size to trigger re-match
},
]

const needs = getEntriesNeedingMatch(entries, sessions, {
minTokens: 0,
skipMatchingPatterns: ['<codex-exec>'],
})
// Only the normal orphan should need matching
// Only the normal orphan should need matching (codex-exec is always skipped)
expect(needs.map((e) => e.sessionId)).toEqual(['session-orphan-normal'])
})

Expand All @@ -171,12 +177,14 @@ describe('logMatchGate', () => {
logFilePath: 'log-orphan-tmp',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different size
},
{
sessionId: 'session-orphan-normal',
logFilePath: 'log-orphan-normal',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different size to trigger re-match
},
]

Expand Down Expand Up @@ -204,10 +212,11 @@ describe('logMatchGate', () => {
logFilePath: 'log-orphan-exec',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different size
},
]

// Should work with different case variations
// Should work with different case variations - codex exec is always skipped
for (const pattern of ['<codex-exec>', '<CODEX-EXEC>', '<Codex-Exec>']) {
const needs = getEntriesNeedingMatch(entries, sessions, {
minTokens: 0,
Expand All @@ -229,12 +238,14 @@ describe('logMatchGate', () => {
logFilePath: 'log-tmp',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different size
},
{
sessionId: 'session-normal',
logFilePath: 'log-normal',
currentWindow: null,
lastActivityAt: '2025-01-01T00:00:00Z',
lastKnownLogSize: 500, // Different size to trigger re-match
},
]

Expand Down
2 changes: 1 addition & 1 deletion src/server/__tests__/logMatchWorkerClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe('LogMatchWorkerClient', () => {
await waitForMessage(worker)
client.dispose()

await expect(promise).rejects.toThrow('Log match worker disposed')
await expect(promise).rejects.toThrow('Log match worker is disposed')
expect(worker.terminated).toBe(true)
})

Expand Down
Loading
Loading