/** * B4/H4 wiring test — closes the frpc-log capture loop that `HealthReport.healthy` depends on. * * Regression guarded: nothing used to WRITE `/frpc.log`, so `readFrpcLog` always returned * '' and `frpcProxyStarted` was permanently false — health could never be true. This exercises the * real production path end-to-end: `createFileLoggingSpawn` (the spawn `superviseNative` injects) * tees a child's stdout into the exact file `readFrpcLog` scans, and `frpcProxyStarted` detects the * "start proxy success" line. Writer path and reader path share `frpcLogPath`, so they can't diverge. */ import { afterEach, describe, expect, it } from 'vitest' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createFileLoggingSpawn, type FrpcChild } from '../src/transport/frpSupervise.js' import { frpcLogPath, readFrpcLog } from '../src/cli/deps.js' import { frpcProxyStarted } from '../src/health/probe.js' const SUCCESS_LINE = '[web-terminal] start proxy success' /** Poll `predicate` until true or `timeoutMs` elapses (real IO flush is async). */ async function waitFor(predicate: () => boolean, timeoutMs = 5000): Promise { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { if (predicate()) return true await new Promise((r) => setTimeout(r, 25)) } return predicate() } /** Write an executable fake `frpc` (node shebang) that repeatedly prints the success line to stdout. */ function writeFakeFrpc(dir: string): string { const bin = join(dir, 'fake-frpc.mjs') const script = `#!${process.execPath}\n` + `process.stdout.write(${JSON.stringify(`${SUCCESS_LINE}\n`)})\n` + `setInterval(() => process.stdout.write(${JSON.stringify(`${SUCCESS_LINE}\n`)}), 100)\n` writeFileSync(bin, script, { mode: 0o755 }) return bin } describe('B4/H4 frpc-log capture wiring (createFileLoggingSpawn ↔ readFrpcLog)', () => { const dirs: string[] = [] let child: FrpcChild | null = null afterEach(() => { child?.kill() child = null for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) }) it('tees the frpc child stdout into the file readFrpcLog scans → proxyStarted becomes true', async () => { const dir = mkdtempSync(join(tmpdir(), 'frpclog-')) dirs.push(dir) // Before any child runs, the log is empty and the proxy is not started. expect(readFrpcLog(dir)).toBe('') expect(frpcProxyStarted(readFrpcLog(dir))).toBe(false) // Spawn via the SAME factory superviseNative injects, pointed at the SAME path it reads. const bin = writeFakeFrpc(dir) const spawn = createFileLoggingSpawn(frpcLogPath(dir)) child = spawn(bin, join(dir, 'frpc.toml')) const detected = await waitFor(() => frpcProxyStarted(readFrpcLog(dir))) expect(detected).toBe(true) expect(readFrpcLog(dir)).toContain('start proxy success') }) it('createFileLoggingSpawn truncates a stale log so a dead child’s success line is not reused', async () => { const dir = mkdtempSync(join(tmpdir(), 'frpclog-')) dirs.push(dir) // Simulate a leftover log from a previous (now dead) frpc that had succeeded. writeFileSync(frpcLogPath(dir), `${SUCCESS_LINE}\n`) expect(frpcProxyStarted(readFrpcLog(dir))).toBe(true) // A fresh spawn whose child never prints the line must NOT keep reporting the stale success. const bin = join(dir, 'silent-frpc.mjs') writeFileSync(bin, `#!${process.execPath}\nsetInterval(() => {}, 100)\n`, { mode: 0o755 }) const spawn = createFileLoggingSpawn(frpcLogPath(dir)) child = spawn(bin, join(dir, 'frpc.toml')) // Truncate-on-spawn clears the stale line; the silent child adds none. const cleared = await waitFor(() => readFrpcLog(dir) === '') expect(cleared).toBe(true) expect(frpcProxyStarted(readFrpcLog(dir))).toBe(false) }) }) describe('frpcLogPath / readFrpcLog (deterministic)', () => { it('frpcLogPath joins frpc.log under the state dir', () => { expect(frpcLogPath('/state/x')).toBe(join('/state/x', 'frpc.log')) }) it('readFrpcLog returns "" when the log file does not exist', () => { const dir = mkdtempSync(join(tmpdir(), 'frpclog-')) try { expect(readFrpcLog(dir)).toBe('') } finally { rmSync(dir, { recursive: true, force: true }) } }) })