|
1 | 1 | import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; |
2 | 2 | import { execFile, type ChildProcess } from "node:child_process"; |
3 | | -import { existsSync, mkdtempSync, writeFileSync, rmSync } from "node:fs"; |
| 3 | +import { existsSync, mkdtempSync, writeFileSync, rmSync, symlinkSync, unlinkSync } from "node:fs"; |
4 | 4 | import { tmpdir } from "node:os"; |
5 | 5 | import { join, resolve } from "node:path"; |
6 | 6 | import { runAimockCli, type AimockCliDeps } from "../aimock-cli.js"; |
@@ -189,6 +189,66 @@ describe.skipIf(!CLI_AVAILABLE)("aimock CLI: server lifecycle", () => { |
189 | 189 | }); |
190 | 190 | }); |
191 | 191 |
|
| 192 | +describe.skipIf(!CLI_AVAILABLE)("aimock CLI: npx/bunx symlink invocation (#160)", () => { |
| 193 | + let tmpDir: string; |
| 194 | + let symlinkPath: string; |
| 195 | + |
| 196 | + beforeEach(() => { |
| 197 | + tmpDir = makeTmpDir(); |
| 198 | + symlinkPath = join(tmpDir, "aimock"); |
| 199 | + symlinkSync(CLI_PATH, symlinkPath); |
| 200 | + }); |
| 201 | + |
| 202 | + afterEach(() => { |
| 203 | + try { |
| 204 | + unlinkSync(symlinkPath); |
| 205 | + } catch { |
| 206 | + /* already cleaned up */ |
| 207 | + } |
| 208 | + rmSync(tmpDir, { recursive: true, force: true }); |
| 209 | + }); |
| 210 | + |
| 211 | + it("starts when invoked as 'aimock' symlink (npx/bunx scenario)", async () => { |
| 212 | + const fixturePath = writeFixtureFile(tmpDir); |
| 213 | + const configPath = writeConfig(tmpDir, { |
| 214 | + llm: { fixtures: fixturePath }, |
| 215 | + }); |
| 216 | + |
| 217 | + let out = ""; |
| 218 | + let err = ""; |
| 219 | + const cp = execFile("node", [symlinkPath, "--config", configPath]); |
| 220 | + cp.stdout?.on("data", (d: string) => { |
| 221 | + out += d; |
| 222 | + }); |
| 223 | + cp.stderr?.on("data", (d: string) => { |
| 224 | + err += d; |
| 225 | + }); |
| 226 | + |
| 227 | + // Wait for "listening" output — proves the entry-point guard fired |
| 228 | + await new Promise<void>((resolve, reject) => { |
| 229 | + const deadline = setTimeout(() => { |
| 230 | + reject(new Error(`Timed out — stdout: ${out}, stderr: ${err}`)); |
| 231 | + }, 5000); |
| 232 | + const check = () => { |
| 233 | + if (/listening on/i.test(out) || /listening on/i.test(err)) { |
| 234 | + clearTimeout(deadline); |
| 235 | + resolve(); |
| 236 | + return; |
| 237 | + } |
| 238 | + setTimeout(check, 50); |
| 239 | + }; |
| 240 | + check(); |
| 241 | + }); |
| 242 | + |
| 243 | + expect(out).toContain("listening on"); |
| 244 | + |
| 245 | + cp.kill("SIGTERM"); |
| 246 | + await new Promise<void>((resolve) => { |
| 247 | + cp.on("close", () => resolve()); |
| 248 | + }); |
| 249 | + }); |
| 250 | +}); |
| 251 | + |
192 | 252 | /* ================================================================== */ |
193 | 253 | /* Unit tests (exercise runAimockCli directly for coverage) */ |
194 | 254 | /* ================================================================== */ |
|
0 commit comments