@axhub/genie 0.2.7 → 0.2.8

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.
Files changed (117) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CTKZtqB1.js +460 -0
  3. package/dist/assets/App-qxJ8_QYu.css +32 -0
  4. package/dist/assets/ReviewApp-DM6BNAzR.js +1 -0
  5. package/dist/assets/{_basePickBy-C19AekOu.js → _basePickBy-CqJbRZ9y.js} +1 -1
  6. package/dist/assets/{_baseUniq-JsnevLw_.js → _baseUniq-BS8YH8jO.js} +1 -1
  7. package/dist/assets/{arc-BLpcuBlf.js → arc-BBmKEN-S.js} +1 -1
  8. package/dist/assets/architectureDiagram-2XIMDMQ5-N5lcb82R.js +36 -0
  9. package/dist/assets/{blockDiagram-WCTKOSBZ-DQBLwsUS.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +3 -3
  10. package/dist/assets/c4Diagram-IC4MRINW-BTKlkXI9.js +10 -0
  11. package/dist/assets/channel-1oJBvF-0.js +1 -0
  12. package/dist/assets/{chunk-4BX2VUAB-De63kbgc.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
  13. package/dist/assets/{chunk-55IACEB6-DtTDDdM9.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
  14. package/dist/assets/{chunk-FMBD7UC4-DHuwd8tw.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
  15. package/dist/assets/{chunk-JSJVCQXG-BgytFtmO.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
  16. package/dist/assets/{chunk-KX2RTZJC-nZdp86aN.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
  17. package/dist/assets/chunk-NQ4KR5QH-CM50ygWP.js +220 -0
  18. package/dist/assets/{chunk-QZHKN3VN-DvUQ3mnO.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
  19. package/dist/assets/chunk-WL4C6EOR-Cm9nQrsr.js +189 -0
  20. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
  21. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
  22. package/dist/assets/clone-CinxIlEu.js +1 -0
  23. package/dist/assets/cose-bilkent-S5V4N54A-Ccp_p0JZ.js +1 -0
  24. package/dist/assets/cytoscape.esm-2ZfV8NB5.js +331 -0
  25. package/dist/assets/{dagre-KLK3FWXG-CHYIvW47.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
  26. package/dist/assets/diagram-E7M64L7V-CeNVmFUp.js +24 -0
  27. package/dist/assets/{diagram-IFDJBPK2-Dzsiln_C.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
  28. package/dist/assets/{diagram-P4PSJMXO-DKnGbUpE.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
  29. package/dist/assets/erDiagram-INFDFZHY-B8R5vwhd.js +70 -0
  30. package/dist/assets/{flowDiagram-PKNHOUZH-BAZ2-jKp.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +4 -4
  31. package/dist/assets/ganttDiagram-A5KZAMGK-DOu3hSNa.js +292 -0
  32. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BflpyjGy.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
  33. package/dist/assets/{graph-suelaXFh.js → graph-D11wiwHo.js} +1 -1
  34. package/dist/assets/highlighted-body-TPN3WLV5-Babpthg-.js +1 -0
  35. package/dist/assets/index-DFxzgWoO.js +2 -0
  36. package/dist/assets/index-YCFGDVKw.css +1 -0
  37. package/dist/assets/{infoDiagram-LFFYTUFH-pfD1FA3p.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
  38. package/dist/assets/ishikawaDiagram-PHBUUO56-BEquZd3E.js +70 -0
  39. package/dist/assets/journeyDiagram-4ABVD52K-BfemGz7f.js +139 -0
  40. package/dist/assets/{kanban-definition-K7BYSVSG-FWinmur1.js → kanban-definition-K7BYSVSG-CWja3mln.js} +5 -5
  41. package/dist/assets/{layout-vcz43XvZ.js → layout-BLUNf-PJ.js} +1 -1
  42. package/dist/assets/{linear-le4gc0vx.js → linear-DukIV_Xv.js} +1 -1
  43. package/dist/assets/mermaid-O7DHMXV3-SgtM28qI.js +1038 -0
  44. package/dist/assets/mindmap-definition-YRQLILUH-4UjqXITU.js +68 -0
  45. package/dist/assets/{pieDiagram-SKSYHLDU-C7PKDh3b.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +2 -2
  46. package/dist/assets/quadrantDiagram-337W2JSQ-D60m8V8r.js +7 -0
  47. package/dist/assets/requirementDiagram-Z7DCOOCP-zqh9jBVf.js +73 -0
  48. package/dist/assets/{sankeyDiagram-WA2Y5GQK-4gulcOP4.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +3 -3
  49. package/dist/assets/sequenceDiagram-2WXFIKYE-7BReFd0L.js +145 -0
  50. package/dist/assets/{stateDiagram-RAJIS63D-CB4Vl7qM.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
  51. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
  52. package/dist/assets/timeline-definition-YZTLITO2-CTVllFgr.js +61 -0
  53. package/dist/assets/{treemap-KZPCXAKY-DZSEE6Hz.js → treemap-KZPCXAKY-BtyxboJZ.js} +58 -58
  54. package/dist/assets/vendor-codemirror-Dz7_EqNA.js +39 -0
  55. package/dist/assets/vendor-react-Cpt6D04s.js +59 -0
  56. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  57. package/dist/assets/{vennDiagram-LZ73GAT5-8E_G06fI.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +4 -4
  58. package/dist/assets/xychartDiagram-JWTSCODW-eRk-39YO.js +7 -0
  59. package/dist/index.html +4 -4
  60. package/package.json +34 -33
  61. package/server/acp-runtime/client.js +82 -15
  62. package/server/acp-runtime/index.js +5 -16
  63. package/server/channels/runtime/AgentRuntimeAdapter.js +1 -10
  64. package/server/claude-sdk.js +1 -3
  65. package/server/cli.js +136 -0
  66. package/server/gemini-cli.js +1 -3
  67. package/server/index.js +86 -14
  68. package/server/openai-codex.js +1 -3
  69. package/server/opencode-cli.js +1 -3
  70. package/server/projects.js +128 -85
  71. package/server/routes/cc-connect.js +1131 -0
  72. package/server/routes/cli-auth.js +1 -73
  73. package/server/routes/commands.js +4 -9
  74. package/server/routes/projects.js +45 -24
  75. package/server/session-core/providerDiscovery.js +8 -3
  76. package/server/utils/ccConnectManager.js +390 -0
  77. package/server/utils/ccConnectState.js +575 -0
  78. package/server/utils/resolveCommandPath.js +71 -0
  79. package/server/utils/workspaceRoots.js +154 -0
  80. package/dist/assets/App-BWSqiXAT.js +0 -220
  81. package/dist/assets/App-DrlLKa8f.css +0 -1
  82. package/dist/assets/ReviewApp-nz3mbArg.js +0 -1
  83. package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +0 -36
  84. package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +0 -10
  85. package/dist/assets/channel-DkFNxV_H.js +0 -1
  86. package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +0 -220
  87. package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +0 -189
  88. package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +0 -1
  89. package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +0 -1
  90. package/dist/assets/clone-C0lCEIEO.js +0 -1
  91. package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +0 -1
  92. package/dist/assets/cytoscape.esm-5J0xJHOV.js +0 -321
  93. package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +0 -24
  94. package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +0 -70
  95. package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +0 -292
  96. package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +0 -1
  97. package/dist/assets/index-B01NxbUv.css +0 -1
  98. package/dist/assets/index-DW5pGgQ_.js +0 -2
  99. package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +0 -70
  100. package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +0 -139
  101. package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +0 -870
  102. package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +0 -68
  103. package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +0 -7
  104. package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +0 -73
  105. package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +0 -145
  106. package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +0 -1
  107. package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +0 -61
  108. package/dist/assets/vendor-codemirror-CyOKkaQZ.js +0 -31
  109. package/dist/assets/vendor-react-CP4yFTs7.js +0 -8
  110. package/dist/assets/vendor-xterm-DfcmCpbH.js +0 -66
  111. package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +0 -7
  112. package/server/acp-runtime/client.test.js +0 -688
  113. package/server/acp-runtime/session-store.test.js +0 -89
  114. package/server/cli.test.js +0 -76
  115. package/server/external-agent/service.test.js +0 -53
  116. package/server/external-agent/ws.test.js +0 -289
  117. package/shared/conversationEvents.test.js +0 -403
@@ -1,89 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'fs/promises';
3
- import os from 'os';
4
- import path from 'path';
5
- import test from 'node:test';
6
- import { pathToFileURL } from 'node:url';
7
-
8
- async function loadSessionStore(sessionRoot) {
9
- process.env.AXHUB_GENIE_ACP_SESSION_ROOT = sessionRoot;
10
- const moduleUrl = `${pathToFileURL(path.resolve('server/acp-runtime/session-store.js')).href}?root=${encodeURIComponent(sessionRoot)}&ts=${Date.now()}`;
11
- return import(moduleUrl);
12
- }
13
-
14
- async function createTempSessionRoot() {
15
- return fs.mkdtemp(path.join(os.tmpdir(), 'axhub-genie-acp-sessions-'));
16
- }
17
-
18
- test('session-store persists and lists ACP sessions', async () => {
19
- const sessionRoot = await createTempSessionRoot();
20
-
21
- try {
22
- const sessionStore = await loadSessionStore(sessionRoot);
23
-
24
- await sessionStore.writeAcpSessionRecord({
25
- provider: 'claude',
26
- sessionId: 'session-a',
27
- projectPath: '/tmp/project-a',
28
- model: 'claude-sonnet-4',
29
- title: 'Session A',
30
- lastActivity: '2026-03-30T10:00:00.000Z',
31
- lastPromptAt: '2026-03-30T10:00:00.000Z'
32
- });
33
-
34
- const record = await sessionStore.readAcpSessionRecord('claude', 'session-a');
35
- assert.equal(record.provider, 'claude');
36
- assert.equal(record.sessionId, 'session-a');
37
- assert.equal(record.projectPath, '/tmp/project-a');
38
-
39
- const sessions = await sessionStore.listAcpSessions({
40
- provider: 'claude',
41
- projectPath: '/tmp/project-a'
42
- });
43
-
44
- assert.equal(sessions.length, 1);
45
- assert.equal(sessions[0].id, 'session-a');
46
- assert.equal(sessions[0].model, 'claude-sonnet-4');
47
- assert.equal(sessions[0].source, 'acp');
48
- } finally {
49
- delete process.env.AXHUB_GENIE_ACP_SESSION_ROOT;
50
- await fs.rm(sessionRoot, { recursive: true, force: true });
51
- }
52
- });
53
-
54
- test('session-store GC removes only stale closed session records by default', async () => {
55
- const sessionRoot = await createTempSessionRoot();
56
-
57
- try {
58
- const sessionStore = await loadSessionStore(sessionRoot);
59
-
60
- await sessionStore.writeAcpSessionRecord({
61
- provider: 'codex',
62
- sessionId: 'closed-session',
63
- projectPath: '/tmp/project-b',
64
- lastActivity: '2025-01-01T00:00:00.000Z',
65
- closedAt: '2025-01-01T00:00:00.000Z',
66
- isClosed: true
67
- });
68
-
69
- await sessionStore.writeAcpSessionRecord({
70
- provider: 'codex',
71
- sessionId: 'open-session',
72
- projectPath: '/tmp/project-b',
73
- lastActivity: '2025-01-01T00:00:00.000Z',
74
- isClosed: false
75
- });
76
-
77
- const gcResult = await sessionStore.gcOldAcpSessions({
78
- provider: 'codex',
79
- maxAgeDays: 30
80
- });
81
-
82
- assert.equal(gcResult.removed, 1);
83
- assert.equal(await sessionStore.readAcpSessionRecord('codex', 'closed-session'), null);
84
- assert.ok(await sessionStore.readAcpSessionRecord('codex', 'open-session'));
85
- } finally {
86
- delete process.env.AXHUB_GENIE_ACP_SESSION_ROOT;
87
- await fs.rm(sessionRoot, { recursive: true, force: true });
88
- }
89
- });
@@ -1,76 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
-
4
- import {
5
- buildEditorRequest,
6
- deriveExternalAgentWebSocketUrl,
7
- normalizeApiBaseUrl,
8
- parseArgs,
9
- } from './cli.js';
10
-
11
- test('parseArgs collects nested editor positionals and options', () => {
12
- const parsed = parseArgs([
13
- 'editor',
14
- 'nodes',
15
- 'list',
16
- '--channel',
17
- 'project-a',
18
- '--target-client-id',
19
- 'figma-123',
20
- '--status',
21
- 'pending-dispatch,dirty',
22
- '--limit',
23
- '25',
24
- ]);
25
-
26
- assert.deepEqual(parsed.positionals, ['editor', 'nodes', 'list']);
27
- assert.equal(parsed.options.channel, 'project-a');
28
- assert.equal(parsed.options.targetClientId, 'figma-123');
29
- assert.equal(parsed.options.status, 'pending-dispatch,dirty');
30
- assert.equal(parsed.options.limit, '25');
31
- });
32
-
33
- test('parseArgs accepts default working directory aliases', () => {
34
- const parsed = parseArgs([
35
- 'start',
36
- '--cwd',
37
- './workspace',
38
- ]);
39
-
40
- assert.deepEqual(parsed.positionals, ['start']);
41
- assert.equal(parsed.options.defaultProjectPath, './workspace');
42
- });
43
-
44
- test('buildEditorRequest creates clients list request without target client', () => {
45
- const request = buildEditorRequest(['editor', 'clients', 'list'], {
46
- channel: 'project-a',
47
- });
48
-
49
- assert.equal(request.message.type, 'integration.editor.clients.list');
50
- assert.equal(request.message.payload.channel, 'project-a');
51
- assert.equal(request.targetClientId, null);
52
- });
53
-
54
- test('buildEditorRequest creates screenshot request with resolved download path', () => {
55
- const request = buildEditorRequest(['editor', 'node', 'screenshot'], {
56
- channel: 'project-a',
57
- targetClientId: 'figma-123',
58
- elementKey: 'hero-card',
59
- outputDir: '.',
60
- });
61
-
62
- assert.equal(request.message.type, 'integration.editor.node.screenshot.get');
63
- assert.equal(request.message.payload.channel, 'project-a');
64
- assert.equal(request.message.payload.targetClientId, 'figma-123');
65
- assert.equal(request.message.payload.elementKey, 'hero-card');
66
- assert.ok(request.message.payload.downloadPath.endsWith('/AI-Web-UI'));
67
- });
68
-
69
- test('normalizeApiBaseUrl trims trailing slashes', () => {
70
- assert.equal(normalizeApiBaseUrl('http://localhost:32123/api///'), 'http://localhost:32123/api');
71
- });
72
-
73
- test('deriveExternalAgentWebSocketUrl switches to ws and preserves apiKey', () => {
74
- const url = deriveExternalAgentWebSocketUrl('http://localhost:32123/api', 'ck_test');
75
- assert.equal(url, 'ws://localhost:32123/api/agent/ws?apiKey=ck_test');
76
- });
@@ -1,53 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import path from 'path';
4
-
5
- import { normalizeExternalAgentRunRequest } from './service.js';
6
-
7
- const DEFAULT_WORKDIR_ENV = 'AXHUB_GENIE_DEFAULT_PROJECT_PATH';
8
- const originalDefaultWorkdir = process.env[DEFAULT_WORKDIR_ENV];
9
-
10
- test.afterEach(() => {
11
- if (originalDefaultWorkdir === undefined) {
12
- delete process.env[DEFAULT_WORKDIR_ENV];
13
- return;
14
- }
15
-
16
- process.env[DEFAULT_WORKDIR_ENV] = originalDefaultWorkdir;
17
- });
18
-
19
- test('normalizeExternalAgentRunRequest falls back to configured default project path', () => {
20
- process.env[DEFAULT_WORKDIR_ENV] = '/tmp/axhub-default-project';
21
-
22
- const normalized = normalizeExternalAgentRunRequest({
23
- provider: 'codex',
24
- message: 'Inspect the repository',
25
- stream: false
26
- });
27
-
28
- assert.equal(normalized.projectPath, path.resolve('/tmp/axhub-default-project'));
29
- });
30
-
31
- test('normalizeExternalAgentRunRequest falls back to process cwd when no project path is provided', () => {
32
- delete process.env[DEFAULT_WORKDIR_ENV];
33
-
34
- const normalized = normalizeExternalAgentRunRequest({
35
- provider: 'codex',
36
- message: 'Inspect the repository',
37
- stream: false
38
- });
39
-
40
- assert.equal(normalized.projectPath, process.cwd());
41
- });
42
-
43
- test('normalizeExternalAgentRunRequest rejects deprecated GitHub parameters', () => {
44
- assert.throws(() => {
45
- normalizeExternalAgentRunRequest({
46
- provider: 'codex',
47
- message: 'Inspect the repository',
48
- githubUrl: 'https://github.com/example/repo'
49
- });
50
- }, {
51
- message: /Unsupported parameters: githubUrl/
52
- });
53
- });
@@ -1,289 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { EventEmitter } from 'node:events';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { promises as fs } from 'node:fs';
7
-
8
- import { handleExternalAgentWebSocketConnection } from './ws.js';
9
-
10
- class FakeWebSocket extends EventEmitter {
11
- constructor() {
12
- super();
13
- this.readyState = 1;
14
- this.sent = [];
15
- }
16
-
17
- send(payload) {
18
- this.sent.push(JSON.parse(String(payload)));
19
- }
20
-
21
- close() {
22
- this.readyState = 3;
23
- this.emit('close');
24
- }
25
-
26
- lastMessage() {
27
- return this.sent[this.sent.length - 1] || null;
28
- }
29
- }
30
-
31
- function attachSocket(socket) {
32
- handleExternalAgentWebSocketConnection(socket, { user: null });
33
- return socket;
34
- }
35
-
36
- function sendMessage(socket, payload) {
37
- socket.emit('message', JSON.stringify(payload));
38
- }
39
-
40
- async function flushAsyncWork() {
41
- await new Promise((resolve) => setImmediate(resolve));
42
- }
43
-
44
- async function createTempDir(prefix) {
45
- return fs.mkdtemp(path.join(os.tmpdir(), `${prefix}-`));
46
- }
47
-
48
- async function waitFor(check, { timeoutMs = 1000, intervalMs = 10 } = {}) {
49
- const startedAt = Date.now();
50
- while (Date.now() - startedAt < timeoutMs) {
51
- const result = check();
52
- if (result) {
53
- return result;
54
- }
55
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
56
- }
57
- throw new Error('Timed out waiting for async WebSocket message');
58
- }
59
-
60
- const TEST_PNG_DATA_URL =
61
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Z0bUAAAAASUVORK5CYII=';
62
-
63
- test('integration.editor.clients.list returns connected frontend clients', async () => {
64
- const frontend = attachSocket(new FakeWebSocket());
65
- const external = attachSocket(new FakeWebSocket());
66
-
67
- try {
68
- sendMessage(frontend, {
69
- type: 'integration.connect',
70
- requestId: 'conn_frontend',
71
- payload: {
72
- role: 'frontend-page',
73
- channel: 'project-a',
74
- clientId: 'figma-123',
75
- pageUrl: 'http://localhost:5173/session/1',
76
- sessionId: 'session-1',
77
- capabilities: ['editor.snapshot'],
78
- },
79
- });
80
-
81
- sendMessage(external, {
82
- type: 'integration.editor.clients.list',
83
- requestId: 'editor_clients',
84
- payload: {
85
- channel: 'project-a',
86
- },
87
- });
88
-
89
- const message = external.lastMessage();
90
- assert.equal(message.type, 'integration.editor.clients.result');
91
- assert.equal(message.requestId, 'editor_clients');
92
- assert.equal(message.payload.total, 1);
93
- assert.equal(message.payload.items[0].clientId, 'figma-123');
94
- assert.deepEqual(message.payload.items[0].capabilities, ['editor.snapshot']);
95
- } finally {
96
- external.close();
97
- frontend.close();
98
- }
99
- });
100
-
101
- test('integration.editor.node.screenshot.result materializes frontend data URL on the server', async () => {
102
- const frontend = attachSocket(new FakeWebSocket());
103
- const external = attachSocket(new FakeWebSocket());
104
- const outputDir = await createTempDir('axhub-genie-shot');
105
-
106
- try {
107
- sendMessage(frontend, {
108
- type: 'integration.connect',
109
- requestId: 'conn_frontend',
110
- payload: {
111
- role: 'frontend-page',
112
- channel: 'project-a',
113
- clientId: 'figma-123',
114
- capabilities: ['editor.node.screenshot'],
115
- },
116
- });
117
-
118
- sendMessage(external, {
119
- type: 'integration.editor.node.screenshot.get',
120
- requestId: 'editor_shot',
121
- payload: {
122
- channel: 'project-a',
123
- targetClientId: 'figma-123',
124
- elementKey: 'hero-card',
125
- downloadPath: outputDir,
126
- },
127
- });
128
-
129
- const forwardedRequest = frontend.lastMessage();
130
- assert.equal(forwardedRequest.type, 'integration.editor.node.screenshot.get');
131
- assert.equal(forwardedRequest.payload.downloadPath, outputDir);
132
-
133
- sendMessage(frontend, {
134
- type: 'integration.ack',
135
- requestId: 'editor_shot',
136
- payload: {
137
- accepted: true,
138
- },
139
- });
140
-
141
- sendMessage(frontend, {
142
- type: 'integration.editor.node.screenshot.result',
143
- requestId: 'editor_shot',
144
- payload: {
145
- width: 1440,
146
- height: 640,
147
- image: {
148
- name: 'hero-card.png',
149
- data: TEST_PNG_DATA_URL,
150
- },
151
- },
152
- });
153
-
154
- await flushAsyncWork();
155
-
156
- const message = await waitFor(() => {
157
- const last = external.lastMessage();
158
- return last?.type === 'integration.editor.node.screenshot.result' ? last : null;
159
- });
160
- assert.equal(message.type, 'integration.editor.node.screenshot.result');
161
- assert.equal(message.requestId, 'editor_shot');
162
- assert.equal(message.payload.elementKey, 'hero-card');
163
- assert.equal(message.payload.mimeType, 'image/png');
164
- assert.equal(message.payload.width, 1440);
165
- assert.equal(message.payload.height, 640);
166
- assert.ok(path.isAbsolute(message.payload.absolutePath));
167
- assert.ok(message.payload.absolutePath.startsWith(outputDir));
168
-
169
- const stat = await fs.stat(message.payload.absolutePath);
170
- assert.equal(stat.size, message.payload.size);
171
- } finally {
172
- external.close();
173
- frontend.close();
174
- await fs.rm(outputDir, { recursive: true, force: true });
175
- }
176
- });
177
-
178
- test('integration.editor.context-images.result materializes frontend data URLs on the server', async () => {
179
- const frontend = attachSocket(new FakeWebSocket());
180
- const external = attachSocket(new FakeWebSocket());
181
- const outputDir = await createTempDir('axhub-genie-context');
182
-
183
- try {
184
- sendMessage(frontend, {
185
- type: 'integration.connect',
186
- requestId: 'conn_frontend',
187
- payload: {
188
- role: 'frontend-page',
189
- channel: 'project-a',
190
- clientId: 'figma-123',
191
- capabilities: ['editor.context-images'],
192
- },
193
- });
194
-
195
- sendMessage(external, {
196
- type: 'integration.editor.context-images.get',
197
- requestId: 'editor_context_images',
198
- payload: {
199
- channel: 'project-a',
200
- targetClientId: 'figma-123',
201
- downloadPath: outputDir,
202
- },
203
- });
204
-
205
- const forwardedRequest = frontend.lastMessage();
206
- assert.equal(forwardedRequest.type, 'integration.editor.context-images.get');
207
- assert.equal(forwardedRequest.payload.downloadPath, outputDir);
208
-
209
- sendMessage(frontend, {
210
- type: 'integration.editor.context-images.result',
211
- requestId: 'editor_context_images',
212
- payload: {
213
- items: [
214
- {
215
- id: 'img_001',
216
- name: 'clipboard-image-1.png',
217
- createdAt: 1742547600000,
218
- source: 'prompt-context',
219
- data: TEST_PNG_DATA_URL,
220
- },
221
- {
222
- id: 'img_002',
223
- name: 'clipboard-image-2.png',
224
- createdAt: 1742547600001,
225
- source: 'prompt-context',
226
- data: TEST_PNG_DATA_URL,
227
- },
228
- ],
229
- },
230
- });
231
-
232
- await flushAsyncWork();
233
-
234
- const message = await waitFor(() => {
235
- const last = external.lastMessage();
236
- return last?.type === 'integration.editor.context-images.result' ? last : null;
237
- });
238
- assert.equal(message.type, 'integration.editor.context-images.result');
239
- assert.equal(message.requestId, 'editor_context_images');
240
- assert.equal(message.payload.items.length, 2);
241
-
242
- for (const item of message.payload.items) {
243
- assert.equal(item.mimeType, 'image/png');
244
- assert.ok(path.isAbsolute(item.absolutePath));
245
- assert.ok(item.absolutePath.startsWith(outputDir));
246
- const stat = await fs.stat(item.absolutePath);
247
- assert.equal(stat.size, item.size);
248
- }
249
- } finally {
250
- external.close();
251
- frontend.close();
252
- await fs.rm(outputDir, { recursive: true, force: true });
253
- }
254
- });
255
-
256
- test('integration.editor.snapshot.get returns unsupported capability error when frontend lacks capability', async () => {
257
- const frontend = attachSocket(new FakeWebSocket());
258
- const external = attachSocket(new FakeWebSocket());
259
-
260
- try {
261
- sendMessage(frontend, {
262
- type: 'integration.connect',
263
- requestId: 'conn_frontend',
264
- payload: {
265
- role: 'frontend-page',
266
- channel: 'project-a',
267
- clientId: 'figma-123',
268
- capabilities: ['context.update'],
269
- },
270
- });
271
-
272
- sendMessage(external, {
273
- type: 'integration.editor.snapshot.get',
274
- requestId: 'editor_snapshot',
275
- payload: {
276
- channel: 'project-a',
277
- targetClientId: 'figma-123',
278
- },
279
- });
280
-
281
- const message = external.lastMessage();
282
- assert.equal(message.type, 'integration.error');
283
- assert.equal(message.requestId, 'editor_snapshot');
284
- assert.equal(message.payload.code, 'UNSUPPORTED_FRONTEND_CAPABILITY');
285
- } finally {
286
- external.close();
287
- frontend.close();
288
- }
289
- });