@harness-mix/cli 0.1.4 → 0.1.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +26 -96
  3. package/docs/chatgpt-web-sidebar.md +7 -1
  4. package/docs/cross-platform.md +15 -0
  5. package/docs/harness-handoff-design.md +377 -0
  6. package/docs/harness-management.md +28 -1
  7. package/docs/native-codex.md +21 -10
  8. package/output/native-build/harness-mix-shim.exe +0 -0
  9. package/output/native-build/renderer-extension.js +972 -35
  10. package/package.json +18 -9
  11. package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
  12. package/scripts/codex-accounts-test.cjs +81 -0
  13. package/scripts/codex-accounts-ui-smoke.cjs +70 -0
  14. package/scripts/e2e-integrations.cjs +52 -0
  15. package/scripts/e2e-native-host.cjs +39 -0
  16. package/scripts/handoff-checkpoint-test.cjs +83 -0
  17. package/scripts/install-native.cjs +4 -1
  18. package/scripts/integrations-test.cjs +64 -0
  19. package/scripts/integrations-ui-smoke.cjs +63 -0
  20. package/scripts/native-protocol-test.cjs +78 -4
  21. package/scripts/native-secret-test.cjs +2 -2
  22. package/scripts/native-shim-test.cjs +20 -4
  23. package/scripts/native-ui-smoke.cjs +58 -2
  24. package/scripts/publish-native-package.cjs +8 -5
  25. package/scripts/support/integrations-mcp-fixture.cjs +15 -0
  26. package/scripts/switch-harness-test.cjs +27 -1
  27. package/src/main/adapters/acp.js +3 -3
  28. package/src/main/adapters/antigravity.js +10 -13
  29. package/src/main/adapters/claude.js +10 -3
  30. package/src/main/adapters/codex-app-server.js +20 -13
  31. package/src/main/adapters/codex.js +6 -4
  32. package/src/main/adapters/dsh.js +2 -1
  33. package/src/main/adapters/grok.js +4 -3
  34. package/src/main/adapters/managed-mcp.js +12 -0
  35. package/src/main/adapters/native-acp.js +4 -4
  36. package/src/main/adapters/opencode.js +9 -3
  37. package/src/main/adapters/pi.js +1 -0
  38. package/src/main/harness-adapter/manifest.js +17 -2
  39. package/src/main/host/handoff-access.js +53 -0
  40. package/src/main/host/handoff-checkpoints.js +206 -0
  41. package/src/main/host/handoff-mcp.cjs +38 -0
  42. package/src/main/host/handoff-tools.js +13 -0
  43. package/src/main/host/handoff.js +28 -6
  44. package/src/main/host/integrations.js +201 -0
  45. package/src/main/host/runtime.js +75 -35
  46. package/src/main/host/store.js +4 -4
  47. package/src/main/native/codex-accounts.js +260 -0
  48. package/src/main/native/host.js +1 -1
  49. package/src/main/native/launcher.js +2 -2
  50. package/src/main/native/protocol.js +74 -23
  51. package/src/main/native/redact.js +1 -1
  52. package/src/main/native/rs/crates/shim/src/main.rs +155 -12
  53. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
  56. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  57. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
  58. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  59. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  60. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
  61. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
  62. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
  63. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
  64. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
  65. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
  66. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
  67. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
  68. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +80 -0
  69. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
  70. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
  71. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  72. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  73. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  74. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
  75. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
  76. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
  77. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
  78. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +5 -0
  79. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
  80. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
  81. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  82. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
  83. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  84. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -1
  86. package/src/native-ui/renderer-extension/src/index.ts +28 -0
  87. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
  88. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
  89. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +159 -40
  90. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +83 -0
  91. package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
  92. package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
  93. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
  94. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +26 -0
  95. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
  96. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  97. package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
  98. package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
  99. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +102 -0
  100. package/src/native-ui/renderer-extension/src/settings/localization.ts +30 -13
  101. package/src/native-ui/renderer-extension/src/settings/pages.ts +47 -18
  102. package/src/native-ui/renderer-extension/src/settings/shell.css +13 -0
  103. package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
  104. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +18 -0
  105. package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
  106. package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
  107. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
  108. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  109. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
  110. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  111. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
  112. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
  113. package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
  114. package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
  115. package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
  116. package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
  117. package/src/native-ui/shared-contracts/dist/index.js +1 -1
  118. package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
  119. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
  120. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  121. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
  122. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  123. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  124. package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
  125. package/src/native-ui/shared-contracts/src/index.ts +8 -1
  126. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-mix/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Harness Mix native Codex UI bridge for Codex, Pi, Claude Code, DeepSeek Harness, Antigravity, CodeBuddy, Kiro CLI, Cursor CLI and other native coding harnesses.",
6
6
  "keywords": [
@@ -69,6 +69,9 @@
69
69
  "node": ">=22.19.0 <23 || >=24 <25"
70
70
  },
71
71
  "scripts": {
72
+ "test:integrations": "node scripts/integrations-test.cjs",
73
+ "smoke:integrations-ui": "electron scripts/integrations-ui-smoke.cjs",
74
+ "e2e:integrations": "node scripts/e2e-integrations.cjs",
72
75
  "health:harnesses": "node scripts/harness-health-check.cjs",
73
76
  "test:dsh-collaboration-acp": "node scripts/dsh-collaboration-acp-test.cjs",
74
77
  "test:codebuddy-migration": "node scripts/codebuddy-migration-test.cjs",
@@ -82,6 +85,7 @@
82
85
  "test:collaboration": "node scripts/collaboration-test.cjs",
83
86
  "test:switch-harness": "node scripts/switch-harness-test.cjs",
84
87
  "smoke:collaboration-ui": "electron scripts/collaboration-ui-smoke.cjs",
88
+ "smoke:chatgpt-context-ui": "electron scripts/chatgpt-context-ui-smoke.cjs",
85
89
  "e2e:collaboration": "node scripts/e2e-collaboration.cjs",
86
90
  "test:native-rollback": "node scripts/native-rollback-test.cjs",
87
91
  "test:native-images": "node scripts/native-images-test.cjs",
@@ -95,6 +99,7 @@
95
99
  "test:native-config": "node scripts/native-config-test.cjs",
96
100
  "test:native-compatibility": "node scripts/native-compatibility-test.cjs",
97
101
  "e2e:native": "node scripts/e2e-native-host.cjs",
102
+ "e2e:native:codex": "node scripts/e2e-native-host.cjs --live-codex",
98
103
  "e2e:native:pi": "node scripts/e2e-native-host.cjs --live --harness=pi",
99
104
  "e2e:native:dsh": "node scripts/e2e-native-host.cjs --live --harness=deepseek-harness",
100
105
  "e2e:native:claude": "node scripts/e2e-native-host.cjs --live --harness=claude-code",
@@ -114,7 +119,10 @@
114
119
  "test:openclaw-adapter": "node scripts/openclaw-adapter-test.cjs",
115
120
  "test:core-concurrency": "node scripts/core-concurrency-test.cjs",
116
121
  "test:thread-title": "node scripts/thread-title-test.cjs",
117
- "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs",
122
+ "test:handoff": "node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs",
123
+ "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/codex-accounts-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs && node scripts/integrations-test.cjs",
124
+ "test:codex-accounts": "node scripts/codex-accounts-test.cjs",
125
+ "smoke:codex-accounts-ui": "electron scripts/codex-accounts-ui-smoke.cjs",
118
126
  "test:transcript": "node scripts/transcript-test.cjs",
119
127
  "test:workspace": "node scripts/workspace-test.cjs",
120
128
  "e2e:pi": "node scripts/e2e-pi.cjs",
@@ -151,7 +159,8 @@
151
159
  "test:collaboration-recovery": "node scripts/collaboration-recovery-test.cjs",
152
160
  "test:tool-concurrency": "node scripts/tool-concurrency-regression-test.cjs",
153
161
  "test:platform": "node scripts/platform-test.cjs",
154
- "test:shim": "node scripts/native-shim-test.cjs"
162
+ "test:shim": "node scripts/native-shim-test.cjs",
163
+ "publish:native": "node scripts/publish-native-package.cjs"
155
164
  },
156
165
  "devDependencies": {
157
166
  "@types/node": "24.13.3",
@@ -167,11 +176,11 @@
167
176
  "zod": "4.4.3"
168
177
  },
169
178
  "optionalDependencies": {
170
- "@harness-mix/native-win32-x64": "0.1.4",
171
- "@harness-mix/native-win32-arm64": "0.1.4",
172
- "@harness-mix/native-darwin-x64": "0.1.4",
173
- "@harness-mix/native-darwin-arm64": "0.1.4",
174
- "@harness-mix/native-linux-x64": "0.1.4",
175
- "@harness-mix/native-linux-arm64": "0.1.4"
179
+ "@harness-mix/native-win32-x64": "0.1.5",
180
+ "@harness-mix/native-win32-arm64": "0.1.5",
181
+ "@harness-mix/native-darwin-x64": "0.1.5",
182
+ "@harness-mix/native-darwin-arm64": "0.1.5",
183
+ "@harness-mix/native-linux-x64": "0.1.5",
184
+ "@harness-mix/native-linux-arm64": "0.1.5"
176
185
  }
177
186
  }
@@ -0,0 +1,16 @@
1
+ const { app, BrowserWindow } = require('electron');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ app.disableHardwareAcceleration();
6
+ app.whenReady().then(async () => {
7
+ const bundle = await require('esbuild').build({ stdin: { contents: "export * from './src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts';", resolveDir: process.cwd() }, bundle: true, platform: 'browser', format: 'iife', globalName: 'ChatGptContext', write: false });
8
+ const win = new BrowserWindow({ show: false, width: 1100, height: 720, webPreferences: { sandbox: true, contextIsolation: true, offscreen: true } });
9
+ try {
10
+ const html = `<!doctype html><meta charset="utf-8"><style>body{margin:0;font:14px system-ui}.composer{position:fixed;left:40px;bottom:36px;width:52%;padding:14px;border:1px solid #ccc;border-radius:14px}.composer textarea{width:100%;height:110px}.quick{position:fixed;right:12px;top:60px;bottom:12px;width:38%;border:1px solid #ddd;border-radius:22px;background:white;overflow:hidden}.quick header{height:54px;display:flex;align-items:center;padding:0 18px;border-bottom:1px solid #eee}.turns{padding:24px}.turn{margin:16px 0;padding:12px;background:#f3f3f0}.assistant{background:#eef5ff}.chat-composer{position:absolute;left:16px;right:16px;bottom:16px}</style><main><div class="composer"><textarea id="draft" placeholder="发送消息"></textarea></div></main><aside class="quick" data-chatgpt-quick-chat-panel><header><span>聊天</span></header><a data-chat-title aria-current="page" href="https://chatgpt.com/c/design?private=1">额度设计讨论</a><section class="turns"><div class="turn" data-message-author-role="user">帮我设计多账号</div><div class="turn assistant" data-message-author-role="assistant">账号应绑定原生运行时。sk-abcdefghijklmnopqrstuvwxyz123456</div></section><textarea class="chat-composer" placeholder="给 ChatGPT 发消息"></textarea></aside>`;
11
+ await win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html)); await win.webContents.executeJavaScript(bundle.outputFiles[0].text);
12
+ const result = await win.webContents.executeJavaScript(`(() => { const api=ChatGptContext.installChatGptContextBridge({locale:()=> 'zh-CN',resolveTarget:()=>({editor:document.querySelector('#draft'),kind:'codex',label:'Codex',accountLabel:'work@example.com'})}); document.querySelector('[data-harness-mix-chatgpt-context-action]').click(); const value=document.querySelector('#draft').value; const status=document.querySelector('[data-harness-mix-chatgpt-context-status]').textContent; document.querySelector('[data-harness-mix-chatgpt-context-action]').click(); const duplicate=document.querySelector('[data-harness-mix-chatgpt-context-status]').textContent; globalThis.chatGptContextApi=api; return {value,status,duplicate}; })()`);
13
+ assert.match(result.value, /不可信历史资料/); assert.match(result.value, /User: 帮我设计多账号/); assert.match(result.value, /Assistant: 账号应绑定原生运行时/); assert.doesNotMatch(result.value, /sk-abcdefghijklmnopqrstuvwxyz/); assert.match(result.status, /Codex(work@example.com)/); assert.match(result.duplicate, /已经包含/);
14
+ const out = path.resolve('output/chatgpt-context-ui'); await fs.mkdir(out, { recursive: true }); await fs.writeFile(path.join(out, 'add-to-task.png'), (await win.webContents.capturePage()).toPNG()); await win.webContents.executeJavaScript('chatGptContextApi.dispose()'); assert.equal(await win.webContents.executeJavaScript('document.querySelectorAll("[data-harness-mix-chatgpt-context-action]").length'), 0); console.log('PASS: Quick chat context button, target account, redaction, draft insertion, duplicate guard and cleanup');
15
+ } finally { win.destroy(); app.quit(); }
16
+ }).catch((error) => { console.error(error); app.exit(1); });
@@ -0,0 +1,81 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const { CodexAccountManager } = require('../src/main/native/codex-accounts');
6
+
7
+ async function main() {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-mix-codex-accounts-'));
9
+ const servers = new Map();
10
+ const emitted = [];
11
+ const acquireServer = async (_diagnostic, codexHome) => {
12
+ let server = servers.get(codexHome);
13
+ if (!server) {
14
+ const listeners = new Set();
15
+ server = {
16
+ refs: 0,
17
+ account: null,
18
+ requests: [],
19
+ async request(method, params) {
20
+ this.requests.push({ method, params });
21
+ if (method === 'account/read') return { account: this.account };
22
+ if (method === 'account/rateLimits/read') return { rateLimits: { primary: { usedPercent: 24, windowDurationMins: 300, resetsAt: 1_800_000_000 }, secondary: { usedPercent: 41, windowDurationMins: 10080, resetsAt: 1_800_604_800 } } };
23
+ if (method === 'account/login/start') return { type: 'chatgptDeviceCode', loginId: 'login-isolated', verificationUrl: 'https://auth.example/device', userCode: 'WXYZ-1234' };
24
+ if (method === 'account/login/cancel') return { status: 'cancelled' };
25
+ throw new Error(`Unexpected fake Account request: ${method}`);
26
+ },
27
+ onNotification(listener) { listeners.add(listener); return () => listeners.delete(listener); },
28
+ notify(message) { for (const listener of listeners) listener(message); },
29
+ release() { this.refs--; },
30
+ };
31
+ servers.set(codexHome, server);
32
+ }
33
+ server.refs++;
34
+ return server;
35
+ };
36
+ const official = async (method) => {
37
+ if (method === 'account/read') return { account: { type: 'chatgpt', email: 'owner@example.com', planType: 'plus' } };
38
+ throw new Error(`Unexpected official request: ${method}`);
39
+ };
40
+ const manager = new CodexAccountManager({ dataDirectory: root, requestOfficial: official, emit: message => emitted.push(message), acquireServer });
41
+ try {
42
+ const initial = await manager.list();
43
+ assert.equal(initial.accounts.length, 1);
44
+ assert.equal(initial.accounts[0].email, 'owner@example.com');
45
+ assert.equal(initial.accounts[0].management, 'native');
46
+
47
+ const created = await manager.create('工作账号');
48
+ assert.equal(created.account.management, 'isolated');
49
+ assert.equal(created.account.authenticated, false);
50
+ const context = manager.executionContext(created.account.accountId);
51
+ assert.ok(context.codexHome.startsWith(path.join(root, 'codex-accounts', 'profiles') + path.sep));
52
+ const server = servers.get(context.codexHome);
53
+ server.account = { type: 'chatgpt', email: 'work@example.com', planType: 'pro' };
54
+
55
+ const activated = await manager.activate(created.account.accountId);
56
+ assert.equal(activated.account.active, true);
57
+ const accounts = await manager.list(true);
58
+ assert.equal(accounts.accounts.find(account => account.isDefault).active, false);
59
+ assert.equal(accounts.accounts.find(account => account.accountId === created.account.accountId).email, 'work@example.com');
60
+ const usage = await manager.usage(created.account.accountId);
61
+ assert.equal(usage.usage.planFiveHourUsedPercent, 24);
62
+ assert.equal(usage.accountCredits.productUsage[0].usagePercent, 41);
63
+
64
+ const login = await manager.startLogin(created.account.accountId);
65
+ assert.equal(login.userCode, 'WXYZ-1234');
66
+ server.notify({ method: 'account/login/completed', params: { loginId: login.loginId, success: true, error: null } });
67
+ assert.deepEqual(emitted.at(-1), { method: 'codexhost/account/login/completed', params: { accountId: created.account.accountId, loginId: login.loginId, success: true, error: null } });
68
+
69
+ const registry = fs.readFileSync(path.join(root, 'codex-accounts', 'accounts.json'), 'utf8');
70
+ assert.equal(registry.includes('owner@example.com'), false, 'Registry must not persist native identity or credentials');
71
+ assert.equal(registry.includes('work@example.com'), false, 'Registry must not persist isolated identity or credentials');
72
+ await manager.delete(created.account.accountId);
73
+ assert.equal(fs.existsSync(context.codexHome), false);
74
+ } finally {
75
+ manager.close();
76
+ fs.rmSync(root, { recursive: true, force: true });
77
+ }
78
+ console.log('Codex multi-account isolation, switching, usage and login lifecycle passed');
79
+ }
80
+
81
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,70 @@
1
+ const { app, BrowserWindow } = require('electron');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const esbuild = require('esbuild');
6
+
7
+ app.disableHardwareAcceleration();
8
+
9
+ app.whenReady().then(async () => {
10
+ const output = path.resolve('output/codex-accounts-ui-smoke');
11
+ fs.mkdirSync(output, { recursive: true });
12
+ const bundle = await esbuild.build({
13
+ stdin: {
14
+ contents: `export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { rendererSettingsMessages } from './src/native-ui/renderer-extension/src/settings/localization.ts'; export { installRendererSettingsShell } from './src/native-ui/renderer-extension/src/settings/shell.ts';`,
15
+ resolveDir: process.cwd(),
16
+ },
17
+ bundle: true,
18
+ alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') },
19
+ platform: 'browser',
20
+ format: 'iife',
21
+ globalName: 'AccountUI',
22
+ write: false,
23
+ loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' },
24
+ logLevel: 'silent',
25
+ });
26
+ const win = new BrowserWindow({ show: false, width: 1080, height: 720, webPreferences: { contextIsolation: true, sandbox: true, offscreen: true, backgroundThrottling: false } });
27
+ await win.loadURL('data:text/html,<html><body></body></html>');
28
+ await win.webContents.executeJavaScript(bundle.outputFiles[0].text);
29
+ const result = await win.webContents.executeJavaScript(`(async () => {
30
+ const accounts = [
31
+ { accountId:'official-codex', label:'owner@example.com', email:'owner@example.com', planType:'plus', codexHome:'C:/Users/test/.codex', active:false, isDefault:true, authenticated:true, management:'native' },
32
+ { accountId:'account-work', label:'work@example.com', email:'work@example.com', planType:'pro', codexHome:'C:/Profiles/work', active:true, isDefault:false, authenticated:true, management:'isolated' },
33
+ ];
34
+ const usage = {
35
+ 'official-codex': { usedPercent:31, periodType:'five_hour', resetsAt:'2026-09-14T03:00:00.000Z', productUsage:[{product:'7-day window',usagePercent:22,resetsAt:'2026-09-20T03:00:00.000Z'}] },
36
+ 'account-work': { usedPercent:68, periodType:'five_hour', resetsAt:'2026-09-14T05:00:00.000Z', productUsage:[{product:'7-day window',usagePercent:47,resetsAt:'2026-09-19T05:00:00.000Z'}] },
37
+ };
38
+ const client = {
39
+ listCodexAccounts: async () => ({accounts}), refreshCodexAccounts: async () => ({accounts}),
40
+ inspectCodexAccountUsage: async ({accountId}) => ({accountId,usage:null,accountCredits:usage[accountId]}),
41
+ createCodexAccount: async () => ({account:accounts[1]}), deleteCodexAccount: async ({accountId}) => ({deletedAccountId:accountId}),
42
+ activateCodexAccount: async ({accountId}) => ({account:accounts.find(account => account.accountId===accountId)}),
43
+ startCodexAccountLogin: async () => { throw new Error('not expected'); }, cancelCodexAccountLogin: async () => ({cancelled:true}),
44
+ logoutCodexAccount: async () => ({account:{...accounts[0],email:undefined,authenticated:false}}),
45
+ };
46
+ const messages=AccountUI.rendererSettingsMessages('zh-CN');
47
+ const shell=AccountUI.installRendererSettingsShell([AccountUI.createAccountsSettingsPage(messages,()=>client)],messages,document);
48
+ shell.openSettings(undefined,'accounts');
49
+ await new Promise(resolve=>setTimeout(resolve,250));
50
+ const shadow=shell.root.shadowRoot;
51
+ const content=shadow.querySelector('.settings-page__content');
52
+ const text=content.textContent;
53
+ const add=[...shadow.querySelectorAll('button')].find(button=>button.textContent.includes('添加账号'));
54
+ const rows=[...content.querySelectorAll('[data-account-id]')].map(row=>row.getAttribute('data-account-id'));
55
+ return {text,addVisible:add && !add.hidden,rows,pathsVisible:text.includes('C:/Profiles')||text.includes('.codex')};
56
+ })()`);
57
+ assert.equal(result.addVisible, true);
58
+ assert.deepEqual(result.rows, ['official-codex', 'account-work']);
59
+ assert.match(result.text, /owner.*example\.com/s);
60
+ assert.match(result.text, /work.*example\.com/s);
61
+ assert.match(result.text, /Plus/);
62
+ assert.match(result.text, /Pro/);
63
+ assert.match(result.text, /5 小时|5小时/);
64
+ assert.match(result.text, /7 天|7天/);
65
+ assert.equal(result.pathsVisible, false, result.text);
66
+ fs.writeFileSync(path.join(output, 'codex-multi-account.png'), (await win.webContents.capturePage()).toPNG());
67
+ win.destroy();
68
+ console.log('PASS: Codex multi-account settings, quota windows, refresh times and hidden CODEX_HOME render in Electron');
69
+ app.quit();
70
+ }).catch(error => { console.error(error); app.exit(1); });
@@ -0,0 +1,52 @@
1
+ const fs = require('node:fs/promises');
2
+ const path = require('node:path');
3
+ const assert = require('node:assert/strict');
4
+ const { randomUUID } = require('node:crypto');
5
+ const { Integrations } = require('../src/main/host/integrations');
6
+ const { buildAdapters } = require('../src/main/adapters');
7
+
8
+ async function main() {
9
+ const id = process.argv.find(a => a.startsWith('--harness='))?.split('=')[1] || 'claude';
10
+ const root = path.resolve('output/integrations-e2e', `${id}-${Date.now()}`), cwd = path.join(root, 'project');
11
+ await fs.mkdir(cwd, { recursive: true });
12
+ const adapters = new Map(buildAdapters(() => {}).map(a => [a.manifest.id, a]));
13
+ const adapter = adapters.get(id); assert.ok(adapter?.manifest.integrations?.mcp);
14
+ const runtime = { adapters, status: {}, sessions: new Map(), store: { directory: path.join(root, 'data') }, resolveHarnessId: id => id };
15
+ const manager = new Integrations(runtime), selected = { harnessId: id, scope: 'project', cwd };
16
+ const log = path.join(root, 'mcp-methods.log');
17
+ await manager.save({ ...selected, server: { name: 'acceptance', command: process.execPath, args: [path.resolve('scripts/support/integrations-mcp-fixture.cjs'), log], enabled: true } });
18
+ if (adapter.manifest.integrations.skills) {
19
+ const source = path.join(root, 'skill'); await fs.mkdir(source);
20
+ await fs.writeFile(path.join(source, 'SKILL.md'), '---\nname: hm-integration-acceptance\ndescription: Read-only Harness Mix integration acceptance fixture.\n---\nReturn HARNESS_MIX_SKILL_OK.\n');
21
+ await manager.skillChange({ ...selected, name: 'hm-integration-acceptance', source, action: 'install' });
22
+ }
23
+ const thread = { id: randomUUID(), nativeSessionId: randomUUID(), cwd, title: 'Integration acceptance', options: {} };
24
+ const injected = await manager.forSession(thread, adapter);
25
+ let session;
26
+ const deadline = setTimeout(() => { console.error('Native acceptance timed out'); process.exit(1); }, 55000);
27
+ try {
28
+ session = await adapter.open({ thread, managedMcp: injected.servers, emit: () => {}, diagnostic: () => {} });
29
+ session.adapter = adapter; session.integrationServers = injected.records; session.integrationCwd = injected.cwd;
30
+ runtime.sessions.set(thread.id, session);
31
+ let native = [], methods = '';
32
+ for (let attempt = 0; attempt < 15; attempt++) {
33
+ native = adapter.inspectIntegrations ? await adapter.inspectIntegrations(session) : [];
34
+ try { methods = await fs.readFile(log, 'utf8'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
35
+ if (native.some(s => s.name === 'hm-user-acceptance' && s.status === 'connected')) break;
36
+ if (!adapter.inspectIntegrations && methods.includes('tools/list')) break;
37
+ await new Promise(resolve => setTimeout(resolve, 1000));
38
+ }
39
+ if (adapter.inspectIntegrations) assert.ok(native.some(s => s.name === 'hm-user-acceptance' && s.status === 'connected'), 'native Harness must report connected');
40
+ methods = await fs.readFile(log, 'utf8'); assert.match(methods, /initialize/); assert.match(methods, /tools\/list/);
41
+ let skillLoaded = null;
42
+ if (session.query?.supportedCommands) {
43
+ const commands = await session.query.supportedCommands();
44
+ skillLoaded = commands.some(c => c.name === 'hm-integration-acceptance');
45
+ assert.equal(skillLoaded, true, 'native Claude must discover the installed project skill');
46
+ }
47
+ const snapshot = await manager.list(selected); assert.equal(snapshot.servers[0].appliedSessions, 1);
48
+ const report = { harness: id, nativeMcpConnected: native.some(s => s.name === 'hm-user-acceptance' && s.status === 'connected') || null, nativeToolDiscovery: true, skillLoaded, modelTurnExecuted: false, automaticApproval: false };
49
+ await fs.writeFile(path.join(root, 'report.json'), JSON.stringify(report, null, 2)); console.log(JSON.stringify({ ...report, root }));
50
+ } finally { clearTimeout(deadline); if (session) await adapter.close(session); }
51
+ }
52
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -41,7 +41,9 @@ async function main() {
41
41
  const ownership = await request('harness-mix/runtime/inspect', {});
42
42
  assert.equal(ownership.owner, 'harness-mix');
43
43
  assert.equal(ownership.core, 'src/main/protocol-core/protocol-core.js');
44
+ assert.deepEqual(ownership.codex, { mode: 'official-passthrough', managedRoute: 'codex-harness' });
44
45
  report.checks.push('Harness Mix HostRuntime and ProtocolCore own external execution');
46
+ report.checks.push('official Codex route is separate from the managed Codex worker');
45
47
  const plugins = await request('codexhost/harness/plugins/list', {});
46
48
  assert.ok(plugins.plugins.some(plugin => (plugin.id || plugin.manifest?.id) === 'pi'), 'Pi plugin registered');
47
49
  report.checks.push('external harness plugins registered');
@@ -54,6 +56,43 @@ async function main() {
54
56
  const officialModels = await request('model/list', {});
55
57
  assert.ok(Array.isArray(officialModels.data), 'Official model/list passes through');
56
58
  report.checks.push('official model/list passthrough');
59
+ const codexAccounts = await request('codexhost/account/refresh', {});
60
+ const officialAccount = codexAccounts.accounts?.find(account => account.management === 'native');
61
+ assert.ok(officialAccount, 'Official Codex account is projected into Account management');
62
+ assert.equal(officialAccount.authenticated, true, 'Current official Codex login is detected');
63
+ assert.ok(officialAccount.email, 'Current ChatGPT account identity is available from account/read');
64
+ const officialAccountUsage = await request('codexhost/account/usage/inspect', { accountId: officialAccount.accountId });
65
+ assert.ok(officialAccountUsage.accountCredits, 'Official Codex rate limits are projected');
66
+ report.account = {
67
+ authenticated: officialAccount.authenticated,
68
+ planType: officialAccount.planType || null,
69
+ fiveHourUsedPercent: officialAccountUsage.usage?.planFiveHourUsedPercent ?? null,
70
+ sevenDayUsedPercent: officialAccountUsage.usage?.planSevenDayUsedPercent ?? null,
71
+ };
72
+ report.checks.push('real official Codex account and rate-limit projection');
73
+ if (process.argv.includes('--live-codex')) {
74
+ const native = await request('thread/start', { cwd: directory });
75
+ const nativeThreadId = native.thread?.id;
76
+ assert.ok(nativeThreadId, 'Official Codex thread/start returns a native Thread');
77
+ const nativeTurn = await request('turn/start', {
78
+ threadId: nativeThreadId,
79
+ input: [{ type: 'text', text: 'Reply with exactly HARNESS_MIX_CODEX_PASSTHROUGH_OK. Do not use tools.' }],
80
+ });
81
+ const nativeTurnId = nativeTurn.turn?.id;
82
+ assert.ok(nativeTurnId, 'Official Codex turn/start returns a native Turn');
83
+ const deadline = Date.now() + 120000;
84
+ while (Date.now() < deadline && !events.some(event => event.method === 'turn/completed' && event.params?.threadId === nativeThreadId && event.params?.turn?.id === nativeTurnId)) {
85
+ await new Promise(resolve => setTimeout(resolve, 250));
86
+ }
87
+ const completed = events.find(event => event.method === 'turn/completed' && event.params?.threadId === nativeThreadId && event.params?.turn?.id === nativeTurnId);
88
+ assert.equal(completed?.params?.turn?.status, 'completed', 'Official Codex Turn completes through the passthrough path');
89
+ const text = events.filter(event => event.params?.threadId === nativeThreadId && event.params?.turnId === nativeTurnId)
90
+ .map(event => event.params?.delta || event.params?.item?.text || '').join('');
91
+ assert.ok(text.includes('HARNESS_MIX_CODEX_PASSTHROUGH_OK'), 'Official Codex response reaches Desktop protocol unchanged');
92
+ await request('thread/delete', { threadId: nativeThreadId }).catch(() => {});
93
+ report.codex = { threadId: nativeThreadId, turnId: nativeTurnId, status: completed.params.turn.status };
94
+ report.checks.push('real official Codex thread/turn passthrough');
95
+ }
57
96
  // Real Host + stock-server section catalog, with test threads isolated in output/.
58
97
  const sidebarThread = await request('thread/start', { cwd: directory, model: 'codexhost/pi-native' });
59
98
  const sidebarId = sidebarThread.thread.id;
@@ -0,0 +1,83 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const { HandoffCheckpoints } = require('../src/main/host/handoff-checkpoints');
5
+ const { HandoffAccess } = require('../src/main/host/handoff-access');
6
+ const { dispatch } = require('../src/main/host/handoff-mcp.cjs');
7
+
8
+ async function main() {
9
+ await fs.mkdir('output', { recursive: true });
10
+ const root = await fs.mkdtemp(path.resolve('output/handoff-checkpoint-'));
11
+ const runtime = {
12
+ store: { directory: path.join(root, 'data') },
13
+ adapters: new Map([['target', { manifest: { integrations: { mcp: true } } }]]),
14
+ };
15
+ runtime.handoffs = new HandoffCheckpoints(runtime);
16
+ await runtime.handoffs.initialize();
17
+
18
+ const thread = {
19
+ id: 'thread-a',
20
+ harnessId: 'source',
21
+ title: '修复登录模块',
22
+ cwd: process.cwd(),
23
+ messages: [
24
+ { id: 'user-1', role: 'user', text: '继续实现,api_key=supersecretvalue123' },
25
+ {
26
+ id: 'assistant-1', role: 'assistant', text: '先跑测试再修复', coreItems: [
27
+ { type: 'plan', entries: [{ text: '定位失败', status: 'completed' }, { text: '实现修复', status: 'in_progress' }] },
28
+ { type: 'tool_call', toolCallId: 'native-replay-id', title: 'npm test', state: 'error', input: 'token=anothersecretvalue', output: 'authorization: Bearer abcdefghijklmnop\n1 test failed' },
29
+ { type: 'file_change', path: 'src/login.js', changeType: 'modified', before: 'a', after: 'b' },
30
+ ],
31
+ },
32
+ ],
33
+ };
34
+
35
+ const checkpoint = await runtime.handoffs.create(thread, 'target', { intent: 'execute-plan' });
36
+ assert.match(checkpoint.checkpointId, /^handoff_[a-f0-9]{24}$/);
37
+ assert.equal(checkpoint.onDemandAccess, 'mcp');
38
+ assert.equal(checkpoint.intent, 'execute-plan');
39
+ assert.equal(checkpoint.plan.inProgress[0], '实现修复');
40
+ assert.equal(checkpoint.fileState.files[0].path, 'src/login.js');
41
+ assert.match(checkpoint.fileState.diffDigest, /^[a-f0-9]{64}$/);
42
+ assert.ok(checkpoint.fileState.gitHead === null || /^[a-f0-9]{40,64}$/.test(checkpoint.fileState.gitHead));
43
+ const serialized = JSON.stringify(checkpoint);
44
+ assert.doesNotMatch(serialized, /supersecretvalue123|anothersecretvalue|abcdefghijklmnop|native-replay-id/);
45
+ assert.match(serialized, /\[redacted\]/);
46
+
47
+ thread.messages[0].text = 'mutated after handoff';
48
+ assert.doesNotMatch(JSON.stringify(runtime.handoffs.owned(thread.id, checkpoint.checkpointId)), /mutated after handoff/);
49
+
50
+ const access = new HandoffAccess(runtime);
51
+ const connection = await access.connection({ ...thread, harnessId: 'target', pendingHandoff: { checkpointId: checkpoint.checkpointId } });
52
+ assert.ok(connection);
53
+ const oldUrl = process.env.HARNESS_MIX_HANDOFF_URL;
54
+ const oldKey = process.env.HARNESS_MIX_HANDOFF_KEY;
55
+ process.env.HARNESS_MIX_HANDOFF_URL = connection.env.HARNESS_MIX_HANDOFF_URL;
56
+ process.env.HARNESS_MIX_HANDOFF_KEY = connection.env.HARNESS_MIX_HANDOFF_KEY;
57
+ try {
58
+ const catalog = await dispatch({ id: 1, method: 'tools/list' });
59
+ assert.deepEqual(catalog.tools.map(tool => tool.name), [
60
+ 'get_handoff_checkpoint', 'list_handoff_conversation', 'list_handoff_evidence',
61
+ 'read_handoff_evidence', 'list_handoff_files', 'read_handoff_plan',
62
+ ]);
63
+ const result = await dispatch({ id: 2, method: 'tools/call', params: { name: 'get_handoff_checkpoint', arguments: { checkpoint_id: checkpoint.checkpointId } } });
64
+ const summary = JSON.parse(result.content[0].text);
65
+ assert.equal(summary.checkpointId, checkpoint.checkpointId);
66
+ assert.equal(summary.status, 'checkpoint-created');
67
+ const denied = await dispatch({ id: 3, method: 'tools/call', params: { name: 'get_handoff_checkpoint', arguments: { checkpoint_id: 'handoff_outside_scope' } } });
68
+ assert.equal(denied.isError, true);
69
+ assert.match(denied.content[0].text, /outside this native session scope/);
70
+ } finally {
71
+ if (oldUrl === undefined) delete process.env.HARNESS_MIX_HANDOFF_URL; else process.env.HARNESS_MIX_HANDOFF_URL = oldUrl;
72
+ if (oldKey === undefined) delete process.env.HARNESS_MIX_HANDOFF_KEY; else process.env.HARNESS_MIX_HANDOFF_KEY = oldKey;
73
+ await access.close();
74
+ }
75
+
76
+ const reloaded = new HandoffCheckpoints(runtime);
77
+ await reloaded.initialize();
78
+ assert.equal(reloaded.get(thread.id, checkpoint.checkpointId).contentDigest, checkpoint.contentDigest);
79
+ await reloaded.close();
80
+ console.log('PASS: immutable handoff checkpoint, redaction, persistence and scoped read-only MCP access');
81
+ }
82
+
83
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -18,7 +18,10 @@ if (!fs.existsSync(packageRoot)) {
18
18
  fs.mkdirSync(destination, { recursive: true });
19
19
  for (const name of files) {
20
20
  const source = path.join(packageRoot, 'bin', name);
21
- if (!fs.existsSync(source)) throw new Error(`${packageName} is missing bin/${name}`);
21
+ if (!fs.existsSync(source)) {
22
+ console.warn(`[Harness Mix] ${packageName} contains source-only platform metadata; run npm run build:native on this machine.`);
23
+ process.exit(0);
24
+ }
22
25
  const target = path.join(destination, name);
23
26
  fs.copyFileSync(source, target);
24
27
  if (platform !== 'win32') fs.chmodSync(target, 0o755);
@@ -0,0 +1,64 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const { Integrations } = require('../src/main/host/integrations');
5
+ const { buildAdapters } = require('../src/main/adapters');
6
+ const { NativeProtocol } = require('../src/main/native/protocol');
7
+ const { acpServers, namedServers } = require('../src/main/adapters/managed-mcp');
8
+
9
+ async function main() {
10
+ const root = path.resolve('output/integrations-test', `${Date.now()}`);
11
+ const project = path.join(root, 'project'), home = path.join(root, 'home');
12
+ await fs.mkdir(project, { recursive: true }); await fs.mkdir(home);
13
+ const adapters = new Map(buildAdapters(() => {}).map(a => [a.manifest.id, a]));
14
+ const runtime = { adapters, status: {}, sessions: new Map(), store: { directory: path.join(root, 'data') }, resolveHarnessId: id => id };
15
+ const manager = runtime.integrations = new Integrations(runtime, { home });
16
+ const global = { harnessId: 'claude', scope: 'global' }, local = { harnessId: 'claude', scope: 'project', cwd: project };
17
+ const server = { name: 'probe', command: process.execPath, args: ['probe.cjs'], enabled: true };
18
+ await Promise.all([manager.save({ ...global, server }), manager.save({ ...local, server: { ...server, enabled: false } })]);
19
+ assert.equal((await manager.forSession({ cwd: project }, adapters.get('claude'))).servers.length, 0, 'project disabled entry masks global');
20
+ let snapshot = await manager.list(local);
21
+ assert.equal(snapshot.servers.length, 2); assert.equal(snapshot.servers.filter(s => s.effective).length, 1);
22
+ await manager.remove({ ...local, id: snapshot.servers.find(s => s.scope === 'project').id });
23
+ const selected = await manager.forSession({ cwd: project }, adapters.get('claude'));
24
+ assert.equal(selected.servers[0].name, 'hm-user-probe');
25
+ runtime.sessions.set('s', { adapter: adapters.get('claude'), nativeSessionId: 'native-test', integrationCwd: await fs.realpath(project), integrationServers: selected.records,
26
+ query: { mcpServerStatus: async () => [{ name: 'hm-user-probe', status: 'connected', config: { token: 'DO_NOT_PROJECT' }, tools: [{ name: 'echo' }] }] } });
27
+ snapshot = await manager.list(local);
28
+ assert.equal(snapshot.servers[0].appliedSessions, 1); assert.equal(snapshot.native[0].status, 'connected'); assert.ok(!JSON.stringify(snapshot).includes('DO_NOT_PROJECT'));
29
+ await manager.save({ ...global, server: { ...server, args: ['changed.cjs'] } });
30
+ assert.equal((await manager.list(global)).servers[0].appliedSessions, 0, 'changed config not claimed as applied');
31
+ const reload = new Integrations(runtime, { home }); assert.equal((await reload.list(global)).servers.length, 1);
32
+ await assert.rejects(manager.save({ ...global, server: { ...server, env: { SECRET: 'never' } } }));
33
+ await assert.rejects(manager.save({ ...global, server: { ...server, args: ['--token', 'never'] } }));
34
+ await assert.rejects(manager.save({ ...global, harnessId: 'pi', server }), /no supported/);
35
+ await assert.rejects(manager.remove({ ...local, id: snapshot.servers[0].id }), /scope/);
36
+ await assert.rejects(manager.list({ ...local, cwd: 'relative' }));
37
+ const source = path.join(root, 'source'); await fs.mkdir(path.join(source, 'scripts'), { recursive: true });
38
+ await fs.writeFile(path.join(source, 'SKILL.md'), '---\nname: sample\ndescription: Sample skill\n---\n# Sample\n');
39
+ await fs.writeFile(path.join(source, 'scripts', 'sample.txt'), 'unchanged');
40
+ await manager.skillChange({ ...local, name: 'sample', source, action: 'install' });
41
+ const nativeFile = path.join(project, '.claude/skills/sample/SKILL.md'); assert.match(await fs.readFile(nativeFile, 'utf8'), /Sample/);
42
+ await assert.rejects(manager.skillChange({ ...local, name: 'sample', source, action: 'install' }), /already exists/);
43
+ await manager.skillChange({ ...local, name: 'sample', action: 'disable' });
44
+ await assert.rejects(fs.access(nativeFile)); assert.equal((await manager.list(local)).skills[0].enabled, false);
45
+ await manager.skillChange({ ...local, name: 'sample', action: 'enable' });
46
+ assert.equal(await fs.readFile(path.join(project, '.claude/skills/sample/scripts/sample.txt'), 'utf8'), 'unchanged');
47
+ await assert.rejects(manager.skillChange({ ...local, name: '../escape', source, action: 'install' }));
48
+ await assert.rejects(manager.skillChange({ ...local, name: 'sample', root, action: 'disable' }));
49
+ const link = path.join(project, '.claude/skills/linked');
50
+ await fs.symlink(source, link, process.platform === 'win32' ? 'junction' : 'dir');
51
+ assert.equal((await manager.list(local)).skills.find(s => s.name === 'linked').writable, false);
52
+ await assert.rejects(manager.skillChange({ ...local, name: 'linked', action: 'disable' }), /read-only/);
53
+ const collab = { command: 'native-node', args: ['bridge'], env: { LOCAL_BRIDGE: 'capability' } };
54
+ const acp = acpServers(selected.servers, collab);
55
+ assert.equal(acp.length, 2); assert.equal(acp[1].name, 'harness-mix'); assert.deepEqual(acp[0].env, []);
56
+ assert.ok(namedServers(selected.servers, collab)['hm-user-probe']);
57
+ const catalog = await manager.catalog(); assert.equal(catalog.harnesses.length, 16);
58
+ for (const a of adapters.values()) assert.equal(catalog.harnesses.find(h => h.id === a.manifest.id).mcp, a.manifest.integrations?.mcp === true);
59
+ // Exercise the real protocol dispatcher without starting native processes.
60
+ assert.equal((await NativeProtocol.prototype.request.call({ runtime }, 'codexhost/integrations/catalog', {})).harnesses.length, 16);
61
+ assert.equal((await NativeProtocol.prototype.request.call({ runtime }, 'codexhost/integrations/list', local)).skills.length, 2);
62
+ console.log('integrations: scope precedence, persistence, native status projection, no credential fields, unsupported capabilities, skill install/disable/restore, links, and protocol PASS');
63
+ }
64
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,63 @@
1
+ const { app, BrowserWindow, ipcMain } = require('electron');
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const assert = require('node:assert/strict');
5
+ const esbuild = require('esbuild');
6
+ const { Integrations } = require('../src/main/host/integrations');
7
+ const { buildAdapters } = require('../src/main/adapters');
8
+ const { NativeProtocol } = require('../src/main/native/protocol');
9
+ app.disableHardwareAcceleration();
10
+
11
+ app.whenReady().then(async () => {
12
+ const out = path.resolve('output/integrations-ui', `${Date.now()}`), project = path.join(out, 'project');
13
+ await fs.mkdir(project, { recursive: true });
14
+ const adapters = new Map(buildAdapters(() => {}).map(a => [a.manifest.id, a]));
15
+ const runtime = { adapters, sessions: new Map(), status: { claude: { available: true } }, store: { directory: path.join(out, 'data') }, resolveHarnessId: id => id };
16
+ runtime.integrations = new Integrations(runtime, { home: path.join(out, 'home') });
17
+ const source = path.join(out, 'skill-source'); await fs.mkdir(source); await fs.writeFile(path.join(source, 'SKILL.md'), '---\nname: ui-skill\ndescription: UI fixture\n---\n# Test\n');
18
+ const preload = path.join(out, 'preload.cjs');
19
+ await fs.writeFile(preload, `const {contextBridge,ipcRenderer}=require('electron');contextBridge.exposeInMainWorld('integrationTest',{request:(method,params)=>ipcRenderer.invoke('integrations-test',method,params)});`);
20
+ ipcMain.handle('integrations-test', (_event, method, params) => {
21
+ assert.ok(method.startsWith('codexhost/integrations/'));
22
+ return NativeProtocol.prototype.request.call({ runtime }, method, params);
23
+ });
24
+ const build = await esbuild.build({ stdin: { contents: `export {createIntegrationsSettingsPage} from './src/native-ui/renderer-extension/src/settings/integrations-page.ts'; export {mountRendererSettingsShell} from './src/native-ui/renderer-extension/src/settings/shell.ts'; export {createRendererSettingsPageRegistry} from './src/native-ui/renderer-extension/src/settings/core.ts'; export {rendererSettingsMessages} from './src/native-ui/renderer-extension/src/settings/localization.ts'; export {createRendererIntegrationsClient} from './src/native-ui/renderer-extension/src/renderer-integrations-client.ts';`, resolveDir: process.cwd() }, bundle: true, platform: 'browser', format: 'iife', globalName: 'IntegrationUI', write: false, alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') }, loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' } });
25
+ const consoleProblems = [];
26
+ const win = new BrowserWindow({ show: false, width: 1100, height: 900, webPreferences: { preload, contextIsolation: true, sandbox: true, offscreen: true } });
27
+ win.webContents.on('console-message', (_event, details) => { if (details.level === 'error' || details.level === 'warning') consoleProblems.push(details.message); });
28
+ try {
29
+ await win.loadURL('data:text/html,<html><head><title>Harness Mix Integrations QA</title><meta http-equiv="Content-Security-Policy" content="default-src %27none%27; script-src %27none%27; style-src %27unsafe-inline%27; img-src data:"></head><body style="background:%23191919;color:white;font-family:Arial"></body></html>');
30
+ await win.webContents.executeJavaScript(build.outputFiles[0].text);
31
+ const result = await win.webContents.executeJavaScript(`(async()=>{
32
+ const messages=IntegrationUI.rendererSettingsMessages('zh-CN');
33
+ const client=IntegrationUI.createRendererIntegrationsClient((m,p)=>window.integrationTest.request(m,p));
34
+ const registry=IntegrationUI.createRendererSettingsPageRegistry([IntegrationUI.createIntegrationsSettingsPage(messages,()=>client)]);
35
+ const shell=IntegrationUI.mountRendererSettingsShell(registry,document,messages); shell.openSettings();
36
+ const root=shell.root.shadowRoot;
37
+ const wait=async predicate=>{for(let i=0;i<100;i++){if(predicate())return;await new Promise(r=>setTimeout(r,30));}throw Error('UI timed out');};
38
+ await wait(()=>root.querySelector('select')?.options.length===16 && !root.querySelector('select').disabled);
39
+ const [harness,scope]=root.querySelectorAll('select');harness.value='claude';harness.dispatchEvent(new Event('change'));
40
+ await wait(()=>root.querySelector('.settings-integrations-form')&&!harness.disabled);
41
+ scope.value='project';scope.dispatchEvent(new Event('change'));
42
+ const cwd=root.querySelector('.settings-integrations-controls input');cwd.value=${JSON.stringify(project)};cwd.dispatchEvent(new Event('change'));
43
+ await wait(()=>root.querySelector('.settings-integrations-form')&&!harness.disabled);
44
+ const form=root.querySelector('form'); const fields=form.querySelectorAll('input');fields[0].value='ui-probe';fields[1].value='node';form.querySelector('textarea').value='["probe.cjs"]';form.requestSubmit();
45
+ await wait(()=>root.textContent.includes('ui-probe')&&!harness.disabled&&!!root.querySelector('.settings-integrations-row'));
46
+ const mcpRow=()=>Array.from(root.querySelectorAll('.settings-integrations-row')).find(r=>r.querySelector('strong')?.textContent==='ui-probe');
47
+ mcpRow().querySelector('button').click();await wait(()=>mcpRow()?.textContent.includes('已停用')&&!harness.disabled);
48
+ mcpRow().querySelector('button').click();await wait(()=>mcpRow()?.textContent.includes('已配置')&&!harness.disabled);
49
+ const skillForm=root.querySelectorAll('form')[1];const skillFields=skillForm.querySelectorAll('input');skillFields[0].value='ui-skill';skillFields[1].value=${JSON.stringify(source)};skillForm.requestSubmit();
50
+ const skillRow=()=>Array.from(root.querySelectorAll('.settings-integrations-row')).find(r=>r.querySelector('strong')?.textContent==='ui-skill');
51
+ await wait(()=>skillRow()&&!harness.disabled);skillRow().querySelector('button').click();await wait(()=>skillRow()?.textContent.includes('已停用')&&!harness.disabled);
52
+ skillRow().querySelector('button').click();await wait(()=>skillRow()?.textContent.includes('已发现')&&!harness.disabled);
53
+ return {mcpSaved:true,mcpToggled:true,skillInstalled:true,skillRestored:true,scope:scope.value,text:root.textContent.slice(-2000)};
54
+ })()`);
55
+ assert.equal(await win.webContents.executeJavaScript('document.title'), 'Harness Mix Integrations QA');
56
+ assert.ok(result.mcpSaved && result.skillRestored); assert.deepEqual(consoleProblems, []);
57
+ const list = await runtime.integrations.list({ harnessId: 'claude', scope: 'project', cwd: project });
58
+ assert.equal(list.servers[0].enabled, true); assert.equal(list.skills[0].enabled, true);
59
+ await fs.writeFile(path.join(out, 'settings.png'), (await win.webContents.capturePage()).toPNG());
60
+ await fs.writeFile(path.join(out, 'report.json'), JSON.stringify(result, null, 2));
61
+ console.log(`Integrations rendered UI -> typed client -> NativeProtocol -> real files PASS: ${out}`);
62
+ } finally { win.destroy(); app.quit(); }
63
+ }).catch(error => { console.error(error); app.exit(1); });