Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/src/main/hl/engines/browserHarnessEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { createHash } from 'node:crypto';
import path from 'node:path';
import type { SpawnContext } from './types';

export function browserHarnessReplPort(sessionId: string): string {
const n = createHash('sha256').update(sessionId).digest().readUInt16BE(0);
export function browserHarnessReplPort(sessionId: string, targetId = ''): string {
const n = createHash('sha256').update(`${sessionId}:${targetId}`).digest().readUInt16BE(0);
return String(18_000 + (n % 20_000));
}

export function applyBrowserHarnessEnv(ctx: SpawnContext, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const sdkDir = path.join(ctx.harnessDir, 'browser-harness-js', 'sdk');
env.PATH = env.PATH ? `${sdkDir}${path.delimiter}${env.PATH}` : sdkDir;
env.CDP_REPL_PORT = env.CDP_REPL_PORT ?? browserHarnessReplPort(ctx.sessionId);
env.CDP_REPL_PORT = env.CDP_REPL_PORT ?? browserHarnessReplPort(ctx.sessionId, ctx.targetId);
env.CDP_REPL_LOG = env.CDP_REPL_LOG ?? path.join(ctx.harnessDir, `browser-harness-js-${ctx.sessionId}.log`);
env.BU_SESSION_ID = ctx.sessionId;
return env;
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/hl/stock/browser-harness-js/sdk/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ async function connectToAssignedTarget(): Promise<{ targetId: string; port: numb
if (!session.isConnected()) {
await session.connect({ port, targetId });
} else {
await session.use(targetId).catch(() => {});
try {
await session.use(targetId);
} catch {
session.close();
await session.connect({ port, targetId });
}
}

await Promise.all([
Expand Down
19 changes: 15 additions & 4 deletions app/src/main/hl/stock/browser-harness-js/sdk/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,16 @@ export class Session implements Transport {
const timer = setTimeout(() => finish(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
ws.addEventListener('open', () => finish());
ws.addEventListener('error', (e) => finish(new Error(`WS error: ${(e as any)?.message ?? 'connect failed (likely 403, permission not granted, or port closed)'}`)));
ws.addEventListener('message', (e) => this.onMessage(String(e.data)));
ws.addEventListener('message', (e) => {
if (this.ws === ws) this.onMessage(String(e.data));
});
ws.addEventListener('close', () => {
for (const [, p] of this.pending) p.reject(new Error('CDP socket closed'));
this.pending.clear();
if (this.ws === ws) {
this.ws = undefined;
this.activeSessionId = undefined;
for (const [, p] of this.pending) p.reject(new Error('CDP socket closed'));
this.pending.clear();
}
finish(new Error('WS closed before open (likely 403 or port closed)'));
});
this.ws = ws;
Expand All @@ -139,7 +145,12 @@ export class Session implements Transport {
}

close(): void {
this.ws?.close();
const ws = this.ws;
this.ws = undefined;
this.activeSessionId = undefined;
for (const [, p] of this.pending) p.reject(new Error('CDP socket closed'));
this.pending.clear();
ws?.close();
}

/**
Expand Down
39 changes: 39 additions & 0 deletions app/tests/unit/hl/browserHarnessEnv.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { applyBrowserHarnessEnv, browserHarnessReplPort } from '../../../src/main/hl/engines/browserHarnessEnv';
import type { SpawnContext } from '../../../src/main/hl/engines/types';

function spawnContext(targetId: string): SpawnContext {
return {
prompt: 'Open example.com',
harnessDir: '/tmp/harness',
sessionId: 'session-123',
targetId,
cdpPort: 9222,
attachmentRefs: [],
};
}

describe('browser harness environment', () => {
it('scopes the REPL port to the assigned target as well as the app session', () => {
const firstTarget = browserHarnessReplPort('session-123', 'target-a');
const secondTarget = browserHarnessReplPort('session-123', 'target-b');

expect(browserHarnessReplPort('session-123', 'target-a')).toBe(firstTarget);
expect(secondTarget).not.toBe(firstTarget);
});

it('gives reruns with a replacement browser target a fresh REPL port', () => {
const firstEnv = applyBrowserHarnessEnv(spawnContext('old-target'), {});
const rerunEnv = applyBrowserHarnessEnv(spawnContext('new-target'), {});

expect(firstEnv.CDP_REPL_PORT).toBe(browserHarnessReplPort('session-123', 'old-target'));
expect(rerunEnv.CDP_REPL_PORT).toBe(browserHarnessReplPort('session-123', 'new-target'));
expect(rerunEnv.CDP_REPL_PORT).not.toBe(firstEnv.CDP_REPL_PORT);
});

it('preserves an explicit REPL port override', () => {
const env = applyBrowserHarnessEnv(spawnContext('target-a'), { CDP_REPL_PORT: '9876' });

expect(env.CDP_REPL_PORT).toBe('9876');
});
});
Loading