|
1 | | -import { chmodSync, copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
| 1 | +import { EventEmitter } from "node:events"; |
| 2 | +import { |
| 3 | + chmodSync, |
| 4 | + copyFileSync, |
| 5 | + existsSync, |
| 6 | + mkdtempSync, |
| 7 | + readFileSync, |
| 8 | + rmSync, |
| 9 | + statSync, |
| 10 | + writeFileSync, |
| 11 | +} from "node:fs"; |
2 | 12 | import { createServer } from "node:net"; |
3 | 13 | import { tmpdir } from "node:os"; |
4 | | -import { delimiter, join, win32 } from "node:path"; |
| 14 | +import { basename, delimiter, join, win32 } from "node:path"; |
5 | 15 | import { setTimeout as delay } from "node:timers/promises"; |
6 | 16 | import { pathToFileURL } from "node:url"; |
7 | 17 | import { beforeAll, describe, expect, it, vi } from "vitest"; |
8 | 18 | import { |
9 | 19 | modelProviderConfigBatchJson, |
10 | 20 | readPositiveIntEnv, |
| 21 | + resolveLatestVersion, |
11 | 22 | resolveParallelsModelTimeoutSeconds, |
12 | 23 | resolveProviderAuth as resolveProviderAuthDirect, |
13 | 24 | resolveSnapshot, |
@@ -84,6 +95,21 @@ function writeFakePrlctl(tempDir: string, posixScript: string, windowsBootstrap: |
84 | 95 | writeFileSync(join(tempDir, "prlctl-bootstrap.mjs"), windowsBootstrap); |
85 | 96 | } |
86 | 97 |
|
| 98 | +class FakeHostServerChild extends EventEmitter { |
| 99 | + exitCode: number | null = null; |
| 100 | + readonly signals: string[] = []; |
| 101 | + |
| 102 | + kill(signal?: NodeJS.Signals | number): boolean { |
| 103 | + this.signals.push(String(signal)); |
| 104 | + return true; |
| 105 | + } |
| 106 | + |
| 107 | + exit(): void { |
| 108 | + this.exitCode = 0; |
| 109 | + this.emit("exit", 0, null); |
| 110 | + } |
| 111 | +} |
| 112 | + |
87 | 113 | function withEnv<T>(env: Record<string, string>, callback: () => T): T { |
88 | 114 | const previous = new Map<string, string | undefined>(); |
89 | 115 | for (const [key, _value] of Object.entries(env)) { |
@@ -286,6 +312,58 @@ describe("Parallels smoke model selection", () => { |
286 | 312 | expect(retained).toBe(`${"a".repeat(2)}${"b".repeat(10)}`); |
287 | 313 | }); |
288 | 314 |
|
| 315 | + it("waits for host artifact server exit after SIGKILL before stop resolves", async () => { |
| 316 | + vi.useFakeTimers(); |
| 317 | + try { |
| 318 | + const child = new FakeHostServerChild(); |
| 319 | + const stop = hostServerTesting.stopHostServerChild(child as never, 100, 100); |
| 320 | + expect(child.signals).toEqual(["SIGTERM"]); |
| 321 | + |
| 322 | + await vi.advanceTimersByTimeAsync(100); |
| 323 | + expect(child.signals).toEqual(["SIGTERM", "SIGKILL"]); |
| 324 | + |
| 325 | + let resolved = false; |
| 326 | + void stop.then(() => { |
| 327 | + resolved = true; |
| 328 | + }); |
| 329 | + await Promise.resolve(); |
| 330 | + expect(resolved).toBe(false); |
| 331 | + |
| 332 | + child.exit(); |
| 333 | + await expect(stop).resolves.toBe(true); |
| 334 | + expect(resolved).toBe(true); |
| 335 | + } finally { |
| 336 | + vi.useRealTimers(); |
| 337 | + } |
| 338 | + }); |
| 339 | + |
| 340 | + it("uses a temporary npmrc file and cleans it after resolving the latest package version", () => { |
| 341 | + const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-parallels-version-")); |
| 342 | + let userConfigPath = ""; |
| 343 | + try { |
| 344 | + const version = resolveLatestVersion("", { |
| 345 | + createTempDir: (prefix) => { |
| 346 | + expect(prefix).toBe(join(tmpdir(), "openclaw-npm-")); |
| 347 | + return mkdtempSync(join(tempRoot, "npm-")); |
| 348 | + }, |
| 349 | + runCommand: (command, args, options) => { |
| 350 | + userConfigPath = args.at(-1) ?? ""; |
| 351 | + expect(command).toBe("npm"); |
| 352 | + expect(args).toEqual(["view", "openclaw", "version", "--userconfig", userConfigPath]); |
| 353 | + expect(options).toEqual({ quiet: true }); |
| 354 | + expect(statSync(userConfigPath).isFile()).toBe(true); |
| 355 | + return { status: 0, stderr: "", stdout: "2026.6.1\n" }; |
| 356 | + }, |
| 357 | + }); |
| 358 | + |
| 359 | + expect(version).toBe("2026.6.1"); |
| 360 | + expect(basename(userConfigPath)).toBe("npmrc"); |
| 361 | + expect(existsSync(userConfigPath)).toBe(false); |
| 362 | + } finally { |
| 363 | + rmSync(tempRoot, { force: true, recursive: true }); |
| 364 | + } |
| 365 | + }); |
| 366 | + |
289 | 367 | it.runIf(process.platform !== "win32")( |
290 | 368 | "reports only the bounded host artifact server stderr tail", |
291 | 369 | async () => { |
|
0 commit comments