mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): tsc clean before dev / build
A stray tsc run can emit foo.js next to foo.ts under apps/shared/src or apps/desktop/src. .gitignore hides the artifact from git status, but Vite resolves extensionless imports .js-before-.ts, so the renderer silently runs the stale compiled copy. tsc -b . --clean already knows the emit graph and deletes matching outputs. Run it before vite in all dev scripts. This bit for real: a Jul 16 artifact of websocket-url.js predated the #68250 getGatewayWsUrl contract change ({ ok, wsUrl } IPC result), so its old 'if (fresh) return fresh' handed the whole result object to new WebSocket(), dialing ws://127.0.0.1:5174/[object%20Object] on every boot. The desktop app could never connect, and the failure survived reboots and cache wipes because the poison lived in src/. JsonRpcGatewayClient.connect() now rejects non-ws:// URLs with a readable error instead of letting new WebSocket() coerce an object into [object%20Object], so any future contract skew fails diagnosably.
This commit is contained in:
parent
cbc1054e23
commit
9160f10fc9
3 changed files with 78 additions and 8 deletions
|
|
@ -11,15 +11,19 @@
|
|||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "npm run clean:e2e && npm run clean:renderer && npm run clean:electron",
|
||||
"clean:e2e":"tsc --build tsconfig.e2e.json --clean",
|
||||
"clean:renderer":"tsc --build tsconfig.json --clean ",
|
||||
"clean:electron":"tsc --build tsconfig.electron.json --clean",
|
||||
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev",
|
||||
"dev:mock": "node scripts/dev-mock.mjs",
|
||||
"dev:renderer": "node scripts/assert-root-install.mjs && vite --host 127.0.0.1 --port 5174",
|
||||
"dev:electron": "wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"profile:main": "wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||
"profile:main:cpu": "wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"dev:renderer": "node scripts/assert-root-install.mjs && npm run clean:renderer && vite --host 127.0.0.1 --port 5174",
|
||||
"dev:electron": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"profile:main": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||
"profile:main:cpu": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"start": "npm run build && electron .",
|
||||
"prebuild": "tsc -b . --clean",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs",
|
||||
"postbuild": "node scripts/assert-dist-built.mjs",
|
||||
"prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs",
|
||||
|
|
@ -51,9 +55,9 @@
|
|||
"test": "vitest run",
|
||||
"preview": "node scripts/assert-root-install.mjs && vite preview --host 127.0.0.1 --port 4174",
|
||||
"check": "npm run typecheck && npm run test && npm run test:desktop:all",
|
||||
"test:e2e": "playwright test e2e/",
|
||||
"test:e2e:visual": "WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- npx playwright test e2e/ --reporter=list",
|
||||
"test:e2e:update-snapshots": "WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- npx playwright test e2e/ --reporter=list --update-snapshots"
|
||||
"test:e2e": "npm run clean:e2e && playwright test e2e/",
|
||||
"test:e2e:visual": "npm run clean:e2e && WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- playwright test e2e/ --reporter=list",
|
||||
"test:e2e:update-snapshots": "npm run clean:e2e && WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- playwright test e2e/ --reporter=list --update-snapshots"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assistant-ui/react": "^0.14.23",
|
||||
|
|
|
|||
59
apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts
Normal file
59
apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// connect() must reject before WebSocket coerces garbage into
|
||||
// `ws://<origin>/[object%20Object]` (#68250 stale-emit boot loop).
|
||||
|
||||
import { JsonRpcGatewayClient } from '@hermes/shared'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
class FakeSocket {
|
||||
static OPEN = 1
|
||||
readyState = 0
|
||||
addEventListener = vi.fn((type: string, handler: () => void) => {
|
||||
if (type === 'open') {
|
||||
setTimeout(() => {
|
||||
this.readyState = FakeSocket.OPEN
|
||||
handler()
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
removeEventListener = vi.fn()
|
||||
close = vi.fn()
|
||||
send = vi.fn()
|
||||
}
|
||||
|
||||
describe('JsonRpcGatewayClient connect() URL guard', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('WebSocket', FakeSocket) // jsdom has none; class reads WebSocket.OPEN
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('rejects a non-string IPC result object', async () => {
|
||||
const client = new JsonRpcGatewayClient()
|
||||
await expect(
|
||||
client.connect({ ok: true, wsUrl: 'ws://127.0.0.1:1/api/ws' } as unknown as string)
|
||||
).rejects.toThrow(/requires a ws:\/\/ or wss:\/\/ URL string, got type "object"/)
|
||||
})
|
||||
|
||||
it('rejects a non-ws URL string', async () => {
|
||||
const client = new JsonRpcGatewayClient()
|
||||
await expect(client.connect('http://127.0.0.1:1234/api/ws')).rejects.toThrow(
|
||||
/requires a ws:\/\/ or wss:\/\/ URL string/
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps connection state idle on rejection', async () => {
|
||||
const client = new JsonRpcGatewayClient()
|
||||
await client.connect(undefined as unknown as string).catch(() => undefined)
|
||||
expect(client.connectionState).toBe('idle')
|
||||
})
|
||||
|
||||
it('accepts ws:// and wss://', async () => {
|
||||
for (const url of ['ws://127.0.0.1:1234/api/ws?token=t', 'wss://gw.example.com/api/ws?ticket=t']) {
|
||||
const client = new JsonRpcGatewayClient({ socketFactory: () => new FakeSocket() as unknown as WebSocket })
|
||||
await client.connect(url)
|
||||
expect(client.connectionState).toBe('open')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -95,6 +95,13 @@ export class JsonRpcGatewayClient {
|
|||
}
|
||||
|
||||
async connect(wsUrl: string): Promise<void> {
|
||||
// Refuse garbage; WebSocket coerces non-strings into
|
||||
// `ws://<origin>/[object%20Object]` (#68250 stale-emit boot loop).
|
||||
if (typeof wsUrl !== 'string' || !/^wss?:\/\//i.test(wsUrl)) {
|
||||
const got = typeof wsUrl === 'string' ? JSON.stringify(wsUrl) : `type "${typeof wsUrl}"`
|
||||
throw new Error(`gateway connect() requires a ws:// or wss:// URL string, got ${got}`)
|
||||
}
|
||||
|
||||
if (this.socket?.readyState === WebSocket.OPEN || this.state === 'connecting') {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue