From 2f1731b930c9b4d05019260f8330f1b37331361f Mon Sep 17 00:00:00 2001 From: Onur Solmaz Date: Fri, 13 Mar 2026 07:19:12 +0100 Subject: [PATCH 1/4] ci(workflows): add missing repo test coverage --- .github/workflows/docs-check.yml | 32 +++++++++++++++++ .github/workflows/e2e-unit-tests.yml | 28 +++++++++++++++ .github/workflows/go-tests.yml | 46 +++++++++++++++++++++++++ .github/workflows/ui-tests.yml | 31 +++++++++++++++++ operator/controllers/spritz_url_test.go | 27 +++++++++++---- 5 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/docs-check.yml create mode 100644 .github/workflows/e2e-unit-tests.yml create mode 100644 .github/workflows/go-tests.yml create mode 100644 .github/workflows/ui-tests.yml diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml new file mode 100644 index 0000000..d9f5a1c --- /dev/null +++ b/.github/workflows/docs-check.yml @@ -0,0 +1,32 @@ +name: docs-check + +on: + pull_request: + paths: + - "docs/**" + - "README.md" + - "AGENTS.md" + - ".github/workflows/docs-check.yml" + push: + branches: + - main + paths: + - "docs/**" + - "README.md" + - "AGENTS.md" + - ".github/workflows/docs-check.yml" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Check docs conventions + run: npx -y @simpledoc/simpledoc check diff --git a/.github/workflows/e2e-unit-tests.yml b/.github/workflows/e2e-unit-tests.yml new file mode 100644 index 0000000..8de3569 --- /dev/null +++ b/.github/workflows/e2e-unit-tests.yml @@ -0,0 +1,28 @@ +name: e2e-unit-tests + +on: + pull_request: + paths: + - "e2e/**" + - ".github/workflows/e2e-unit-tests.yml" + push: + branches: + - main + paths: + - "e2e/**" + - ".github/workflows/e2e-unit-tests.yml" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Run e2e unit tests + run: node --test e2e/*.test.mjs diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml new file mode 100644 index 0000000..5aacc94 --- /dev/null +++ b/.github/workflows/go-tests.yml @@ -0,0 +1,46 @@ +name: go-tests + +on: + pull_request: + paths: + - "api/**" + - "operator/**" + - "integrations/github-app/**" + - ".github/workflows/go-tests.yml" + push: + branches: + - main + paths: + - "api/**" + - "operator/**" + - "integrations/github-app/**" + - ".github/workflows/go-tests.yml" + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: api + working-directory: api + - name: operator + working-directory: operator + - name: integrations-github-app + working-directory: integrations/github-app + defaults: + run: + working-directory: ${{ matrix.working-directory }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ${{ matrix.working-directory }}/go.mod + cache-dependency-path: ${{ matrix.working-directory }}/go.sum + + - name: Test + run: go test ./... diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000..b6bb180 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,31 @@ +name: ui-tests + +on: + pull_request: + paths: + - "ui/**" + - ".github/workflows/ui-tests.yml" + push: + branches: + - main + paths: + - "ui/**" + - ".github/workflows/ui-tests.yml" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Run UI tests + run: node --test ui/entrypoint.test.mjs ui/nginx-config.test.mjs ui/public/*.test.mjs + + - name: Check entrypoint shell syntax + run: bash -n ui/entrypoint.sh diff --git a/operator/controllers/spritz_url_test.go b/operator/controllers/spritz_url_test.go index bf34d51..445625f 100644 --- a/operator/controllers/spritz_url_test.go +++ b/operator/controllers/spritz_url_test.go @@ -6,44 +6,59 @@ import ( spritzv1 "spritz.sh/operator/api/v1" ) -func TestSpritzURLIngressAddsTrailingSlash(t *testing.T) { +func TestWorkspaceURLIngressAddsTrailingSlash(t *testing.T) { spritz := &spritzv1.Spritz{} spritz.Spec.Ingress = &spritzv1.SpritzIngress{ Host: "console.example.com", Path: "/workspaces/w/tidy-fjord", } - got := spritzURL(spritz) + got := spritzv1.WorkspaceURLForSpritz(spritz) want := "https://console.example.com/workspaces/w/tidy-fjord/" if got != want { t.Fatalf("expected %q, got %q", want, got) } } -func TestSpritzURLIngressRootStaysRoot(t *testing.T) { +func TestWorkspaceURLIngressRootStaysRoot(t *testing.T) { spritz := &spritzv1.Spritz{} spritz.Spec.Ingress = &spritzv1.SpritzIngress{ Host: "console.example.com", Path: "/", } - got := spritzURL(spritz) + got := spritzv1.WorkspaceURLForSpritz(spritz) want := "https://console.example.com/" if got != want { t.Fatalf("expected %q, got %q", want, got) } } -func TestSpritzURLIngressKeepsExistingTrailingSlash(t *testing.T) { +func TestWorkspaceURLIngressKeepsExistingTrailingSlash(t *testing.T) { spritz := &spritzv1.Spritz{} spritz.Spec.Ingress = &spritzv1.SpritzIngress{ Host: "console.example.com", Path: "/workspaces/w/tidy-fjord/", } - got := spritzURL(spritz) + got := spritzv1.WorkspaceURLForSpritz(spritz) want := "https://console.example.com/workspaces/w/tidy-fjord/" if got != want { t.Fatalf("expected %q, got %q", want, got) } } + +func TestSpritzURLPrefersChatURLWhenWorkspaceIsWebAccessible(t *testing.T) { + spritz := &spritzv1.Spritz{} + spritz.Name = "tidy-fjord" + spritz.Spec.Ingress = &spritzv1.SpritzIngress{ + Host: "console.example.com", + Path: "/workspaces/w/tidy-fjord", + } + + got := spritzURL(spritz) + want := "https://console.example.com/#chat/tidy-fjord" + if got != want { + t.Fatalf("expected %q, got %q", want, got) + } +} From bc072eb7cf03c2f73dcae19bdce7692ce8c00f06 Mon Sep 17 00:00:00 2001 From: Onur Solmaz Date: Fri, 13 Mar 2026 07:26:45 +0100 Subject: [PATCH 2/4] test(ui): make workflow paths portable --- e2e/acp-smoke-lib.mjs | 3 +++ ui/entrypoint.test.mjs | 7 ++++--- ui/nginx-config.test.mjs | 3 ++- ui/public/acp-client.test.mjs | 3 ++- ui/public/acp-layout-css.test.mjs | 3 ++- ui/public/acp-page-cache.test.mjs | 5 +++-- ui/public/acp-page-layout.test.mjs | 5 +++-- ui/public/acp-page-notice.test.mjs | 5 +++-- ui/public/acp-page-session-binding.test.mjs | 5 +++-- ui/public/acp-render.test.mjs | 3 ++- ui/public/app-chat-route.test.mjs | 3 ++- ui/public/app-list-actions.test.mjs | 3 ++- ui/public/preset-config.test.mjs | 3 ++- ui/test-paths.mjs | 18 ++++++++++++++++++ 14 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 ui/test-paths.mjs diff --git a/e2e/acp-smoke-lib.mjs b/e2e/acp-smoke-lib.mjs index 5a00ab7..b48dfa7 100644 --- a/e2e/acp-smoke-lib.mjs +++ b/e2e/acp-smoke-lib.mjs @@ -230,6 +230,9 @@ export function resolveACPEndpoint(spritz) { * Load a Node-compatible WebSocket client constructor from the CLI dependency set. */ export function resolveWebSocketConstructor() { + if (typeof globalThis.WebSocket === 'function') { + return globalThis.WebSocket; + } const wsModule = cliRequire('ws'); return wsModule.WebSocket || wsModule.default || wsModule; } diff --git a/ui/entrypoint.test.mjs b/ui/entrypoint.test.mjs index ffcd74b..68f2eda 100644 --- a/ui/entrypoint.test.mjs +++ b/ui/entrypoint.test.mjs @@ -4,14 +4,15 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import vm from 'node:vm'; +import { uiPath, uiPublicPath } from './test-paths.mjs'; import { execFileSync } from 'node:child_process'; function renderConfig(env = {}) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spritz-ui-')); - fs.copyFileSync('/Users/onur/repos/spritz/ui/public/config.js', path.join(tmpDir, 'config.js')); - fs.copyFileSync('/Users/onur/repos/spritz/ui/public/index.html', path.join(tmpDir, 'index.html')); + fs.copyFileSync(uiPublicPath('config.js'), path.join(tmpDir, 'config.js')); + fs.copyFileSync(uiPublicPath('index.html'), path.join(tmpDir, 'index.html')); - execFileSync('/bin/sh', ['/Users/onur/repos/spritz/ui/entrypoint.sh'], { + execFileSync('/bin/sh', [uiPath('entrypoint.sh')], { env: { ...process.env, SPRITZ_UI_HTML_DIR: tmpDir, diff --git a/ui/nginx-config.test.mjs b/ui/nginx-config.test.mjs index 7d8f113..772bf5f 100644 --- a/ui/nginx-config.test.mjs +++ b/ui/nginx-config.test.mjs @@ -1,8 +1,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; +import { uiPath } from './test-paths.mjs'; -const nginxConfig = fs.readFileSync('/Users/onur/repos/spritz/ui/nginx.conf', 'utf8'); +const nginxConfig = fs.readFileSync(uiPath('nginx.conf'), 'utf8'); test('nginx config disables caching for index and runtime config', () => { assert.match(nginxConfig, /location = \/index\.html \{[\s\S]*Cache-Control "no-store, max-age=0"/); diff --git a/ui/public/acp-client.test.mjs b/ui/public/acp-client.test.mjs index 730a887..14ff960 100644 --- a/ui/public/acp-client.test.mjs +++ b/ui/public/acp-client.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function loadACPClientModule() { const sockets = []; @@ -57,7 +58,7 @@ function loadACPClientModule() { }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-client.js', 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiPublicPath('acp-client.js'), 'utf8'), context, { filename: 'acp-client.js', }); diff --git a/ui/public/acp-layout-css.test.mjs b/ui/public/acp-layout-css.test.mjs index edd894d..6e58fca 100644 --- a/ui/public/acp-layout-css.test.mjs +++ b/ui/public/acp-layout-css.test.mjs @@ -1,8 +1,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; +import { uiPublicPath } from '../test-paths.mjs'; -const styles = fs.readFileSync('/Users/onur/repos/spritz/ui/public/styles.css', 'utf8'); +const styles = fs.readFileSync(uiPublicPath('styles.css'), 'utf8'); function getRule(selector) { const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); diff --git a/ui/public/acp-page-cache.test.mjs b/ui/public/acp-page-cache.test.mjs index 2496b5b..ffb19c6 100644 --- a/ui/public/acp-page-cache.test.mjs +++ b/ui/public/acp-page-cache.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function createStorage(seed = {}) { const values = new Map(Object.entries(seed)); @@ -110,10 +111,10 @@ function loadModules(storageSeed = {}, createACPClient = null) { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-render.js', 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'), context, { filename: 'acp-render.js', }); - vm.runInContext(fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-page.js', 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'), context, { filename: 'acp-page.js', }); return window; diff --git a/ui/public/acp-page-layout.test.mjs b/ui/public/acp-page-layout.test.mjs index ef6e384..5fca34f 100644 --- a/ui/public/acp-page-layout.test.mjs +++ b/ui/public/acp-page-layout.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function createElement(tagName) { return { @@ -75,9 +76,9 @@ function loadModules() { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout }); context.globalThis = context.window; - const renderScript = fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-render.js', 'utf8'); + const renderScript = fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'); vm.runInContext(renderScript, context, { filename: 'acp-render.js' }); - const pageScript = fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-page.js', 'utf8'); + const pageScript = fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'); vm.runInContext(pageScript, context, { filename: 'acp-page.js' }); return { window, document }; } diff --git a/ui/public/acp-page-notice.test.mjs b/ui/public/acp-page-notice.test.mjs index a493a6e..33b4a87 100644 --- a/ui/public/acp-page-notice.test.mjs +++ b/ui/public/acp-page-notice.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function createElement(tagName) { return { @@ -64,10 +65,10 @@ function loadModules(createACPClient) { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-render.js', 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'), context, { filename: 'acp-render.js', }); - vm.runInContext(fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-page.js', 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'), context, { filename: 'acp-page.js', }); return window; diff --git a/ui/public/acp-page-session-binding.test.mjs b/ui/public/acp-page-session-binding.test.mjs index 573808a..883ef24 100644 --- a/ui/public/acp-page-session-binding.test.mjs +++ b/ui/public/acp-page-session-binding.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function createElement(tagName) { const listeners = new Map(); @@ -81,10 +82,10 @@ function loadModules(createACPClient) { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-render.js', 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'), context, { filename: 'acp-render.js', }); - vm.runInContext(fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-page.js', 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'), context, { filename: 'acp-page.js', }); return window; diff --git a/ui/public/acp-render.test.mjs b/ui/public/acp-render.test.mjs index 88725b3..e3dabdb 100644 --- a/ui/public/acp-render.test.mjs +++ b/ui/public/acp-render.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function createElement(tagName) { return { @@ -50,7 +51,7 @@ function loadRenderModule() { window.window = window; const context = vm.createContext({ window, document, console }); context.globalThis = context.window; - const script = fs.readFileSync('/Users/onur/repos/spritz/ui/public/acp-render.js', 'utf8'); + const script = fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'); vm.runInContext(script, context, { filename: 'acp-render.js' }); return window.SpritzACPRender; } diff --git a/ui/public/app-chat-route.test.mjs b/ui/public/app-chat-route.test.mjs index 7ff20e8..cff9c1d 100644 --- a/ui/public/app-chat-route.test.mjs +++ b/ui/public/app-chat-route.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function createStorage() { const values = new Map(); @@ -151,7 +152,7 @@ test('chat hash route initializes without throwing', () => { }); context.globalThis = context.window; - const script = fs.readFileSync('/Users/onur/repos/spritz/ui/public/app.js', 'utf8'); + const script = fs.readFileSync(uiPublicPath('app.js'), 'utf8'); assert.doesNotThrow(() => { vm.runInContext(script, context, { filename: 'app.js' }); }); diff --git a/ui/public/app-list-actions.test.mjs b/ui/public/app-list-actions.test.mjs index 21c714a..56cf89c 100644 --- a/ui/public/app-list-actions.test.mjs +++ b/ui/public/app-list-actions.test.mjs @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; +import { uiPublicPath } from '../test-paths.mjs'; function createStorage() { const values = new Map(); @@ -174,7 +175,7 @@ test('spritz list shows a transitional chat action while workspace chat is still }); context.globalThis = context.window; - const script = fs.readFileSync('/Users/onur/repos/spritz/ui/public/app.js', 'utf8'); + const script = fs.readFileSync(uiPublicPath('app.js'), 'utf8'); vm.runInContext(script, context, { filename: 'app.js' }); await new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/ui/public/preset-config.test.mjs b/ui/public/preset-config.test.mjs index b09479b..fa44098 100644 --- a/ui/public/preset-config.test.mjs +++ b/ui/public/preset-config.test.mjs @@ -1,9 +1,10 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; +import { uiPublicPath } from '../test-paths.mjs'; const require = createRequire(import.meta.url); -const { parsePresets } = require('/Users/onur/repos/spritz/ui/public/preset-config.js'); +const { parsePresets } = require(uiPublicPath('preset-config.js')); test('parsePresets returns raw arrays directly', () => { const presets = [{ name: 'OpenClaw', image: 'example/openclaw' }]; diff --git a/ui/test-paths.mjs b/ui/test-paths.mjs new file mode 100644 index 0000000..70f4ce0 --- /dev/null +++ b/ui/test-paths.mjs @@ -0,0 +1,18 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const uiDir = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Resolve a path relative to the ui/ directory. + */ +export function uiPath(...parts) { + return path.join(uiDir, ...parts); +} + +/** + * Resolve a path relative to the ui/public/ directory. + */ +export function uiPublicPath(...parts) { + return path.join(uiDir, 'public', ...parts); +} From 21e9659aafa6ecdfaea4abbaf768efb60299963c Mon Sep 17 00:00:00 2001 From: Onur Solmaz Date: Fri, 13 Mar 2026 07:28:31 +0100 Subject: [PATCH 3/4] test(e2e): remove environment-specific path assumptions --- e2e/acp-smoke-lib.mjs | 10 ++++++---- e2e/acp-smoke-lib.test.mjs | 9 ++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/e2e/acp-smoke-lib.mjs b/e2e/acp-smoke-lib.mjs index b48dfa7..7723a20 100644 --- a/e2e/acp-smoke-lib.mjs +++ b/e2e/acp-smoke-lib.mjs @@ -229,11 +229,13 @@ export function resolveACPEndpoint(spritz) { /** * Load a Node-compatible WebSocket client constructor from the CLI dependency set. */ -export function resolveWebSocketConstructor() { - if (typeof globalThis.WebSocket === 'function') { - return globalThis.WebSocket; +export function resolveWebSocketConstructor(options = {}) { + const globalObject = options.globalObject ?? globalThis; + if (typeof globalObject?.WebSocket === 'function') { + return globalObject.WebSocket; } - const wsModule = cliRequire('ws'); + const requireFn = options.requireFn ?? cliRequire; + const wsModule = requireFn('ws'); return wsModule.WebSocket || wsModule.default || wsModule; } diff --git a/e2e/acp-smoke-lib.test.mjs b/e2e/acp-smoke-lib.test.mjs index 5e93e6b..80ab5bc 100644 --- a/e2e/acp-smoke-lib.test.mjs +++ b/e2e/acp-smoke-lib.test.mjs @@ -140,8 +140,15 @@ test('resolveACPEndpoint prefers discovered ACP port/path and normalizes the pat }); test('resolveWebSocketConstructor returns a usable client constructor', () => { - const WebSocketCtor = resolveWebSocketConstructor(); + class FakeWebSocket {} + const WebSocketCtor = resolveWebSocketConstructor({ + globalObject: {}, + requireFn() { + return { WebSocket: FakeWebSocket }; + }, + }); assert.equal(typeof WebSocketCtor, 'function'); + assert.equal(WebSocketCtor, FakeWebSocket); }); test('assertSmokeCreateResponse accepts canonicalized preset ids from the API', () => { From d4165dbee34cbc2759eb854c8b4e818fad903589 Mon Sep 17 00:00:00 2001 From: Onur Solmaz Date: Fri, 13 Mar 2026 07:30:52 +0100 Subject: [PATCH 4/4] fix(e2e): keep rpc timeout alive in tests --- e2e/acp-client.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/acp-client.mjs b/e2e/acp-client.mjs index 6e2ce44..98cc684 100644 --- a/e2e/acp-client.mjs +++ b/e2e/acp-client.mjs @@ -103,7 +103,7 @@ export async function connectACPWebSocket(url, timeoutSeconds, options = {}) { } pending.delete(id); reject(new Error(`ACP request ${method} timed out`)); - }, rpcTimeoutMs).unref?.(); + }, rpcTimeoutMs); }); }