/** * Integration test for GET /projects/log (W3 quick-wins d). * * Starts a real HTTP server, then asserts the recent-commit log route against a * real temp git repo (3 commits): 200 + structured GitLogResult; missing path → * 400; a non-git temp dir → 404 (isValidGitDir three-prong); ?n clamped. */ import fs from 'node:fs/promises' import net from 'node:net' import os from 'node:os' import path from 'node:path' import { execFile } from 'node:child_process' import { promisify } from 'node:util' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { loadConfig } from '../../src/config.js' import { startServer } from '../../src/server.js' import type { GitLogResult } from '../../src/types.js' const execFileAsync = promisify(execFile) function getFreePort(): Promise { return new Promise((resolve, reject) => { const srv = net.createServer() srv.listen(0, '127.0.0.1', () => { const addr = srv.address() if (addr === null || typeof addr === 'string') { srv.close() reject(new Error('unexpected address type')) return } const port = addr.port srv.close(() => resolve(port)) }) srv.on('error', reject) }) } async function git(cwd: string, ...args: string[]): Promise { await execFileAsync('git', args, { cwd }) } async function gitAvailable(): Promise { try { await execFileAsync('git', ['--version']) return true } catch { return false } } describe('GET /projects/log — integration', () => { let port: number let tmpRoot: string let repoPath: string let plainPath: string let serverHandle: { close(): Promise } let haveGit = false beforeAll(async () => { haveGit = await gitAvailable() port = await getFreePort() tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'webterm-log-test-')) repoPath = path.join(tmpRoot, 'repo') plainPath = path.join(tmpRoot, 'plain') await fs.mkdir(plainPath, { recursive: true }) if (haveGit) { await fs.mkdir(repoPath, { recursive: true }) await git(repoPath, 'init', '-q', '-b', 'main') await git(repoPath, 'config', 'user.email', 't@t.local') await git(repoPath, 'config', 'user.name', 'tester') await git(repoPath, 'config', 'commit.gpgsign', 'false') for (const [file, msg] of [['a.txt', 'first'], ['b.txt', 'second'], ['c.txt', 'third']]) { await fs.writeFile(path.join(repoPath, file), `${file}\n`) await git(repoPath, 'add', '.') await git(repoPath, 'commit', '-q', '-m', msg) } } const cfg = loadConfig({ PORT: String(port), BIND_HOST: '127.0.0.1', SHELL_PATH: process.env['SHELL'] ?? '/bin/zsh', ALLOWED_ORIGINS: `http://127.0.0.1:${port}`, USE_TMUX: '0', IDLE_TTL: '86400', }) serverHandle = startServer(cfg) await new Promise((r) => setTimeout(r, 100)) }) afterAll(async () => { await serverHandle.close() await fs.rm(tmpRoot, { recursive: true, force: true }) }) it('returns 400 when path is missing', async () => { const res = await fetch(`http://127.0.0.1:${port}/projects/log`) expect(res.status).toBe(400) }) it('returns 404 for a non-git directory (isValidGitDir three-prong)', async () => { const res = await fetch(`http://127.0.0.1:${port}/projects/log?path=${encodeURIComponent(plainPath)}`) expect(res.status).toBe(404) }) it('returns 200 with the recent commits newest-first', async () => { if (!haveGit) return const res = await fetch(`http://127.0.0.1:${port}/projects/log?path=${encodeURIComponent(repoPath)}`) expect(res.status).toBe(200) const body = (await res.json()) as GitLogResult expect(body.commits.map((c) => c.subject)).toEqual(['third', 'second', 'first']) }) it('clamps ?n and flags truncated', async () => { if (!haveGit) return const res = await fetch( `http://127.0.0.1:${port}/projects/log?path=${encodeURIComponent(repoPath)}&n=2`, ) const body = (await res.json()) as GitLogResult expect(body.commits).toHaveLength(2) expect(body.truncated).toBe(true) // A huge n must be clamped to ≤ 50 (never explodes) — here only 3 commits exist. const res2 = await fetch( `http://127.0.0.1:${port}/projects/log?path=${encodeURIComponent(repoPath)}&n=999`, ) const body2 = (await res2.json()) as GitLogResult expect(body2.commits.length).toBeLessThanOrEqual(50) expect(body2.commits).toHaveLength(3) }) })