@harness-fe/runtime 4.0.0-next.0 → 4.0.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +2 -2
- package/dist/dashboardUrl.d.ts +7 -10
- package/dist/dashboardUrl.js +3 -7
- package/dist/index.js +1 -2
- package/dist/overlay.js +4 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +8 -6
- package/src/client.ts +2 -2
- package/src/dashboardUrl.test.ts +16 -35
- package/src/dashboardUrl.ts +10 -17
- package/src/index.ts +1 -3
- package/src/overlay.test.ts +8 -5
- package/src/overlay.ts +4 -0
- package/src/runtimeClient.e2e.test.ts +16 -12
- package/src/version.ts +3 -0
package/dist/client.js
CHANGED
|
@@ -123,7 +123,7 @@ export class RuntimeClient {
|
|
|
123
123
|
this.recorder = new RrwebRecorder((chunk) => this.sendEvent(EVENT_NAME.RRWEB, chunk), { checkoutEveryNms: opts.rrwebCheckoutEveryNms });
|
|
124
124
|
}
|
|
125
125
|
start() {
|
|
126
|
-
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
126
|
+
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
127
127
|
this.ctx.capture.install((name, payload) => this.sendEvent(name, payload), { daemonUrl });
|
|
128
128
|
this.recorder.start();
|
|
129
129
|
this.connect();
|
|
@@ -134,7 +134,7 @@ export class RuntimeClient {
|
|
|
134
134
|
this.ws?.close();
|
|
135
135
|
}
|
|
136
136
|
connect() {
|
|
137
|
-
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
137
|
+
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
138
138
|
try {
|
|
139
139
|
this.ws = new WebSocket(url);
|
|
140
140
|
}
|
package/dist/dashboardUrl.d.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convert the runtime's `mcpUrl` (
|
|
3
|
-
* the
|
|
2
|
+
* Convert the runtime's `mcpUrl` (the gateway WebSocket URL the plugin gave us)
|
|
3
|
+
* into the console URL the same gateway serves: `<http>://<host>:<port>/console`,
|
|
4
|
+
* deep-linking to `/console/sessions/:id` when a sessionId is given.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `packages/mcp-server/src/dashboardSpa.ts`).
|
|
10
|
-
*
|
|
11
|
-
* Optionally deep-links into a session's detail page when `sessionId` is
|
|
12
|
-
* provided.
|
|
6
|
+
* This is a **pure shortcut** — it carries NO token. Opening it is plain
|
|
7
|
+
* navigation; the console authorizes the viewer on its own (admin sign-in or a
|
|
8
|
+
* pasted read token). The runtime's token is write-only and must never be used
|
|
9
|
+
* as a console auth grant, so it's deliberately omitted.
|
|
13
10
|
*/
|
|
14
11
|
export interface DashboardUrlInput {
|
|
15
12
|
mcpUrl: string;
|
package/dist/dashboardUrl.js
CHANGED
|
@@ -10,11 +10,7 @@ export function deriveDashboardUrl(input) {
|
|
|
10
10
|
}
|
|
11
11
|
const httpScheme = url.protocol === 'wss:' ? 'https:' : 'http:';
|
|
12
12
|
const path = input.sessionId
|
|
13
|
-
? `/
|
|
14
|
-
: '/
|
|
15
|
-
|
|
16
|
-
const search = token ? `?token=${encodeURIComponent(token)}` : '';
|
|
17
|
-
// Build manually so we don't leak any extra query/hash from the WS URL
|
|
18
|
-
// (rare, but be defensive — the agent only ever sees what we hand it).
|
|
19
|
-
return `${httpScheme}//${url.host}${path}${search}`;
|
|
13
|
+
? `/console/sessions/${encodeURIComponent(input.sessionId)}`
|
|
14
|
+
: '/console';
|
|
15
|
+
return `${httpScheme}//${url.host}${path}`;
|
|
20
16
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
import { installOverlay } from './overlay.js';
|
|
8
8
|
import { RuntimeClient, readInjectedConfig } from './client.js';
|
|
9
9
|
import { registerOverlayPlugin, drainPluginQueue, } from './pluginRegistry.js';
|
|
10
|
-
|
|
11
|
-
const VERSION = '3.3.0';
|
|
10
|
+
import { VERSION } from './version.js';
|
|
12
11
|
const w = window;
|
|
13
12
|
if (typeof window !== 'undefined' && !w.__harness_fe_started__) {
|
|
14
13
|
w.__harness_fe_started__ = true;
|
package/dist/overlay.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { EVENT_NAME, } from '@harness-fe/protocol';
|
|
17
17
|
import { snapdom } from '@zumer/snapdom';
|
|
18
18
|
import { deriveDashboardUrl } from './dashboardUrl.js';
|
|
19
|
+
import { VERSION } from './version.js';
|
|
19
20
|
import { getCaptureStore } from './capture.js';
|
|
20
21
|
import { collectPageLoadSnapshot } from './snapshot.js';
|
|
21
22
|
import { getOverlayPlugins, subscribeOverlayPlugins, } from './pluginRegistry.js';
|
|
@@ -411,11 +412,13 @@ export function installOverlay(client) {
|
|
|
411
412
|
// ─── Info card rendering ─────────────────────────────────────────────
|
|
412
413
|
const renderInfo = () => {
|
|
413
414
|
const proj = infoCard.querySelector('[data-role=project]');
|
|
415
|
+
const version = infoCard.querySelector('[data-role=version]');
|
|
414
416
|
const build = infoCard.querySelector('[data-role=build]');
|
|
415
417
|
const session = infoCard.querySelector('[data-role=session]');
|
|
416
418
|
const tab = infoCard.querySelector('[data-role=tab]');
|
|
417
419
|
const url = infoCard.querySelector('[data-role=url]');
|
|
418
420
|
proj.textContent = client.displayName ?? client.projectId;
|
|
421
|
+
version.textContent = `v${VERSION}`;
|
|
419
422
|
build.textContent = client.buildId ? abbr(client.buildId) : '—';
|
|
420
423
|
build.title = client.buildId ?? 'No buildId — set HarnessScript buildId prop in prod';
|
|
421
424
|
session.textContent = abbr(client.sessionId);
|
|
@@ -1969,6 +1972,7 @@ function buildInfoCard() {
|
|
|
1969
1972
|
<button class="close-btn" data-role="close" title="Close (Esc)" type="button">×</button>
|
|
1970
1973
|
</div>
|
|
1971
1974
|
<div class="rows">
|
|
1975
|
+
<div class="row"><span class="key">version</span><span class="pill" data-role="version" title="harness runtime version"></span></div>
|
|
1972
1976
|
<div class="row"><span class="key">build</span><span class="pill" data-role="build" title="Click to copy"></span></div>
|
|
1973
1977
|
<div class="row"><span class="key">session</span><span class="pill" data-role="session" title="Click to copy"></span></div>
|
|
1974
1978
|
<div class="row"><span class="key">tab</span><span class="pill" data-role="tab" title="Click to copy"></span></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "4.0.0-next.5";
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-fe/runtime",
|
|
3
|
-
"version": "4.0.0-next.
|
|
3
|
+
"version": "4.0.0-next.5",
|
|
4
4
|
"description": "Browser-side SDK injected into the dev page. Connects to the MCP server via WebSocket and executes commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,21 +30,23 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@zumer/snapdom": "^2.12.0",
|
|
32
32
|
"rrweb": "2.0.0-alpha.4",
|
|
33
|
-
"@harness-fe/protocol": "4.0.0-next.
|
|
33
|
+
"@harness-fe/protocol": "4.0.0-next.4",
|
|
34
34
|
"@harness-fe/sandbox": "^3.2.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"happy-dom": "^20.9.0",
|
|
38
38
|
"typescript": "^5.6.0",
|
|
39
|
-
"vitest": "^2.1.9"
|
|
39
|
+
"vitest": "^2.1.9",
|
|
40
|
+
"@harness-fe/core": "4.0.0-next.5",
|
|
41
|
+
"@harness-fe/gateway": "4.0.0-next.5"
|
|
40
42
|
},
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"access": "public"
|
|
43
45
|
},
|
|
44
46
|
"scripts": {
|
|
45
|
-
"build": "tsc",
|
|
46
|
-
"dev": "tsc --watch --preserveWatchOutput",
|
|
47
|
-
"watch": "tsc --watch --preserveWatchOutput",
|
|
47
|
+
"build": "node scripts/gen-version.mjs && tsc",
|
|
48
|
+
"dev": "node scripts/gen-version.mjs && tsc --watch --preserveWatchOutput",
|
|
49
|
+
"watch": "node scripts/gen-version.mjs && tsc --watch --preserveWatchOutput",
|
|
48
50
|
"typecheck": "tsc --noEmit",
|
|
49
51
|
"test": "vitest run"
|
|
50
52
|
}
|
package/src/client.ts
CHANGED
|
@@ -190,7 +190,7 @@ export class RuntimeClient {
|
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
start(): void {
|
|
193
|
-
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
193
|
+
const daemonUrl = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
194
194
|
this.ctx.capture.install(
|
|
195
195
|
(name, payload) => this.sendEvent(name, payload),
|
|
196
196
|
{ daemonUrl },
|
|
@@ -206,7 +206,7 @@ export class RuntimeClient {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
private connect(): void {
|
|
209
|
-
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}`;
|
|
209
|
+
const url = this.opts.mcpUrl ?? `ws://127.0.0.1:${DEFAULT_WS_PORT}/ws`;
|
|
210
210
|
try {
|
|
211
211
|
this.ws = new WebSocket(url);
|
|
212
212
|
} catch (err) {
|
package/src/dashboardUrl.test.ts
CHANGED
|
@@ -1,55 +1,36 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { deriveDashboardUrl } from './dashboardUrl.js';
|
|
3
3
|
|
|
4
|
-
describe('deriveDashboardUrl', () => {
|
|
5
|
-
it('swaps ws:// to http:// and points at /
|
|
6
|
-
expect(deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729' })).toBe(
|
|
7
|
-
'http://127.0.0.1:47729/
|
|
4
|
+
describe('deriveDashboardUrl (pure shortcut — no token)', () => {
|
|
5
|
+
it('swaps ws:// to http:// and points at /console', () => {
|
|
6
|
+
expect(deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws' })).toBe(
|
|
7
|
+
'http://127.0.0.1:47729/console',
|
|
8
8
|
);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('swaps wss:// to https:// (production / LAN with TLS)', () => {
|
|
12
|
-
expect(deriveDashboardUrl({ mcpUrl: 'wss://harness.lan:47729' })).toBe(
|
|
13
|
-
'https://harness.lan:47729/
|
|
12
|
+
expect(deriveDashboardUrl({ mcpUrl: 'wss://harness.lan:47729/ws' })).toBe(
|
|
13
|
+
'https://harness.lan:47729/console',
|
|
14
14
|
);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it('
|
|
17
|
+
it('deep-links to /console/sessions/:id when sessionId is provided', () => {
|
|
18
18
|
expect(
|
|
19
|
-
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729?token=abc' }),
|
|
20
|
-
).toBe('http://127.0.0.1:47729/
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('URL-encodes the token (defensive against weird HARNESS_FE_TOKEN values)', () => {
|
|
24
|
-
expect(
|
|
25
|
-
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729?token=a%20b%26c' }),
|
|
26
|
-
).toBe('http://127.0.0.1:47729/dashboard/?token=a%20b%26c');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('deep-links to /dashboard/sessions/:id when sessionId is provided', () => {
|
|
30
|
-
expect(
|
|
31
|
-
deriveDashboardUrl({
|
|
32
|
-
mcpUrl: 'ws://127.0.0.1:47729?token=abc',
|
|
33
|
-
sessionId: 'sess-1',
|
|
34
|
-
}),
|
|
35
|
-
).toBe('http://127.0.0.1:47729/dashboard/sessions/sess-1?token=abc');
|
|
19
|
+
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=abc', sessionId: 'sess-1' }),
|
|
20
|
+
).toBe('http://127.0.0.1:47729/console/sessions/sess-1');
|
|
36
21
|
});
|
|
37
22
|
|
|
38
23
|
it('URL-encodes the session id', () => {
|
|
39
24
|
expect(
|
|
40
|
-
deriveDashboardUrl({
|
|
41
|
-
|
|
42
|
-
sessionId: 'a/b c',
|
|
43
|
-
}),
|
|
44
|
-
).toBe('http://127.0.0.1:47729/dashboard/sessions/a%2Fb%20c');
|
|
25
|
+
deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws', sessionId: 'a/b c' }),
|
|
26
|
+
).toBe('http://127.0.0.1:47729/console/sessions/a%2Fb%20c');
|
|
45
27
|
});
|
|
46
28
|
|
|
47
|
-
it('
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
).toBe('http://127.0.0.1:47729/dashboard/?token=abc');
|
|
29
|
+
it('never carries the runtime token — it is a navigation shortcut, not an auth grant', () => {
|
|
30
|
+
const url = deriveDashboardUrl({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=secret&other=x#h', sessionId: 'sess-1' });
|
|
31
|
+
expect(url).toBe('http://127.0.0.1:47729/console/sessions/sess-1');
|
|
32
|
+
expect(url).not.toContain('token');
|
|
33
|
+
expect(url).not.toContain('secret');
|
|
53
34
|
});
|
|
54
35
|
|
|
55
36
|
it('returns undefined for empty or invalid input', () => {
|
package/src/dashboardUrl.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convert the runtime's `mcpUrl` (
|
|
3
|
-
* the
|
|
2
|
+
* Convert the runtime's `mcpUrl` (the gateway WebSocket URL the plugin gave us)
|
|
3
|
+
* into the console URL the same gateway serves: `<http>://<host>:<port>/console`,
|
|
4
|
+
* deep-linking to `/console/sessions/:id` when a sessionId is given.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `packages/mcp-server/src/dashboardSpa.ts`).
|
|
10
|
-
*
|
|
11
|
-
* Optionally deep-links into a session's detail page when `sessionId` is
|
|
12
|
-
* provided.
|
|
6
|
+
* This is a **pure shortcut** — it carries NO token. Opening it is plain
|
|
7
|
+
* navigation; the console authorizes the viewer on its own (admin sign-in or a
|
|
8
|
+
* pasted read token). The runtime's token is write-only and must never be used
|
|
9
|
+
* as a console auth grant, so it's deliberately omitted.
|
|
13
10
|
*/
|
|
14
11
|
export interface DashboardUrlInput {
|
|
15
12
|
mcpUrl: string;
|
|
@@ -26,11 +23,7 @@ export function deriveDashboardUrl(input: DashboardUrlInput): string | undefined
|
|
|
26
23
|
}
|
|
27
24
|
const httpScheme = url.protocol === 'wss:' ? 'https:' : 'http:';
|
|
28
25
|
const path = input.sessionId
|
|
29
|
-
? `/
|
|
30
|
-
: '/
|
|
31
|
-
|
|
32
|
-
const search = token ? `?token=${encodeURIComponent(token)}` : '';
|
|
33
|
-
// Build manually so we don't leak any extra query/hash from the WS URL
|
|
34
|
-
// (rare, but be defensive — the agent only ever sees what we hand it).
|
|
35
|
-
return `${httpScheme}//${url.host}${path}${search}`;
|
|
26
|
+
? `/console/sessions/${encodeURIComponent(input.sessionId)}`
|
|
27
|
+
: '/console';
|
|
28
|
+
return `${httpScheme}//${url.host}${path}`;
|
|
36
29
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,9 +12,7 @@ import {
|
|
|
12
12
|
drainPluginQueue,
|
|
13
13
|
type OverlayPlugin,
|
|
14
14
|
} from './pluginRegistry.js';
|
|
15
|
-
|
|
16
|
-
// Informational; keep in sync with package.json on release.
|
|
17
|
-
const VERSION = '3.3.0';
|
|
15
|
+
import { VERSION } from './version.js';
|
|
18
16
|
|
|
19
17
|
const w = window as unknown as {
|
|
20
18
|
__harness_fe_started__?: boolean;
|
package/src/overlay.test.ts
CHANGED
|
@@ -58,6 +58,8 @@ describe('installOverlay', () => {
|
|
|
58
58
|
const card = root.querySelector('.info-card') as HTMLElement;
|
|
59
59
|
expect(card.style.display).toBe('flex');
|
|
60
60
|
expect(root.querySelector('[data-role=project]')!.textContent).toBe('Demo App');
|
|
61
|
+
// Runtime version surfaced in the card (real value from version.ts).
|
|
62
|
+
expect(root.querySelector('[data-role=version]')!.textContent).toMatch(/^v\d/);
|
|
61
63
|
// Abbreviated to 8 chars
|
|
62
64
|
expect(root.querySelector('[data-role=build]')!.textContent).toBe('build-12');
|
|
63
65
|
expect(root.querySelector('[data-role=session]')!.textContent).toBe('sess-123');
|
|
@@ -218,12 +220,13 @@ describe('installOverlay', () => {
|
|
|
218
220
|
|
|
219
221
|
it('shows the "Open dashboard" button only when the client has an mcpUrl', () => {
|
|
220
222
|
setupDom();
|
|
221
|
-
installOverlay(makeFakeClient({ mcpUrl: 'ws://127.0.0.1:47729?token=demo' }));
|
|
223
|
+
installOverlay(makeFakeClient({ mcpUrl: 'ws://127.0.0.1:47729/ws?token=demo' }));
|
|
222
224
|
const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
|
|
223
225
|
const btn = root.querySelector('[data-role=open-dashboard]') as HTMLButtonElement;
|
|
224
226
|
expect(btn.style.display).toBe('');
|
|
225
|
-
expect(btn.title).toContain('http://127.0.0.1:47729/
|
|
226
|
-
|
|
227
|
+
expect(btn.title).toContain('http://127.0.0.1:47729/console/sessions/');
|
|
228
|
+
// The overlay link is a pure shortcut — it must NOT carry the runtime token.
|
|
229
|
+
expect(btn.title).not.toContain('token=');
|
|
227
230
|
});
|
|
228
231
|
|
|
229
232
|
it('hides the "Open dashboard" button when mcpUrl is missing', () => {
|
|
@@ -236,7 +239,7 @@ describe('installOverlay', () => {
|
|
|
236
239
|
|
|
237
240
|
it('clicking "Open dashboard" calls window.open with the derived URL in a new tab', () => {
|
|
238
241
|
setupDom();
|
|
239
|
-
installOverlay(makeFakeClient({ mcpUrl: 'wss://harness.lan:8443?token=t' }));
|
|
242
|
+
installOverlay(makeFakeClient({ mcpUrl: 'wss://harness.lan:8443/ws?token=t' }));
|
|
240
243
|
const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
|
|
241
244
|
const calls: Array<{ url: string; target: string; features: string }> = [];
|
|
242
245
|
(globalThis.window as unknown as { open: typeof window.open }).open = ((
|
|
@@ -250,7 +253,7 @@ describe('installOverlay', () => {
|
|
|
250
253
|
const btn = root.querySelector('[data-role=open-dashboard]') as HTMLButtonElement;
|
|
251
254
|
btn.click();
|
|
252
255
|
expect(calls).toHaveLength(1);
|
|
253
|
-
expect(calls[0].url).toBe('https://harness.lan:8443/
|
|
256
|
+
expect(calls[0].url).toBe('https://harness.lan:8443/console/sessions/sess-12345-abcdef-9876');
|
|
254
257
|
expect(calls[0].target).toBe('_blank');
|
|
255
258
|
expect(calls[0].features).toMatch(/noopener/);
|
|
256
259
|
});
|
package/src/overlay.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '@harness-fe/protocol';
|
|
25
25
|
import { snapdom } from '@zumer/snapdom';
|
|
26
26
|
import { deriveDashboardUrl } from './dashboardUrl.js';
|
|
27
|
+
import { VERSION } from './version.js';
|
|
27
28
|
import { getCaptureStore } from './capture.js';
|
|
28
29
|
import { collectPageLoadSnapshot } from './snapshot.js';
|
|
29
30
|
import {
|
|
@@ -482,11 +483,13 @@ export function installOverlay(client: OverlayClient): void {
|
|
|
482
483
|
// ─── Info card rendering ─────────────────────────────────────────────
|
|
483
484
|
const renderInfo = () => {
|
|
484
485
|
const proj = infoCard.querySelector<HTMLElement>('[data-role=project]')!;
|
|
486
|
+
const version = infoCard.querySelector<HTMLElement>('[data-role=version]')!;
|
|
485
487
|
const build = infoCard.querySelector<HTMLElement>('[data-role=build]')!;
|
|
486
488
|
const session = infoCard.querySelector<HTMLElement>('[data-role=session]')!;
|
|
487
489
|
const tab = infoCard.querySelector<HTMLElement>('[data-role=tab]')!;
|
|
488
490
|
const url = infoCard.querySelector<HTMLElement>('[data-role=url]')!;
|
|
489
491
|
proj.textContent = client.displayName ?? client.projectId;
|
|
492
|
+
version.textContent = `v${VERSION}`;
|
|
490
493
|
build.textContent = client.buildId ? abbr(client.buildId) : '—';
|
|
491
494
|
build.title = client.buildId ?? 'No buildId — set HarnessScript buildId prop in prod';
|
|
492
495
|
session.textContent = abbr(client.sessionId);
|
|
@@ -2113,6 +2116,7 @@ function buildInfoCard(): HTMLDivElement {
|
|
|
2113
2116
|
<button class="close-btn" data-role="close" title="Close (Esc)" type="button">×</button>
|
|
2114
2117
|
</div>
|
|
2115
2118
|
<div class="rows">
|
|
2119
|
+
<div class="row"><span class="key">version</span><span class="pill" data-role="version" title="harness runtime version"></span></div>
|
|
2116
2120
|
<div class="row"><span class="key">build</span><span class="pill" data-role="build" title="Click to copy"></span></div>
|
|
2117
2121
|
<div class="row"><span class="key">session</span><span class="pill" data-role="session" title="Click to copy"></span></div>
|
|
2118
2122
|
<div class="row"><span class="key">tab</span><span class="pill" data-role="tab" title="Click to copy"></span></div>
|
|
@@ -24,15 +24,15 @@ vi.mock('rrweb', () => ({
|
|
|
24
24
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
25
25
|
import { tmpdir } from 'node:os';
|
|
26
26
|
import { join } from 'node:path';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { InProcessCoreClient, JsonlStore, type StoreEvent } from '@harness-fe/core';
|
|
28
|
+
import { createGateway, Policy, type GatewayHandle } from '@harness-fe/gateway';
|
|
29
29
|
import { RuntimeClient } from './client.js';
|
|
30
30
|
import { getCaptureStore } from './capture.js';
|
|
31
|
-
import type { StoreEvent } from '../../mcp-server/src/store/index.js';
|
|
32
31
|
import type { NetworkEntry, StorageEntry, WsEntry } from '@harness-fe/protocol';
|
|
33
32
|
|
|
34
33
|
interface Env {
|
|
35
|
-
|
|
34
|
+
core: InProcessCoreClient;
|
|
35
|
+
gw: GatewayHandle;
|
|
36
36
|
store: JsonlStore;
|
|
37
37
|
dir: string;
|
|
38
38
|
port: number;
|
|
@@ -59,9 +59,12 @@ async function rmDirWithRetry(dir: string, attempts = 5): Promise<void> {
|
|
|
59
59
|
async function setup(): Promise<Env> {
|
|
60
60
|
const dir = mkdtempSync(join(tmpdir(), 'harness-rt-e2e-'));
|
|
61
61
|
const store = new JsonlStore(dir);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
62
|
+
// New architecture: an in-process core behind the gateway front door; the
|
|
63
|
+
// runtime connects to the gateway's /ws (Open policy → local principal).
|
|
64
|
+
const core = new InProcessCoreClient({ store, taskStore: null, autoPurge: { enabled: false } });
|
|
65
|
+
await core.start();
|
|
66
|
+
const gw = createGateway({ coreClient: core, policy: new Policy({ mode: 'open' }) });
|
|
67
|
+
const port = await gw.listen(0, '127.0.0.1');
|
|
65
68
|
if (!port) throw new Error('no port');
|
|
66
69
|
|
|
67
70
|
// happy-dom keeps singletons across tests — reset the patch state so this
|
|
@@ -70,7 +73,7 @@ async function setup(): Promise<Env> {
|
|
|
70
73
|
|
|
71
74
|
const client = new RuntimeClient({
|
|
72
75
|
projectId: 'rt-e2e',
|
|
73
|
-
mcpUrl: `ws://127.0.0.1:${port}`,
|
|
76
|
+
mcpUrl: `ws://127.0.0.1:${port}/ws`,
|
|
74
77
|
});
|
|
75
78
|
client.start();
|
|
76
79
|
|
|
@@ -88,7 +91,7 @@ async function setup(): Promise<Env> {
|
|
|
88
91
|
throw new Error('runtime-client never connected');
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
return {
|
|
94
|
+
return { core, gw, store, dir, port, client, sessionId: client.sessionId };
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
beforeEach(async () => {
|
|
@@ -98,7 +101,8 @@ beforeEach(async () => {
|
|
|
98
101
|
afterEach(async () => {
|
|
99
102
|
if (!env) return;
|
|
100
103
|
env.client.stop();
|
|
101
|
-
await env.
|
|
104
|
+
await env.gw.close();
|
|
105
|
+
await env.core.stop();
|
|
102
106
|
// close() drains the async write queue — must await, else rmSync races
|
|
103
107
|
// file writes and the dir-recursive-rm trips ENOTEMPTY on Linux CI.
|
|
104
108
|
await env.store.close();
|
|
@@ -186,7 +190,7 @@ describe('RuntimeClient E2E — patched WebSocket flows to bridge', () => {
|
|
|
186
190
|
(window as unknown as { WebSocket: typeof WebSocket }).WebSocket = FakeWS as unknown as typeof WebSocket;
|
|
187
191
|
cap.install(
|
|
188
192
|
(name, payload) => e.client.sendEvent(name, payload),
|
|
189
|
-
{ daemonUrl: `ws://127.0.0.1:${e.port}` },
|
|
193
|
+
{ daemonUrl: `ws://127.0.0.1:${e.port}/ws` },
|
|
190
194
|
);
|
|
191
195
|
|
|
192
196
|
try {
|
|
@@ -245,7 +249,7 @@ describe('RuntimeClient E2E — patched fetch initiator round-trip', () => {
|
|
|
245
249
|
cap.dispose();
|
|
246
250
|
cap.install(
|
|
247
251
|
(name, payload) => e.client.sendEvent(name, payload),
|
|
248
|
-
{ daemonUrl: `ws://127.0.0.1:${e.port}` },
|
|
252
|
+
{ daemonUrl: `ws://127.0.0.1:${e.port}/ws` },
|
|
249
253
|
);
|
|
250
254
|
|
|
251
255
|
try {
|
package/src/version.ts
ADDED