This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Moar tests

Nathan Herald (Apr 3, 2026, 11:19 AM +0200) 5e7c2d38 8ac8e0e2

+234
+234
tests/rm-kill-ephemeral.test.ts
··· 1 + import { describe, it, expect, afterEach, afterAll } from "vitest"; 2 + import * as fs from "node:fs"; 3 + import * as os from "node:os"; 4 + import * as path from "node:path"; 5 + import { fileURLToPath } from "node:url"; 6 + import { spawn, execFileSync } from "node:child_process"; 7 + 8 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 + const nodeBin = process.execPath; 10 + const cliPath = path.join(__dirname, "..", "dist", "cli.js"); 11 + const serverModule = path.join(__dirname, "..", "dist", "server.js"); 12 + 13 + const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pty-rmkill-")); 14 + afterAll(() => { 15 + fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); 16 + }); 17 + 18 + let bgPids: number[] = []; 19 + let sessionDirs: string[] = []; 20 + 21 + function makeSessionDir(): string { 22 + const dir = fs.mkdtempSync(path.join(testRoot, "d-")); 23 + sessionDirs.push(dir); 24 + return dir; 25 + } 26 + 27 + let nameCounter = 0; 28 + function uniqueName(): string { 29 + return `t${++nameCounter}-${Math.random().toString(36).slice(2, 6)}`; 30 + } 31 + 32 + async function startDaemon( 33 + sessionDir: string, 34 + name: string, 35 + command: string, 36 + args: string[] = [], 37 + opts: { ephemeral?: boolean } = {}, 38 + ): Promise<number> { 39 + const config = JSON.stringify({ 40 + name, 41 + command, 42 + args, 43 + displayCommand: command, 44 + cwd: os.tmpdir(), 45 + rows: 24, 46 + cols: 80, 47 + ephemeral: opts.ephemeral ?? false, 48 + }); 49 + 50 + const child = spawn(nodeBin, [serverModule], { 51 + detached: true, 52 + stdio: ["ignore", "ignore", "pipe"], 53 + env: { 54 + ...process.env, 55 + PTY_SERVER_CONFIG: config, 56 + PTY_SESSION_DIR: sessionDir, 57 + }, 58 + }); 59 + 60 + let stderr = ""; 61 + child.stderr?.on("data", (d: Buffer) => { stderr += d.toString(); }); 62 + let exitCode: number | null = null; 63 + child.on("exit", (code) => { exitCode = code; }); 64 + (child.stderr as any)?.unref?.(); 65 + child.unref(); 66 + 67 + const socketPath = path.join(sessionDir, `${name}.sock`); 68 + const start = Date.now(); 69 + while (Date.now() - start < 5000) { 70 + if (exitCode !== null) { 71 + throw new Error(`Daemon exited with code ${exitCode}. stderr:\n${stderr}`); 72 + } 73 + try { 74 + fs.statSync(socketPath); 75 + await new Promise((r) => setTimeout(r, 100)); 76 + bgPids.push(child.pid!); 77 + return child.pid!; 78 + } catch {} 79 + await new Promise((r) => setTimeout(r, 50)); 80 + } 81 + throw new Error(`Timeout waiting for daemon socket: ${socketPath}`); 82 + } 83 + 84 + function runCli(sessionDir: string, ...args: string[]): { stdout: string; status: number } { 85 + try { 86 + const stdout = execFileSync(nodeBin, [cliPath, ...args], { 87 + env: { ...process.env, PTY_SESSION_DIR: sessionDir }, 88 + encoding: "utf-8", 89 + timeout: 10000, 90 + }); 91 + return { stdout, status: 0 }; 92 + } catch (err: any) { 93 + return { stdout: (err.stdout ?? "") + (err.stderr ?? ""), status: err.status ?? 1 }; 94 + } 95 + } 96 + 97 + function sessionFiles(dir: string, name: string): string[] { 98 + try { 99 + return fs.readdirSync(dir).filter((f) => f.startsWith(name)); 100 + } catch { 101 + return []; 102 + } 103 + } 104 + 105 + afterEach(() => { 106 + for (const pid of bgPids) { 107 + try { process.kill(pid, "SIGTERM"); } catch {} 108 + } 109 + bgPids = []; 110 + for (const dir of sessionDirs) { 111 + try { 112 + for (const e of fs.readdirSync(dir)) { 113 + try { fs.unlinkSync(path.join(dir, e)); } catch {} 114 + } 115 + } catch {} 116 + } 117 + sessionDirs = []; 118 + }); 119 + 120 + // --- pty kill --- 121 + 122 + describe("pty kill", () => { 123 + it("kills a running session and keeps metadata", async () => { 124 + const dir = makeSessionDir(); 125 + const name = uniqueName(); 126 + await startDaemon(dir, name, "cat"); 127 + 128 + const result = runCli(dir, "kill", name); 129 + expect(result.stdout).toContain("killed"); 130 + 131 + // Wait for process to die 132 + await new Promise((r) => setTimeout(r, 500)); 133 + 134 + // Socket should be gone, but metadata should remain 135 + const files = sessionFiles(dir, name); 136 + expect(files.some((f) => f.endsWith(".json"))).toBe(true); 137 + expect(files.some((f) => f.endsWith(".sock"))).toBe(false); 138 + }, 15000); 139 + 140 + it("refuses to kill an exited session", async () => { 141 + const dir = makeSessionDir(); 142 + const name = uniqueName(); 143 + await startDaemon(dir, name, "true"); // exits immediately 144 + await new Promise((r) => setTimeout(r, 1000)); 145 + 146 + const result = runCli(dir, "kill", name); 147 + expect(result.status).not.toBe(0); 148 + expect(result.stdout).toContain("not running"); 149 + }, 15000); 150 + 151 + it("errors for nonexistent session", () => { 152 + const dir = makeSessionDir(); 153 + const result = runCli(dir, "kill", "nope"); 154 + expect(result.status).not.toBe(0); 155 + expect(result.stdout).toContain("not found"); 156 + }, 15000); 157 + }); 158 + 159 + // --- pty rm --- 160 + 161 + describe("pty rm", () => { 162 + it("removes metadata for an exited session", async () => { 163 + const dir = makeSessionDir(); 164 + const name = uniqueName(); 165 + await startDaemon(dir, name, "true"); // exits immediately 166 + await new Promise((r) => setTimeout(r, 1000)); 167 + 168 + // Verify metadata exists 169 + expect(sessionFiles(dir, name).some((f) => f.endsWith(".json"))).toBe(true); 170 + 171 + const result = runCli(dir, "rm", name); 172 + expect(result.stdout).toContain("removed"); 173 + 174 + // All files should be gone 175 + expect(sessionFiles(dir, name)).toEqual([]); 176 + }, 15000); 177 + 178 + it("refuses to remove a running session", async () => { 179 + const dir = makeSessionDir(); 180 + const name = uniqueName(); 181 + await startDaemon(dir, name, "cat"); 182 + 183 + const result = runCli(dir, "rm", name); 184 + expect(result.status).not.toBe(0); 185 + expect(result.stdout).toContain("still running"); 186 + }, 15000); 187 + 188 + it("errors for nonexistent session", () => { 189 + const dir = makeSessionDir(); 190 + const result = runCli(dir, "rm", "nope"); 191 + expect(result.status).not.toBe(0); 192 + expect(result.stdout).toContain("not found"); 193 + }, 15000); 194 + }); 195 + 196 + // --- --ephemeral --- 197 + 198 + describe("--ephemeral", () => { 199 + it("cleans up all session files after process exits", async () => { 200 + const dir = makeSessionDir(); 201 + const name = uniqueName(); 202 + await startDaemon(dir, name, "sh", ["-c", "exit 0"], { ephemeral: true }); 203 + 204 + // Wait for process to exit and cleanup 205 + await new Promise((r) => setTimeout(r, 2000)); 206 + 207 + // All session files should be gone 208 + const files = sessionFiles(dir, name); 209 + expect(files).toEqual([]); 210 + }, 15000); 211 + 212 + it("session is visible while running", async () => { 213 + const dir = makeSessionDir(); 214 + const name = uniqueName(); 215 + await startDaemon(dir, name, "cat", [], { ephemeral: true }); 216 + 217 + // Should appear in ls while running 218 + const result = runCli(dir, "ls", "--json"); 219 + const sessions = JSON.parse(result.stdout); 220 + expect(sessions.some((s: any) => s.name === name)).toBe(true); 221 + }, 15000); 222 + 223 + it("disappears from ls after exit", async () => { 224 + const dir = makeSessionDir(); 225 + const name = uniqueName(); 226 + await startDaemon(dir, name, "sh", ["-c", "exit 0"], { ephemeral: true }); 227 + 228 + await new Promise((r) => setTimeout(r, 2000)); 229 + 230 + const result = runCli(dir, "ls", "--json"); 231 + const sessions = JSON.parse(result.stdout); 232 + expect(sessions.some((s: any) => s.name === name)).toBe(false); 233 + }, 15000); 234 + });