Skip to content

Commit 108fffc

Browse files
committed
feat: Neural Link Driven Playwright Integration (#8851)
Resolves #8851, #9782, #9783 - Updated Bridge and ConnectionService to track test clients and App Names. - Created neuralLink test fixture exposing MCP state observation. - Added end-to-end smoke test validating God Mode access.
1 parent e9d8fbb commit 108fffc

5 files changed

Lines changed: 149 additions & 12 deletions

File tree

ai/mcp/server/neural-link/Bridge.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ class Bridge extends Base {
136136

137137
if (role === 'agent') {
138138
this.registerAgent(id, ws);
139+
} else if (role === 'test') {
140+
logger.info(`Bridge: Test client connected [${id}]`);
141+
this.registerAgent(id, ws);
139142
} else {
140143
// Default to app if no role specified (backward compatibility)
141144
this.registerApp(id, ws, appName);

ai/mcp/server/neural-link/services/ConnectionService.mjs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,12 @@ class ConnectionService extends Base {
295295

296296
/**
297297
* @param {String} appWorkerId
298+
* @param {String} [appName='Unknown']
298299
*/
299-
handleAppConnected(appWorkerId) {
300-
logger.info(`App Worker connected: ${appWorkerId}`);
300+
handleAppConnected(appWorkerId, appName = 'Unknown') {
301+
logger.info(`App Worker connected: ${appWorkerId} (${appName})`);
301302
this.sessionData.set(appWorkerId, {
303+
appName,
302304
connectedAt: Date.now(),
303305
logs : [],
304306
sessionId : appWorkerId
@@ -342,7 +344,7 @@ class ConnectionService extends Base {
342344

343345
switch (payload.type) {
344346
case 'app_connected':
345-
this.handleAppConnected(payload.appWorkerId);
347+
this.handleAppConnected(payload.appWorkerId, payload.appName);
346348
break;
347349
case 'app_disconnected':
348350
this.handleAppDisconnected(payload.appWorkerId);
@@ -497,6 +499,40 @@ class ConnectionService extends Base {
497499
setTimeout(resolve, 2000);
498500
});
499501
}
502+
503+
/**
504+
* Waits for a session matching the given ID or AppName to become active.
505+
* @param {String} target The appWorkerId or appName to wait for.
506+
* @param {Number} [timeout=10000] Ms to wait before rejecting.
507+
* @returns {Promise<String>} The matched appWorkerId.
508+
*/
509+
async waitForSession(target, timeout = 10000) {
510+
const check = () => {
511+
for (const [id, meta] of this.sessionData.entries()) {
512+
if (id === target || meta.appName === target) {
513+
return id;
514+
}
515+
}
516+
return null;
517+
};
518+
519+
let found = check();
520+
if (found) return found;
521+
522+
return new Promise((resolve, reject) => {
523+
const startTime = Date.now();
524+
const interval = setInterval(() => {
525+
found = check();
526+
if (found) {
527+
clearInterval(interval);
528+
resolve(found);
529+
} else if (Date.now() - startTime > timeout) {
530+
clearInterval(interval);
531+
reject(new Error(`waitForSession timed out looking for: ${target}`));
532+
}
533+
}, 100);
534+
});
535+
}
500536
}
501537

502538
export default Neo.setupClass(ConnectionService);

resources/content/sandman_handoff.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,9 @@ This file tracks topological conflict alerts generated during overnight REM slee
1717

1818
Based on the latest Tri-Vector Synthesis and Topological Priorities, the following tasks are mathematically recommended as the next immediate focus:
1919

20-
1. **issue-9673**: Score 11.00 (Semantic: 5.00, Structural: 1.00)
20+
1. **issue-9673**: Score 3.54 (Semantic: 1.27, Structural: 1.00)
2121
- *Technical Awareness: Hybrid GraphRAG (Native Edge Graph & App Mapping)*
22-
2. **issue-9637**: Score 11.00 (Semantic: 5.00, Structural: 1.00)
23-
- *Grid Multi-Body: E2E Telemetry Adjustments for Dual-Pipeline Scrolling*
24-
3. **issue-9636**: Score 11.00 (Semantic: 5.00, Structural: 1.00)
25-
- *Grid Multi-Body: Simplify GridDragScroll Scrollbar Hit Detection*
26-
- **[Codebase Gap]** Node `Zombie Ollama Runner Processes`: [DOC_GAP] No documentation or tests found regarding the handling, detection, or cleanup of zombie Ollama runner processes in the provided directory tree. (Source Session: 67b641b1-4354-4bd5-ba22-0ed333a6847b)
27-
- **[Codebase Gap]** Node `Neo.canvas.Base`: [DOC_GAP] The 'Neo.canvas.Base' class is a critical foundation for high-performance canvas renderers (OffscreenCanvas in Workers). While 'learn/guides/advanced/CanvasArchitecture.md' exists and explains the conceptual architecture and a specific implementation (Luminous Flux), it lacks a formal API reference for the 'Neo.canvas.Base' class itself. There is no documentation describing the methods 'initGraph', 'clearGraph', 'updateSize', 'updateMouseState', and hooks like 'onGraphMounted' or 'updateResources'. Developers cannot know the API surface of the base class without reading the source code. Additionally, there is no unit test coverage for the base class in 'test/playwright/unit/'. (Source Session: f971ec44-5ad6-48b9-92e0-790524267d09)
28-
- **[Codebase Gap]** Node `Neo.canvas.Header`: [DOC_GAP] Class `Neo.canvas.Header` implements complex 'Luminous Flux' visual theme, Zero-Allocation geometry buffers, and specific physics for UI diversion (detailed in code comments but not in native documentation). No corresponding doc file exists in `docs/` or `learn/` that explains this architectural approach or the theme's configuration. (Source Session: f971ec44-5ad6-48b9-92e0-790524267d09)
29-
- **[Codebase Gap]** Node `Neo.component.CanvasShared`: [DOC_GAP] The class `Neo.app.SharedCanvas` (implemented in `src/app/SharedCanvas.mjs`) provides a critical abstraction for connecting App Workers to SharedWorker canvas renderers. While `learn/guides/advanced/CanvasArchitecture.md` discusses the high-level architecture and uses a conceptual `HeaderCanvas` example, there is no API reference or developer guide specifically for the `Neo.app.SharedCanvas` base class itself. Developers need documentation on its required config properties (`rendererClassName`, `rendererImportPath`) and its lifecycle methods to properly implement their own SharedWorker-backed components. (Source Session: f971ec44-5ad6-48b9-92e0-790524267d09)
22+
2. **issue-9299**: Score 3.40 (Semantic: 1.20, Structural: 1.00)
23+
- *Implement Agent Self-Discovery via Neural Link Introspection*
24+
3. **issue-9298**: Score 3.39 (Semantic: 1.19, Structural: 1.00)
25+
- *Implement Moltbook Demo Agent using Chrome DevTools MCP*
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { test, expect } from '../fixtures.mjs';
2+
3+
test.describe('Neural Link Driven Playwright Integration', () => {
4+
// Shared Worker environments can take time to spin up
5+
test.setTimeout(90000);
6+
7+
test('God Mode access to internal worker state', async ({ page, neuralLink }) => {
8+
console.log('[NeuralLink.spec.mjs] Loading Portal App...');
9+
// 1. Setup - Let Playwright load the actual App UI
10+
await page.goto('/apps/portal/');
11+
12+
// 2. Connect via Neural Link SDK (bypassing normal DOM observation)
13+
console.log('[NeuralLink.spec.mjs] Requesting SDK connection...');
14+
const app = await neuralLink.connectToApp('Portal');
15+
16+
console.log(`[NeuralLink.spec.mjs] Connected to App Session ID: ${app.sessionId}`);
17+
18+
// Get the dynamic ID of the Viewport to avoid guessing "neo-viewport-1"
19+
await page.waitForSelector('.neo-viewport', { timeout: 30000 });
20+
const viewportId = await page.evaluate(() => document.querySelector('.neo-viewport').id);
21+
22+
// 3. Inspect internal structural state directly from worker memory
23+
const rootProps = await app.getComponent(viewportId);
24+
25+
// Assertions against the internal truth, not the DOM snapshot
26+
expect(rootProps).toBeTruthy();
27+
expect(rootProps.ntype).toBe('viewport');
28+
expect(rootProps.windowId).toBeDefined();
29+
30+
// 4. Test Mutations (Write Access)
31+
// Adjust the app internal configuration values live and instantly assert Playwright recognizes the DOM change
32+
await app.setProperties(viewportId, { style: { backgroundColor: 'rgb(123, 12, 123)' } });
33+
34+
// Verify the reactive system pumped the virtual DOM update instantly
35+
await expect(page.locator('.neo-viewport').first()).toHaveCSS('background-color', 'rgb(123, 12, 123)');
36+
});
37+
});

test/playwright/fixtures.mjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { test as base, expect } from '@playwright/test';
22
import * as RmaHelpers from './util/RmaHelpers.mjs';
3+
import {
4+
NeuralLink_ConnectionService,
5+
NeuralLink_InstanceService,
6+
NeuralLink_DataService,
7+
NeuralLink_RuntimeService
8+
} from '../../ai/services.mjs';
39

410
export const test = base.extend({
511
neo: async ({ page }, use) => {
@@ -59,6 +65,65 @@ export const test = base.extend({
5965
};
6066

6167
await use(neo);
68+
},
69+
70+
neuralLink: async ({ page }, use) => {
71+
// 1. Ensure Bridge connects
72+
await NeuralLink_ConnectionService.manageConnection({action: 'start'});
73+
74+
// 2. Define the Neural Link Fixture object
75+
const nl = {
76+
/**
77+
* Waits for the Test's specific App Worker to connect to the Bridge and returns an SDK wrapper.
78+
* @param {String} [appName] Optional explicitly named app to wait for. Mostly we wait for this specific page's workerId.
79+
*/
80+
async connectToApp(appName) {
81+
let inferredAppName = null;
82+
83+
try {
84+
// Give the page a moment to initialize Neo
85+
await page.waitForFunction(() => window.Neo && window.Neo.config, { timeout: 2000 }).catch(() => {});
86+
inferredAppName = await page.evaluate(() => {
87+
const path = window.Neo?.config?.appPath;
88+
return path ? path.split('/').slice(-2, -1)[0] : null;
89+
});
90+
} catch (e) {
91+
// Ignore, page might not exist or be blank
92+
}
93+
94+
// If user passed 'Portal', prefer that, otherwise use the inferred appName like 'portal'
95+
const targetId = appName || inferredAppName;
96+
if (!targetId) {
97+
throw new Error('neuralLink.connectToApp requires either an initialized Neo environment or an explicit appName to wait for.');
98+
}
99+
100+
// Lowercase for the connection service match
101+
const sessionId = await NeuralLink_ConnectionService.waitForSession(targetId.toLowerCase());
102+
103+
return {
104+
sessionId,
105+
106+
async getComponent(id) {
107+
const response = await NeuralLink_InstanceService.getInstanceProperties({ sessionId, id, properties: ['ntype', 'windowId', 'cls', 'className', 'vnode'] });
108+
return response.properties;
109+
},
110+
111+
async getStore(storeId) {
112+
return NeuralLink_DataService.inspectStore({ sessionId, storeId });
113+
},
114+
115+
async callMethod(id, method, args = []) {
116+
return NeuralLink_RuntimeService.callMethod({ sessionId, id, method, args });
117+
},
118+
119+
async setProperties(id, properties) {
120+
return NeuralLink_InstanceService.setInstanceProperties({ sessionId, id, properties });
121+
}
122+
};
123+
}
124+
};
125+
126+
await use(nl);
62127
}
63128
});
64129

0 commit comments

Comments
 (0)