fix: address review report across security, architecture, quality, tests
Implements the fixes from docs/REVIEW_REPORT.md (4-agent parallel review). typecheck clean; 341 tests pass (16 files, +113); build:web ok; coverage thresholds (80%) enforced in vitest.config.ts. Critical: - multi-device approval race: release held approval only when the last client detaches (closing one mirror no longer cancels another's prompt) - unbounded session creation (DoS): Config.maxSessions cap (env MAX_SESSIONS), enforced in manager via the existing M4 exit(-1) path - signal-handler leak: named SIGINT/SIGTERM/uncaughtException refs removed in close() - terminal-session initialInput timer tracked + cleared on dispose - tabs.addEntry null-as-cast type hole removed (build session before entry) Should-fix: - security-headers middleware + Origin/CSRF guard on DELETE /live-sessions[/:id] - history.ts converted to fs/promises (async /sessions handler) - removed dead clientDims map + blur protocol message end-to-end - per-connection WS message rate limit (Config.maxMsgsPerSec) - /sessions behavior kept; documented as accepted LAN risk (TECH_DOC §7) Tests: - new tmux / preview-grid / terminal-session (jsdom) / tabs (jsdom) suites - extended history/config/manager/integration coverage incl. regressions Hygiene: - parsePositiveInt -> parseNonNegativeInt; ALLOWED_ORIGINS scheme validation - log-injection sanitize; isLoopback handles 127.0.0.0/8 + IPv4-mapped - operational constants moved into Config - extracted public/preview-grid.ts (DRY launcher/manage) - doc sweeps: ARCHITECTURE §8 runtime-handle exception, stale comments
This commit is contained in:
@@ -1,6 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseSessionMeta } from '../src/http/history.js'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// ── mock node:fs/promises + node:os ──────────────────────────────────────────
|
||||
// Hoisted by vitest; history.ts binds to these mocks.
|
||||
const mockReaddir = vi.fn()
|
||||
const mockStat = vi.fn()
|
||||
const mockOpen = vi.fn()
|
||||
const mockHomedir = vi.fn(() => '/home/tester')
|
||||
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
default: {
|
||||
readdir: (...a: unknown[]) => mockReaddir(...a),
|
||||
stat: (...a: unknown[]) => mockStat(...a),
|
||||
open: (...a: unknown[]) => mockOpen(...a),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>()
|
||||
return { ...actual, default: { ...actual.default, homedir: () => mockHomedir() } }
|
||||
})
|
||||
|
||||
const { parseSessionMeta, listSessions } = await import('../src/http/history.js')
|
||||
|
||||
// ── parseSessionMeta (pure) ───────────────────────────────────────────────────
|
||||
describe('parseSessionMeta', () => {
|
||||
it('extracts cwd and the first user prompt (string content)', () => {
|
||||
const jsonl = [
|
||||
@@ -31,3 +53,71 @@ describe('parseSessionMeta', () => {
|
||||
expect(parseSessionMeta('not json\n{"type":"summary"}')).toEqual({ cwd: null, preview: '' })
|
||||
})
|
||||
})
|
||||
|
||||
// ── listSessions (fs traversal, async) ────────────────────────────────────────
|
||||
describe('listSessions', () => {
|
||||
function mockFile(text: string) {
|
||||
const buf = Buffer.from(text, 'utf8')
|
||||
return {
|
||||
read: vi.fn(async (b: Buffer, off: number, len: number) => {
|
||||
const n = buf.copy(b, off, 0, Math.min(len, buf.length))
|
||||
return { bytesRead: n }
|
||||
}),
|
||||
close: vi.fn(async () => undefined),
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockReaddir.mockReset()
|
||||
mockStat.mockReset()
|
||||
mockOpen.mockReset()
|
||||
mockHomedir.mockReturnValue('/home/tester')
|
||||
})
|
||||
|
||||
it('returns [] when the projects root is missing', async () => {
|
||||
mockReaddir.mockRejectedValueOnce(new Error('ENOENT'))
|
||||
expect(await listSessions()).toEqual([])
|
||||
})
|
||||
|
||||
it('lists .jsonl sessions newest-first with parsed cwd/preview/project', async () => {
|
||||
mockReaddir
|
||||
.mockResolvedValueOnce(['projA']) // root
|
||||
.mockResolvedValueOnce(['old.jsonl', 'new.jsonl', 'ignore.txt']) // projA
|
||||
mockStat.mockImplementation(async (p: string) => ({
|
||||
mtimeMs: p.endsWith('new.jsonl') ? 2000 : 1000,
|
||||
}))
|
||||
mockOpen.mockImplementation(async (p: string) =>
|
||||
p.endsWith('new.jsonl')
|
||||
? mockFile(JSON.stringify({ type: 'user', cwd: '/work/newdir', message: { role: 'user', content: 'newest task' } }))
|
||||
: mockFile(JSON.stringify({ type: 'user', cwd: '/work/olddir', message: { role: 'user', content: 'older task' } })),
|
||||
)
|
||||
|
||||
const out = await listSessions()
|
||||
expect(out.map((s) => s.id)).toEqual(['new', 'old']) // newest first
|
||||
expect(out[0]).toMatchObject({ id: 'new', cwd: '/work/newdir', project: 'newdir', preview: 'newest task' })
|
||||
// .txt is skipped
|
||||
expect(out).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('skips a project subdir that cannot be read', async () => {
|
||||
mockReaddir
|
||||
.mockResolvedValueOnce(['good', 'bad'])
|
||||
.mockResolvedValueOnce(['s.jsonl']) // good
|
||||
.mockRejectedValueOnce(new Error('EACCES')) // bad
|
||||
mockStat.mockResolvedValue({ mtimeMs: 1000 })
|
||||
mockOpen.mockResolvedValue(mockFile(JSON.stringify({ type: 'user', cwd: '/g', message: { role: 'user', content: 'hi' } })))
|
||||
|
||||
const out = await listSessions()
|
||||
expect(out).toHaveLength(1)
|
||||
expect(out[0]!.id).toBe('s')
|
||||
})
|
||||
|
||||
it('respects the limit', async () => {
|
||||
mockReaddir.mockResolvedValueOnce(['p']).mockResolvedValueOnce(['a.jsonl', 'b.jsonl', 'c.jsonl'])
|
||||
mockStat.mockImplementation(async (p: string) => ({ mtimeMs: p.charCodeAt(p.length - 7) }))
|
||||
mockOpen.mockResolvedValue(mockFile(JSON.stringify({ type: 'user', cwd: '/p', message: { role: 'user', content: 'x' } })))
|
||||
|
||||
const out = await listSessions(2)
|
||||
expect(out).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user