upfynai-code 3.0.4 → 3.2.0
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/README.md +69 -92
- package/bin/cli.js +191 -0
- package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
- package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
- package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
- package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
- package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
- package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
- package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
- package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
- package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
- package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
- package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
- package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
- package/dist/client/assets/index-CnNNzw9A.css +1 -0
- package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
- package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
- package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
- package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
- package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
- package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
- package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
- package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
- package/dist/client/favicon.png +0 -0
- package/dist/client/icons/icon-128x128.png +0 -0
- package/dist/client/icons/icon-144x144.png +0 -0
- package/dist/client/icons/icon-152x152.png +0 -0
- package/dist/client/icons/icon-192x192.png +0 -0
- package/dist/client/icons/icon-384x384.png +0 -0
- package/dist/client/icons/icon-512x512.png +0 -0
- package/dist/client/icons/icon-72x72.png +0 -0
- package/dist/client/icons/icon-96x96.png +0 -0
- package/{client/dist → dist/client}/index.html +37 -36
- package/dist/client/logo-128.png +0 -0
- package/dist/client/logo-256.png +0 -0
- package/dist/client/logo-32.png +0 -0
- package/dist/client/logo-512.png +0 -0
- package/dist/client/logo-64.png +0 -0
- package/dist/client/logo.png +0 -0
- package/{client/dist → dist/client}/manifest.json +12 -12
- package/{client/dist → dist/client}/mcp-docs.html +1 -1
- package/{client/dist → dist/client}/sw.js +2 -2
- package/package.json +56 -105
- package/scripts/postinstall.js +9 -0
- package/scripts/prepublish.js +77 -0
- package/src/animation.js +228 -0
- package/src/auth.js +142 -0
- package/src/config.js +40 -0
- package/src/connect.js +416 -0
- package/src/launch.js +81 -0
- package/src/mcp.js +57 -0
- package/src/permissions.js +140 -0
- package/src/persistent-shell.js +261 -0
- package/src/server.js +54 -0
- package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
- package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
- package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
- package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
- package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
- package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
- package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
- package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
- package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
- package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
- package/client/dist/assets/index-46kkVu2i.css +0 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
- package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
- package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
- package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
- package/client/dist/favicon.png +0 -0
- package/client/dist/icons/icon-128x128.png +0 -0
- package/client/dist/icons/icon-144x144.png +0 -0
- package/client/dist/icons/icon-152x152.png +0 -0
- package/client/dist/icons/icon-192x192.png +0 -0
- package/client/dist/icons/icon-384x384.png +0 -0
- package/client/dist/icons/icon-512x512.png +0 -0
- package/client/dist/icons/icon-72x72.png +0 -0
- package/client/dist/icons/icon-96x96.png +0 -0
- package/client/dist/logo-128.png +0 -0
- package/client/dist/logo-256.png +0 -0
- package/client/dist/logo-32.png +0 -0
- package/client/dist/logo-512.png +0 -0
- package/client/dist/logo-64.png +0 -0
- package/commands/upfynai-connect.md +0 -59
- package/commands/upfynai-disconnect.md +0 -31
- package/commands/upfynai-doctor.md +0 -99
- package/commands/upfynai-export.md +0 -49
- package/commands/upfynai-local.md +0 -82
- package/commands/upfynai-status.md +0 -75
- package/commands/upfynai-stop.md +0 -49
- package/commands/upfynai-uninstall.md +0 -58
- package/commands/upfynai.md +0 -69
- package/scripts/build-client.js +0 -17
- package/scripts/fix-node-pty.js +0 -67
- package/scripts/install-commands.js +0 -78
- package/server/agent-loop.js +0 -242
- package/server/auto-compact.js +0 -99
- package/server/browser.js +0 -131
- package/server/claude-sdk.js +0 -797
- package/server/cli-ui.js +0 -798
- package/server/cli.js +0 -751
- package/server/constants/config.js +0 -31
- package/server/cursor-cli.js +0 -270
- package/server/database/auth.db +0 -0
- package/server/database/db.js +0 -1547
- package/server/database/init.sql +0 -70
- package/server/index.js +0 -3813
- package/server/load-env.js +0 -26
- package/server/mcp-server.js +0 -621
- package/server/middleware/auth.js +0 -184
- package/server/middleware/relayHelpers.js +0 -44
- package/server/middleware/sandboxRouter.js +0 -174
- package/server/openai-codex.js +0 -403
- package/server/openrouter.js +0 -137
- package/server/projects.js +0 -1807
- package/server/provider-factory.js +0 -174
- package/server/relay-client.js +0 -390
- package/server/routes/agent.js +0 -1234
- package/server/routes/auth.js +0 -559
- package/server/routes/browser.js +0 -419
- package/server/routes/canvas.js +0 -53
- package/server/routes/cli-auth.js +0 -263
- package/server/routes/codex.js +0 -396
- package/server/routes/commands.js +0 -707
- package/server/routes/composio.js +0 -176
- package/server/routes/cursor.js +0 -770
- package/server/routes/dashboard.js +0 -295
- package/server/routes/git.js +0 -1208
- package/server/routes/keys.js +0 -34
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -661
- package/server/routes/payments.js +0 -227
- package/server/routes/projects.js +0 -754
- package/server/routes/sessions.js +0 -146
- package/server/routes/settings.js +0 -261
- package/server/routes/taskmaster.js +0 -1928
- package/server/routes/user.js +0 -106
- package/server/routes/vapi-chat.js +0 -624
- package/server/routes/voice.js +0 -235
- package/server/routes/webhooks.js +0 -166
- package/server/routes/workflows.js +0 -312
- package/server/sandbox.js +0 -120
- package/server/services/browser-ai.js +0 -154
- package/server/services/composio.js +0 -204
- package/server/services/sessionRegistry.js +0 -139
- package/server/services/whisperService.js +0 -84
- package/server/services/workflowScheduler.js +0 -211
- package/server/tests/relay-flow.test.js +0 -570
- package/server/tests/sessions.test.js +0 -259
- package/server/utils/commandParser.js +0 -303
- package/server/utils/email.js +0 -66
- package/server/utils/gitConfig.js +0 -24
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
- package/shared/integrationCatalog.d.ts +0 -12
- package/shared/integrationCatalog.js +0 -172
- package/shared/modelConstants.js +0 -96
- /package/{shared → dist}/agents/claude.js +0 -0
- /package/{shared → dist}/agents/codex.js +0 -0
- /package/{shared → dist}/agents/cursor.js +0 -0
- /package/{shared → dist}/agents/detect.js +0 -0
- /package/{shared → dist}/agents/exec.js +0 -0
- /package/{shared → dist}/agents/files.js +0 -0
- /package/{shared → dist}/agents/git.js +0 -0
- /package/{shared → dist}/agents/gitagent.js +0 -0
- /package/{shared → dist}/agents/index.js +0 -0
- /package/{shared → dist}/agents/shell.js +0 -0
- /package/{shared → dist}/agents/utils.js +0 -0
- /package/{client/dist → dist/client}/api-docs.html +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
- /package/{client/dist → dist/client}/clear-cache.html +0 -0
- /package/{client/dist → dist/client}/convert-icons.md +0 -0
- /package/{client/dist → dist/client}/favicon.svg +0 -0
- /package/{client/dist → dist/client}/generate-icons.js +0 -0
- /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
- /package/{client/dist → dist/client}/logo.svg +0 -0
- /package/{client/dist → dist/client}/offline.html +0 -0
- /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
- /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
- /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
- /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
- /package/{shared → dist}/gitagent/index.js +0 -0
- /package/{shared → dist}/gitagent/parser.js +0 -0
- /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* End-to-end tests for Relay AI Chat Flow & Multi-User Isolation
|
|
3
|
-
*
|
|
4
|
-
* Tests verify:
|
|
5
|
-
* 1. Relay message routing (UI → Backend → CLI → AI → Response)
|
|
6
|
-
* 2. Streaming event translation (relay-stream → content_block_delta)
|
|
7
|
-
* 3. Multi-user isolation (broadcasts, sessions, projects, relay connections)
|
|
8
|
-
* 4. Session locking and ownership
|
|
9
|
-
* 5. Agent abstraction (executeAction dispatch)
|
|
10
|
-
*
|
|
11
|
-
* Run: node --test backend/server/tests/relay-flow.test.js
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it } from 'node:test';
|
|
15
|
-
import assert from 'node:assert/strict';
|
|
16
|
-
|
|
17
|
-
// ── Test 1: Relay Connection Isolation ──
|
|
18
|
-
|
|
19
|
-
describe('Relay Connection Isolation', () => {
|
|
20
|
-
it('relayConnections map keys by userId — different users get separate entries', () => {
|
|
21
|
-
const relayConnections = new Map();
|
|
22
|
-
|
|
23
|
-
// User A connects
|
|
24
|
-
relayConnections.set(1, { ws: { readyState: 1 }, user: { userId: 1, username: 'userA' }, connectedAt: Date.now() });
|
|
25
|
-
// User B connects
|
|
26
|
-
relayConnections.set(2, { ws: { readyState: 1 }, user: { userId: 2, username: 'userB' }, connectedAt: Date.now() });
|
|
27
|
-
|
|
28
|
-
assert.equal(relayConnections.size, 2, 'Two separate relay connections');
|
|
29
|
-
assert.equal(relayConnections.get(1).user.username, 'userA');
|
|
30
|
-
assert.equal(relayConnections.get(2).user.username, 'userB');
|
|
31
|
-
|
|
32
|
-
// User A's lookup cannot access User B's relay
|
|
33
|
-
const relayA = relayConnections.get(1);
|
|
34
|
-
const relayB = relayConnections.get(2);
|
|
35
|
-
assert.notEqual(relayA.ws, relayB.ws, 'Different users have different WebSocket connections');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('hasActiveRelay returns true only for the correct user', () => {
|
|
39
|
-
const relayConnections = new Map();
|
|
40
|
-
relayConnections.set(1, { ws: { readyState: 1 } });
|
|
41
|
-
relayConnections.set(2, { ws: { readyState: 3 } }); // User 2 disconnected (readyState=3)
|
|
42
|
-
|
|
43
|
-
function hasActiveRelay(userId) {
|
|
44
|
-
if (!userId) return false;
|
|
45
|
-
const relay = relayConnections.get(Number(userId));
|
|
46
|
-
return relay && relay.ws.readyState === 1;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
assert.equal(hasActiveRelay(1), true, 'User 1 has active relay');
|
|
50
|
-
assert.ok(!hasActiveRelay(2), 'User 2 relay is disconnected');
|
|
51
|
-
assert.ok(!hasActiveRelay(3), 'User 3 has no relay');
|
|
52
|
-
assert.ok(!hasActiveRelay(null), 'Null userId returns falsy');
|
|
53
|
-
assert.ok(!hasActiveRelay(undefined), 'Undefined userId returns falsy');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('sendRelayCommand targets only the correct userId relay', () => {
|
|
57
|
-
const relayConnections = new Map();
|
|
58
|
-
const sentMessages = { user1: [], user2: [] };
|
|
59
|
-
|
|
60
|
-
relayConnections.set(1, {
|
|
61
|
-
ws: {
|
|
62
|
-
readyState: 1,
|
|
63
|
-
send: (msg) => sentMessages.user1.push(JSON.parse(msg)),
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
relayConnections.set(2, {
|
|
67
|
-
ws: {
|
|
68
|
-
readyState: 1,
|
|
69
|
-
send: (msg) => sentMessages.user2.push(JSON.parse(msg)),
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Send command to user 1's relay
|
|
74
|
-
const relay = relayConnections.get(1);
|
|
75
|
-
relay.ws.send(JSON.stringify({ type: 'relay-command', action: 'claude-query', command: 'hello' }));
|
|
76
|
-
|
|
77
|
-
assert.equal(sentMessages.user1.length, 1, 'User 1 received the command');
|
|
78
|
-
assert.equal(sentMessages.user2.length, 0, 'User 2 did NOT receive the command');
|
|
79
|
-
assert.equal(sentMessages.user1[0].action, 'claude-query');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ── Test 2: Broadcast User Filtering ──
|
|
84
|
-
|
|
85
|
-
describe('Broadcast User Filtering', () => {
|
|
86
|
-
it('relay-status broadcast only reaches clients with matching userId', () => {
|
|
87
|
-
const connectedClients = new Set();
|
|
88
|
-
const received = { userA: [], userB: [], noAuth: [] };
|
|
89
|
-
|
|
90
|
-
const clientA = {
|
|
91
|
-
readyState: 1,
|
|
92
|
-
_wsUser: { userId: 1 },
|
|
93
|
-
send: (msg) => received.userA.push(JSON.parse(msg)),
|
|
94
|
-
};
|
|
95
|
-
const clientB = {
|
|
96
|
-
readyState: 1,
|
|
97
|
-
_wsUser: { userId: 2 },
|
|
98
|
-
send: (msg) => received.userB.push(JSON.parse(msg)),
|
|
99
|
-
};
|
|
100
|
-
const clientNoAuth = {
|
|
101
|
-
readyState: 1,
|
|
102
|
-
_wsUser: null,
|
|
103
|
-
send: (msg) => received.noAuth.push(JSON.parse(msg)),
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
connectedClients.add(clientA);
|
|
107
|
-
connectedClients.add(clientB);
|
|
108
|
-
connectedClients.add(clientNoAuth);
|
|
109
|
-
|
|
110
|
-
// Broadcast relay-status for userId=1
|
|
111
|
-
const userId = 1;
|
|
112
|
-
for (const client of connectedClients) {
|
|
113
|
-
try {
|
|
114
|
-
if (client.readyState === 1 && client._wsUser?.userId === userId) {
|
|
115
|
-
client.send(JSON.stringify({ type: 'relay-status', userId, connected: true }));
|
|
116
|
-
}
|
|
117
|
-
} catch { /* ignore */ }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
assert.equal(received.userA.length, 1, 'User A received the broadcast');
|
|
121
|
-
assert.equal(received.userB.length, 0, 'User B did NOT receive it');
|
|
122
|
-
assert.equal(received.noAuth.length, 0, 'Unauthenticated client did NOT receive it');
|
|
123
|
-
assert.equal(received.userA[0].type, 'relay-status');
|
|
124
|
-
assert.equal(received.userA[0].connected, true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('projects_updated broadcast only sends user-owned projects', () => {
|
|
128
|
-
const connectedClients = new Set();
|
|
129
|
-
const received = { userA: [], userB: [] };
|
|
130
|
-
|
|
131
|
-
const clientA = {
|
|
132
|
-
readyState: 1,
|
|
133
|
-
_wsUser: { userId: 1 },
|
|
134
|
-
send: (msg) => received.userA.push(JSON.parse(msg)),
|
|
135
|
-
};
|
|
136
|
-
const clientB = {
|
|
137
|
-
readyState: 1,
|
|
138
|
-
_wsUser: { userId: 2 },
|
|
139
|
-
send: (msg) => received.userB.push(JSON.parse(msg)),
|
|
140
|
-
};
|
|
141
|
-
connectedClients.add(clientA);
|
|
142
|
-
connectedClients.add(clientB);
|
|
143
|
-
|
|
144
|
-
// Simulate per-user project lists
|
|
145
|
-
const userProjects = {
|
|
146
|
-
1: [{ name: 'projectA' }],
|
|
147
|
-
2: [{ name: 'projectB' }],
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
for (const client of connectedClients) {
|
|
151
|
-
if (client.readyState !== 1) continue;
|
|
152
|
-
const uid = client._wsUser?.userId;
|
|
153
|
-
if (uid && userProjects[uid]) {
|
|
154
|
-
client.send(JSON.stringify({ type: 'projects_updated', projects: userProjects[uid] }));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
assert.equal(received.userA.length, 1);
|
|
159
|
-
assert.equal(received.userB.length, 1);
|
|
160
|
-
assert.equal(received.userA[0].projects[0].name, 'projectA', 'User A sees only projectA');
|
|
161
|
-
assert.equal(received.userB[0].projects[0].name, 'projectB', 'User B sees only projectB');
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ── Test 3: Streaming Event Translation ──
|
|
166
|
-
|
|
167
|
-
describe('Streaming Event Translation', () => {
|
|
168
|
-
it('relay-stream with claude-response translates to content_block_delta', () => {
|
|
169
|
-
const frontendMessages = [];
|
|
170
|
-
const writer = {
|
|
171
|
-
send: (msg) => frontendMessages.push(msg),
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// Simulate onStream callback from routeViaRelay
|
|
175
|
-
const responseType = 'claude-response';
|
|
176
|
-
const sessionId = 'session-123';
|
|
177
|
-
let fullContent = '';
|
|
178
|
-
|
|
179
|
-
function onStream(streamData) {
|
|
180
|
-
if (streamData.type === 'claude-response') {
|
|
181
|
-
const chunk = streamData.content || '';
|
|
182
|
-
if (chunk) {
|
|
183
|
-
fullContent += chunk;
|
|
184
|
-
writer.send({
|
|
185
|
-
type: responseType,
|
|
186
|
-
data: { type: 'content_block_delta', delta: { text: chunk } },
|
|
187
|
-
sessionId,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Simulate streaming chunks
|
|
194
|
-
onStream({ type: 'claude-response', content: 'Hello' });
|
|
195
|
-
onStream({ type: 'claude-response', content: ', I am Claude.' });
|
|
196
|
-
onStream({ type: 'claude-response', content: ' How can I help?' });
|
|
197
|
-
|
|
198
|
-
assert.equal(frontendMessages.length, 3, 'Three streaming events sent to frontend');
|
|
199
|
-
assert.equal(frontendMessages[0].data.type, 'content_block_delta');
|
|
200
|
-
assert.equal(frontendMessages[0].data.delta.text, 'Hello');
|
|
201
|
-
assert.equal(frontendMessages[1].data.delta.text, ', I am Claude.');
|
|
202
|
-
assert.equal(frontendMessages[2].data.delta.text, ' How can I help?');
|
|
203
|
-
assert.equal(fullContent, 'Hello, I am Claude. How can I help?', 'Full content accumulated correctly');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('claude-system event translates to relay-session-id', () => {
|
|
207
|
-
const frontendMessages = [];
|
|
208
|
-
const writer = { send: (msg) => frontendMessages.push(msg) };
|
|
209
|
-
const sessionId = 'session-456';
|
|
210
|
-
let capturedCliSessionId = null;
|
|
211
|
-
|
|
212
|
-
function onStream(streamData) {
|
|
213
|
-
if (streamData.type === 'claude-system' && streamData.sessionId) {
|
|
214
|
-
capturedCliSessionId = streamData.sessionId;
|
|
215
|
-
writer.send({
|
|
216
|
-
type: 'relay-session-id',
|
|
217
|
-
sessionId,
|
|
218
|
-
cliSessionId: capturedCliSessionId,
|
|
219
|
-
model: streamData.model,
|
|
220
|
-
cwd: streamData.cwd,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
onStream({ type: 'claude-system', sessionId: 'cli-sess-789', model: 'claude-sonnet-4-6', cwd: '/home/user/project' });
|
|
226
|
-
|
|
227
|
-
assert.equal(frontendMessages.length, 1);
|
|
228
|
-
assert.equal(frontendMessages[0].type, 'relay-session-id');
|
|
229
|
-
assert.equal(frontendMessages[0].cliSessionId, 'cli-sess-789');
|
|
230
|
-
assert.equal(frontendMessages[0].model, 'claude-sonnet-4-6');
|
|
231
|
-
assert.equal(capturedCliSessionId, 'cli-sess-789');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('completion event includes viaRelay: true', () => {
|
|
235
|
-
const frontendMessages = [];
|
|
236
|
-
const writer = { send: (msg) => frontendMessages.push(msg) };
|
|
237
|
-
|
|
238
|
-
// Simulate routeViaRelay completion
|
|
239
|
-
writer.send({
|
|
240
|
-
type: 'claude-complete',
|
|
241
|
-
sessionId: 'session-123',
|
|
242
|
-
cliSessionId: 'cli-sess-456',
|
|
243
|
-
exitCode: 0,
|
|
244
|
-
isNewSession: true,
|
|
245
|
-
viaRelay: true,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
assert.equal(frontendMessages[0].type, 'claude-complete');
|
|
249
|
-
assert.equal(frontendMessages[0].viaRelay, true, 'Must indicate response came via relay');
|
|
250
|
-
assert.equal(frontendMessages[0].exitCode, 0);
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// ── Test 4: Session Ownership Isolation ──
|
|
255
|
-
|
|
256
|
-
describe('Session Ownership Isolation', () => {
|
|
257
|
-
it('sessionOwners map enforces user isolation', () => {
|
|
258
|
-
const sessionOwners = new Map();
|
|
259
|
-
|
|
260
|
-
// User 1 creates a session
|
|
261
|
-
sessionOwners.set('session-A', 1);
|
|
262
|
-
// User 2 creates a session
|
|
263
|
-
sessionOwners.set('session-B', 2);
|
|
264
|
-
|
|
265
|
-
// Simulate getUserSessions filter
|
|
266
|
-
function getUserSessions(userId, allSessions) {
|
|
267
|
-
return allSessions.filter(s => {
|
|
268
|
-
const owner = sessionOwners.get(s.sessionId);
|
|
269
|
-
return !owner || owner === userId;
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const allSessions = [
|
|
274
|
-
{ sessionId: 'session-A', provider: 'claude' },
|
|
275
|
-
{ sessionId: 'session-B', provider: 'claude' },
|
|
276
|
-
{ sessionId: 'session-C', provider: 'cursor' }, // unowned
|
|
277
|
-
];
|
|
278
|
-
|
|
279
|
-
const user1Sessions = getUserSessions(1, allSessions);
|
|
280
|
-
const user2Sessions = getUserSessions(2, allSessions);
|
|
281
|
-
|
|
282
|
-
assert.equal(user1Sessions.length, 2, 'User 1 sees session-A + unowned session-C');
|
|
283
|
-
assert.ok(user1Sessions.some(s => s.sessionId === 'session-A'), 'User 1 sees their own session');
|
|
284
|
-
assert.ok(!user1Sessions.some(s => s.sessionId === 'session-B'), 'User 1 cannot see User 2 session');
|
|
285
|
-
|
|
286
|
-
assert.equal(user2Sessions.length, 2, 'User 2 sees session-B + unowned session-C');
|
|
287
|
-
assert.ok(user2Sessions.some(s => s.sessionId === 'session-B'), 'User 2 sees their own session');
|
|
288
|
-
assert.ok(!user2Sessions.some(s => s.sessionId === 'session-A'), 'User 2 cannot see User 1 session');
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('abort rejects when user does not own the session', () => {
|
|
292
|
-
const sessionOwners = new Map();
|
|
293
|
-
sessionOwners.set('session-X', 1); // owned by user 1
|
|
294
|
-
|
|
295
|
-
const userId = 2; // user 2 tries to abort
|
|
296
|
-
const owner = sessionOwners.get('session-X');
|
|
297
|
-
const canAbort = !owner || owner === userId;
|
|
298
|
-
|
|
299
|
-
assert.equal(canAbort, false, 'User 2 cannot abort User 1 session');
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// ── Test 5: Agent Action Dispatch ──
|
|
304
|
-
|
|
305
|
-
describe('Agent Action Dispatch', () => {
|
|
306
|
-
it('executeAction routes to correct handler', async () => {
|
|
307
|
-
const callLog = [];
|
|
308
|
-
const actionMap = new Map([
|
|
309
|
-
['claude-query', async (params) => { callLog.push('claude'); return { exitCode: 0 }; }],
|
|
310
|
-
['cursor-query', async (params) => { callLog.push('cursor'); return { exitCode: 0 }; }],
|
|
311
|
-
['codex-query', async (params) => { callLog.push('codex'); return { exitCode: 0 }; }],
|
|
312
|
-
['shell-exec', async (params) => { callLog.push('shell'); return { output: 'hello' }; }],
|
|
313
|
-
]);
|
|
314
|
-
|
|
315
|
-
async function executeAction(action, params) {
|
|
316
|
-
const handler = actionMap.get(action);
|
|
317
|
-
if (!handler) throw new Error(`Unknown action: ${action}`);
|
|
318
|
-
return handler(params);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
await executeAction('claude-query', { command: 'hey' });
|
|
322
|
-
await executeAction('cursor-query', { command: 'hey' });
|
|
323
|
-
await executeAction('shell-exec', { command: 'ls' });
|
|
324
|
-
|
|
325
|
-
assert.deepEqual(callLog, ['claude', 'cursor', 'shell']);
|
|
326
|
-
|
|
327
|
-
// Unknown action throws
|
|
328
|
-
await assert.rejects(
|
|
329
|
-
() => executeAction('unknown-action', {}),
|
|
330
|
-
{ message: 'Unknown action: unknown-action' }
|
|
331
|
-
);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('streaming actions are identified correctly', () => {
|
|
335
|
-
const streamingActions = new Set(['claude-query', 'claude-task-query', 'codex-query', 'cursor-query']);
|
|
336
|
-
|
|
337
|
-
function isStreamingAction(action) {
|
|
338
|
-
return streamingActions.has(action);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
assert.equal(isStreamingAction('claude-query'), true);
|
|
342
|
-
assert.equal(isStreamingAction('codex-query'), true);
|
|
343
|
-
assert.equal(isStreamingAction('cursor-query'), true);
|
|
344
|
-
assert.equal(isStreamingAction('shell-exec'), false);
|
|
345
|
-
assert.equal(isStreamingAction('file-read'), false);
|
|
346
|
-
assert.equal(isStreamingAction('git-status'), false);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// ── Test 6: Relay Payload Validation ──
|
|
351
|
-
|
|
352
|
-
describe('Relay Payload Validation', () => {
|
|
353
|
-
it('validates action is in allowlist', () => {
|
|
354
|
-
const ALLOWED_RELAY_ACTIONS = new Set([
|
|
355
|
-
'claude-query', 'claude-task-query', 'cursor-query', 'codex-query',
|
|
356
|
-
'shell-exec', 'file-read', 'file-write', 'file-list',
|
|
357
|
-
'git-status', 'git-diff', 'git-log', 'git-branches',
|
|
358
|
-
'gitagent-parse', 'detect-agents',
|
|
359
|
-
]);
|
|
360
|
-
|
|
361
|
-
function validateRelayPayload(action) {
|
|
362
|
-
if (!ALLOWED_RELAY_ACTIONS.has(action)) {
|
|
363
|
-
return { valid: false, reason: `Action "${action}" is not allowed` };
|
|
364
|
-
}
|
|
365
|
-
return { valid: true };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
assert.equal(validateRelayPayload('claude-query').valid, true);
|
|
369
|
-
assert.equal(validateRelayPayload('shell-exec').valid, true);
|
|
370
|
-
assert.equal(validateRelayPayload('rm-rf-everything').valid, false, 'Unknown action blocked');
|
|
371
|
-
assert.equal(validateRelayPayload('').valid, false, 'Empty action blocked');
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// ── Test 7: IP Pinning Security ──
|
|
376
|
-
|
|
377
|
-
describe('IP Pinning Security', () => {
|
|
378
|
-
it('blocks relay connection from different IP when another is active', () => {
|
|
379
|
-
const relayConnections = new Map();
|
|
380
|
-
|
|
381
|
-
// User 1 connects from IP A
|
|
382
|
-
relayConnections.set(1, { ws: { readyState: 1 }, clientIp: '1.2.3.4', connectedAt: Date.now() });
|
|
383
|
-
|
|
384
|
-
// Same user tries from IP B
|
|
385
|
-
const existing = relayConnections.get(1);
|
|
386
|
-
const newIp = '5.6.7.8';
|
|
387
|
-
const blocked = existing && existing.ws.readyState === 1 && existing.clientIp !== newIp;
|
|
388
|
-
|
|
389
|
-
assert.equal(blocked, true, 'Second connection from different IP is blocked');
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it('allows reconnection from same IP', () => {
|
|
393
|
-
const relayConnections = new Map();
|
|
394
|
-
relayConnections.set(1, { ws: { readyState: 1 }, clientIp: '1.2.3.4' });
|
|
395
|
-
|
|
396
|
-
const existing = relayConnections.get(1);
|
|
397
|
-
const sameIp = '1.2.3.4';
|
|
398
|
-
const blocked = existing && existing.ws.readyState === 1 && existing.clientIp !== sameIp;
|
|
399
|
-
|
|
400
|
-
assert.equal(blocked, false, 'Reconnection from same IP is allowed');
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it('allows new connection when previous relay is disconnected', () => {
|
|
404
|
-
const relayConnections = new Map();
|
|
405
|
-
relayConnections.set(1, { ws: { readyState: 3 }, clientIp: '1.2.3.4' }); // disconnected
|
|
406
|
-
|
|
407
|
-
const existing = relayConnections.get(1);
|
|
408
|
-
const newIp = '5.6.7.8';
|
|
409
|
-
const blocked = existing && existing.ws.readyState === 1 && existing.clientIp !== newIp;
|
|
410
|
-
|
|
411
|
-
assert.equal(blocked, false, 'New connection allowed when previous relay is disconnected');
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// ── Test 8: Dashboard Stats Cache ──
|
|
416
|
-
|
|
417
|
-
describe('Dashboard Stats Cache', () => {
|
|
418
|
-
it('cache returns fresh data within TTL', () => {
|
|
419
|
-
const cache = new Map();
|
|
420
|
-
const CACHE_TTL = 60_000;
|
|
421
|
-
|
|
422
|
-
function getCachedStats(userId) {
|
|
423
|
-
const entry = cache.get(userId);
|
|
424
|
-
if (entry && Date.now() - entry.ts < CACHE_TTL) return entry.data;
|
|
425
|
-
return null;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function setCachedStats(userId, data) {
|
|
429
|
-
cache.set(userId, { data, ts: Date.now() });
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Set cache
|
|
433
|
-
const testData = { account: { username: 'testuser' } };
|
|
434
|
-
setCachedStats(1, testData);
|
|
435
|
-
|
|
436
|
-
// Immediately retrieve — should hit
|
|
437
|
-
const cached = getCachedStats(1);
|
|
438
|
-
assert.deepEqual(cached, testData, 'Cache hit returns data');
|
|
439
|
-
|
|
440
|
-
// Different user — should miss
|
|
441
|
-
const otherUser = getCachedStats(2);
|
|
442
|
-
assert.equal(otherUser, null, 'Different user has no cache');
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('cache invalidation clears entry for specific user', () => {
|
|
446
|
-
const cache = new Map();
|
|
447
|
-
|
|
448
|
-
cache.set(1, { data: { user: 'A' }, ts: Date.now() });
|
|
449
|
-
cache.set(2, { data: { user: 'B' }, ts: Date.now() });
|
|
450
|
-
|
|
451
|
-
// Invalidate user 1 only
|
|
452
|
-
cache.delete(1);
|
|
453
|
-
|
|
454
|
-
assert.equal(cache.has(1), false, 'User 1 cache invalidated');
|
|
455
|
-
assert.equal(cache.has(2), true, 'User 2 cache unaffected');
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
// ── Test 9: Frontend SessionStorage Stale-While-Revalidate ──
|
|
460
|
-
|
|
461
|
-
describe('Stale-While-Revalidate Pattern', () => {
|
|
462
|
-
it('shows cached data immediately then updates with fresh data', () => {
|
|
463
|
-
// Simulate sessionStorage
|
|
464
|
-
const storage = {};
|
|
465
|
-
const setItem = (k, v) => { storage[k] = v; };
|
|
466
|
-
const getItem = (k) => storage[k] || null;
|
|
467
|
-
|
|
468
|
-
// Initial state — no cache
|
|
469
|
-
let stats = null;
|
|
470
|
-
let loading = true;
|
|
471
|
-
|
|
472
|
-
const cached = getItem('dash_stats');
|
|
473
|
-
assert.equal(cached, null, 'No cache initially');
|
|
474
|
-
assert.equal(loading, true, 'Loading state true initially');
|
|
475
|
-
|
|
476
|
-
// Simulate first load — data comes from server
|
|
477
|
-
const serverData = { account: { username: 'user1' }, apiKeys: { keys: [] } };
|
|
478
|
-
stats = serverData;
|
|
479
|
-
loading = false;
|
|
480
|
-
setItem('dash_stats', JSON.stringify(serverData));
|
|
481
|
-
|
|
482
|
-
assert.equal(loading, false);
|
|
483
|
-
assert.deepEqual(stats, serverData);
|
|
484
|
-
|
|
485
|
-
// Simulate second page load — cache hit
|
|
486
|
-
stats = null;
|
|
487
|
-
loading = true;
|
|
488
|
-
|
|
489
|
-
const cachedStr = getItem('dash_stats');
|
|
490
|
-
if (cachedStr) {
|
|
491
|
-
stats = JSON.parse(cachedStr);
|
|
492
|
-
loading = false; // Show cached data immediately
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
assert.equal(loading, false, 'Loading resolved immediately from cache');
|
|
496
|
-
assert.deepEqual(stats.account.username, 'user1', 'Cached data shown immediately');
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
// ── Test 10: Multi-User Chat Isolation (End-to-End) ──
|
|
501
|
-
|
|
502
|
-
describe('Multi-User Chat Isolation (E2E)', () => {
|
|
503
|
-
it('two users sending commands simultaneously route to correct relays', () => {
|
|
504
|
-
const relayConnections = new Map();
|
|
505
|
-
const relayReceived = { user1: [], user2: [] };
|
|
506
|
-
|
|
507
|
-
relayConnections.set(1, {
|
|
508
|
-
ws: { readyState: 1, send: (msg) => relayReceived.user1.push(JSON.parse(msg)) }
|
|
509
|
-
});
|
|
510
|
-
relayConnections.set(2, {
|
|
511
|
-
ws: { readyState: 1, send: (msg) => relayReceived.user2.push(JSON.parse(msg)) }
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// User 1 sends a claude-command
|
|
515
|
-
function routeToRelay(userId, action, command) {
|
|
516
|
-
const relay = relayConnections.get(userId);
|
|
517
|
-
if (!relay || relay.ws.readyState !== 1) throw new Error('No relay');
|
|
518
|
-
relay.ws.send(JSON.stringify({ type: 'relay-command', action, command }));
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
routeToRelay(1, 'claude-query', 'Hey, I am user 1');
|
|
522
|
-
routeToRelay(2, 'claude-query', 'Hey, I am user 2');
|
|
523
|
-
routeToRelay(1, 'claude-query', 'Second message from user 1');
|
|
524
|
-
|
|
525
|
-
assert.equal(relayReceived.user1.length, 2, 'User 1 relay received 2 commands');
|
|
526
|
-
assert.equal(relayReceived.user2.length, 1, 'User 2 relay received 1 command');
|
|
527
|
-
assert.equal(relayReceived.user1[0].command, 'Hey, I am user 1');
|
|
528
|
-
assert.equal(relayReceived.user2[0].command, 'Hey, I am user 2');
|
|
529
|
-
assert.equal(relayReceived.user1[1].command, 'Second message from user 1');
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it('streaming responses go back to the correct frontend client', () => {
|
|
533
|
-
// Simulate pending relay requests with different userIds
|
|
534
|
-
const pendingRelayRequests = new Map();
|
|
535
|
-
const frontendReceived = { user1: [], user2: [] };
|
|
536
|
-
|
|
537
|
-
const writer1 = { send: (msg) => frontendReceived.user1.push(msg) };
|
|
538
|
-
const writer2 = { send: (msg) => frontendReceived.user2.push(msg) };
|
|
539
|
-
|
|
540
|
-
pendingRelayRequests.set('req-1', {
|
|
541
|
-
userId: 1,
|
|
542
|
-
onStream: (data) => {
|
|
543
|
-
if (data.type === 'claude-response') {
|
|
544
|
-
writer1.send({ type: 'claude-response', data: { type: 'content_block_delta', delta: { text: data.content } } });
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
pendingRelayRequests.set('req-2', {
|
|
549
|
-
userId: 2,
|
|
550
|
-
onStream: (data) => {
|
|
551
|
-
if (data.type === 'claude-response') {
|
|
552
|
-
writer2.send({ type: 'claude-response', data: { type: 'content_block_delta', delta: { text: data.content } } });
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// Relay stream for user 1
|
|
558
|
-
const pending1 = pendingRelayRequests.get('req-1');
|
|
559
|
-
pending1.onStream({ type: 'claude-response', content: 'Hello user 1!' });
|
|
560
|
-
|
|
561
|
-
// Relay stream for user 2
|
|
562
|
-
const pending2 = pendingRelayRequests.get('req-2');
|
|
563
|
-
pending2.onStream({ type: 'claude-response', content: 'Hello user 2!' });
|
|
564
|
-
|
|
565
|
-
assert.equal(frontendReceived.user1.length, 1);
|
|
566
|
-
assert.equal(frontendReceived.user2.length, 1);
|
|
567
|
-
assert.equal(frontendReceived.user1[0].data.delta.text, 'Hello user 1!');
|
|
568
|
-
assert.equal(frontendReceived.user2[0].data.delta.text, 'Hello user 2!');
|
|
569
|
-
});
|
|
570
|
-
});
|