carthagent 1.0.7 → 1.1.0-beta.1

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 (294) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/beta-rewards-e2e-plan.md +48 -0
  4. package/docs/beta-rewards-e2e-results.md +84 -0
  5. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  6. package/docs/glyph-review/test-isolation-v1.md +26 -0
  7. package/docs/glyph-review/typecheck-base.log +1 -0
  8. package/docs/glyph-review/typecheck-v4.log +1 -0
  9. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  10. package/docs/help-dispatch-review.md +69 -0
  11. package/docs/launch-readiness-live-results.md +65 -0
  12. package/docs/legacy-cli.md +150 -0
  13. package/docs/live-theme-plugin-api.md +18 -0
  14. package/docs/native-cli.md +102 -0
  15. package/docs/native-compatibility.md +16 -0
  16. package/docs/native-daily-quests.md +132 -0
  17. package/docs/native-glyph-pty.md +59 -0
  18. package/docs/native-manual-testing.md +336 -0
  19. package/docs/native-slash-acceptance-handoff.md +49 -0
  20. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  21. package/docs/native-terminal-performance.md +310 -0
  22. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  23. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  24. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  35. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  36. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  37. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  38. package/docs/native-terminal-semantics.md +89 -0
  39. package/docs/native-upgrade.md +175 -0
  40. package/docs/opencode-composed-source-review.md +89 -0
  41. package/docs/opencode-migration.md +225 -9
  42. package/docs/opencode-owned-runtime.md +241 -0
  43. package/docs/opencode-runtime.md +12 -0
  44. package/docs/opencode-theme-provider-proof/README.md +55 -0
  45. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  46. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  47. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  48. package/docs/owned-host-controls-proof.md +39 -0
  49. package/docs/owned-runtime-release-manifest.md +42 -0
  50. package/docs/owned-visual-clear-proof.md +40 -0
  51. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  52. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  53. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  54. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  61. package/docs/release-notes-beta.1.md +60 -0
  62. package/docs/release-notes-native-draft.md +73 -0
  63. package/docs/release-notes-preview.10.md +121 -0
  64. package/docs/release-notes-preview.11.md +76 -0
  65. package/docs/release-notes-preview.12.md +55 -0
  66. package/docs/release-notes-preview.13.md +68 -0
  67. package/docs/release-notes-preview.9.md +130 -0
  68. package/docs/source-preview.md +120 -0
  69. package/evaluate.mjs +28 -1
  70. package/lib/budget.mjs +1 -1
  71. package/lib/codegraph.mjs +1 -1
  72. package/lib/delivery-review.mjs +19 -6
  73. package/lib/delivery.mjs +3 -2
  74. package/lib/environment.mjs +8 -0
  75. package/lib/guidance.mjs +1 -1
  76. package/lib/opencode/cli-runtime.mjs +1171 -0
  77. package/lib/opencode/cli.mjs +252 -0
  78. package/lib/opencode/credentials.mjs +63 -8
  79. package/lib/opencode/demo.mjs +53 -0
  80. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  81. package/lib/opencode/evaluation.mjs +32 -12
  82. package/lib/opencode/headless.mjs +22 -5
  83. package/lib/opencode/provider-policy.mjs +23 -0
  84. package/lib/opencode/review.mjs +37 -16
  85. package/lib/opencode/runtime-artifact.mjs +335 -0
  86. package/lib/opencode/runtime-github-transport.mjs +121 -0
  87. package/lib/opencode/runtime-release.json +36 -0
  88. package/lib/opencode/runtime.mjs +299 -20
  89. package/lib/opencode/service.mjs +77 -35
  90. package/lib/opencode/session-options.mjs +6 -2
  91. package/lib/opencode/update.mjs +222 -0
  92. package/lib/opencode/workspace.mjs +1 -1
  93. package/lib/planning-advice.mjs +2 -2
  94. package/lib/providers/experiential.mjs +2 -2
  95. package/lib/tui/framing.mjs +1 -80
  96. package/lib/update.mjs +10 -1
  97. package/package.json +33 -7
  98. package/plugins/carthagent/README.md +23 -0
  99. package/plugins/carthagent/src/bootstrap.ts +1 -1
  100. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  101. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  102. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  103. package/plugins/carthagent/src/index.ts +71 -24
  104. package/plugins/carthagent/src/rpc.ts +51 -6
  105. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  106. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  107. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  108. package/plugins/carthagent/src/server/delivery.ts +227 -112
  109. package/plugins/carthagent/src/server/framing.mjs +80 -0
  110. package/plugins/carthagent/src/server/policy.ts +1 -1
  111. package/plugins/carthagent/src/server/providers.ts +59 -31
  112. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  113. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  114. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  115. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  116. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  117. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  118. package/plugins/carthagent/src/testing.ts +5 -0
  119. package/plugins/carthagent/src/tui.tsx +496 -0
  120. package/plugins/carthagent/src/ui/client.ts +127 -0
  121. package/plugins/carthagent/src/ui/commands.ts +70 -0
  122. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  123. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  124. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  125. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  126. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  127. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  128. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  129. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  130. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  131. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  132. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  133. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  134. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  135. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  136. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  137. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  138. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  139. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  140. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  141. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  142. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  143. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  144. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  145. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  146. package/plugins/carthagent/tests/commands.test.ts +107 -0
  147. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  148. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  149. package/plugins/carthagent/themes/carthage.json +67 -0
  150. package/plugins/carthagent/tui.ts +26 -1
  151. package/script/ctg/build-owned-runtime.sh +166 -0
  152. package/tests/budget-engine.test.mjs +4 -3
  153. package/tests/cli.test.mjs +52 -96
  154. package/tests/daily-evidence.test.mjs +62 -0
  155. package/tests/delivery-review.test.mjs +57 -2
  156. package/tests/delivery.test.mjs +22 -1
  157. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  158. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  159. package/tests/installed-runtime-contract.test.mjs +43 -0
  160. package/tests/native-cli-host-smoke.mjs +457 -0
  161. package/tests/native-daily-editor-focus.test.mjs +213 -0
  162. package/tests/native-daily-editor.test.mjs +43 -0
  163. package/tests/native-daily-quests-layout.test.mjs +55 -0
  164. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  165. package/tests/native-daily-quests-ui.test.mjs +778 -0
  166. package/tests/native-daily-source.test.mjs +525 -0
  167. package/tests/native-demo-ui.test.mjs +48 -0
  168. package/tests/native-evaluation-admission.test.mjs +24 -0
  169. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  170. package/tests/native-fixture-runtime.test.mjs +111 -0
  171. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  172. package/tests/native-glyph-pty.test.mjs +107 -0
  173. package/tests/native-manual-projects.test.mjs +170 -0
  174. package/tests/native-model-command.test.mjs +67 -0
  175. package/tests/native-owned-runtime-env.test.mjs +140 -0
  176. package/tests/native-paired-guards.test.mjs +464 -0
  177. package/tests/native-provider-policy.test.mjs +31 -0
  178. package/tests/native-pty-invocation.test.mjs +114 -0
  179. package/tests/native-pty-runtime.test.mjs +45 -0
  180. package/tests/native-resume-session.test.mjs +46 -0
  181. package/tests/native-review-consumer.test.mjs +286 -0
  182. package/tests/native-runtime-identity.test.mjs +214 -0
  183. package/tests/native-select-mouse.test.mjs +23 -0
  184. package/tests/native-ship-guidance.test.mjs +16 -0
  185. package/tests/native-ship-parity.test.mjs +526 -0
  186. package/tests/native-ship-plan.test.mjs +85 -0
  187. package/tests/native-ship-rpc.test.mjs +71 -0
  188. package/tests/native-ui-commands.test.mjs +6 -0
  189. package/tests/native-voyages-layout.test.mjs +28 -0
  190. package/tests/native-voyages-navigation.test.mjs +169 -0
  191. package/tests/native-voyages-rpc.test.mjs +397 -0
  192. package/tests/native-voyages-ui.test.mjs +307 -0
  193. package/tests/opencode-atomic-write.test.mjs +55 -0
  194. package/tests/opencode-cli.test.mjs +1312 -0
  195. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  196. package/tests/opencode-delivery.test.mjs +16 -0
  197. package/tests/opencode-providers.test.mjs +245 -18
  198. package/tests/opencode-runtime.test.mjs +597 -8
  199. package/tests/opencode-session-controls.test.mjs +15 -0
  200. package/tests/opencode-ship-rpc.test.mjs +71 -0
  201. package/tests/opencode-update.test.mjs +475 -0
  202. package/tests/opencode-workspace.test.mjs +51 -2
  203. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  204. package/tests/release-workflow.test.mjs +46 -0
  205. package/tests/runtime-artifact.test.mjs +269 -0
  206. package/tests/runtime-development-trust.test.mjs +231 -0
  207. package/tests/runtime-github-transport.test.mjs +132 -0
  208. package/tests/runtime-token-env.test.mjs +23 -0
  209. package/tests/server.test.mjs +3 -1
  210. package/tests/ship-telemetry.test.mjs +1 -1
  211. package/tests/source-preview.test.mjs +253 -0
  212. package/tests/test_native_terminal_probe.py +76 -0
  213. package/tests/test_native_terminal_semantics.py +232 -0
  214. package/tests/update.test.mjs +42 -5
  215. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  216. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  217. package/tests/workspace-live.test.mjs +127 -0
  218. package/tools/benchmark/diagnose-request.mjs +59 -36
  219. package/tools/benchmark/eval-common.mjs +3 -3
  220. package/tools/benchmark/evaluators.test.mjs +4 -4
  221. package/tools/benchmark/guard.test.mjs +35 -2
  222. package/tools/benchmark/guard.ts +20 -7
  223. package/tools/benchmark/planning-startup.test.mjs +30 -34
  224. package/tools/benchmark/runner.mjs +12 -7
  225. package/tools/benchmark/runner.test.mjs +23 -7
  226. package/tools/benchmark/upstream-error.test.mjs +16 -2
  227. package/tools/generate-owned-runtime-release.mjs +217 -0
  228. package/tools/installed-runtime-contract.mjs +16 -0
  229. package/tools/manual-runtime-manifest.mjs +39 -0
  230. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  231. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  232. package/tools/native-fixture.mjs +199 -40
  233. package/tools/native-glyph-helpers.py +127 -0
  234. package/tools/native-glyph-pty.py +144 -0
  235. package/tools/native-manual-projects.mjs +259 -0
  236. package/tools/native-manual.mjs +338 -0
  237. package/tools/native-model-picker-acceptance.py +306 -0
  238. package/tools/native-owned-runtime-env.sh +47 -0
  239. package/tools/native-paired-guards.mjs +282 -0
  240. package/tools/native-paired-manifest.mjs +72 -0
  241. package/tools/native-paired-orchestration.sh +51 -0
  242. package/tools/native-plugin-readiness.mjs +33 -0
  243. package/tools/native-plugin-readiness.test.mjs +53 -0
  244. package/tools/native-pty-invocation.mjs +26 -0
  245. package/tools/native-pty-runtime.mjs +83 -0
  246. package/tools/native-pty-smoke.py +147 -24
  247. package/tools/native-public-repo-shell-result.mjs +13 -0
  248. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  249. package/tools/native-runtime-identity.mjs +201 -0
  250. package/tools/native-slash-acceptance-env.py +38 -0
  251. package/tools/native-terminal-semantics.py +876 -0
  252. package/tools/owned-shell-observer/index.ts +16 -0
  253. package/tools/owned-shell-observer/package.json +1 -0
  254. package/tools/owned-visual-clear-env.py +48 -0
  255. package/tools/owned-visual-clear-model.py +21 -0
  256. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  257. package/tools/prepare-native-manual.mjs +122 -0
  258. package/tools/run-native-terminal-paired.sh +403 -0
  259. package/tools/runtime-token-env.mjs +8 -0
  260. package/tools/seed-native-switch-fixture.mjs +79 -0
  261. package/tools/test-native-controls.mjs +3 -1
  262. package/tools/test-native-daily-quests.mjs +65 -0
  263. package/tools/test-native-default-cli.mjs +106 -0
  264. package/tools/test-native-delivery-host.mjs +102 -20
  265. package/tools/test-native-evaluation.mjs +7 -3
  266. package/tools/test-native-glyph-pty.mjs +94 -0
  267. package/tools/test-native-host.mjs +87 -63
  268. package/tools/test-native-outcomes.mjs +3 -1
  269. package/tools/test-native-package.mjs +206 -38
  270. package/tools/test-native-performance.mjs +26 -0
  271. package/tools/test-native-product.mjs +90 -0
  272. package/tools/test-native-pty.mjs +34 -9
  273. package/tools/test-native-public-repo.mjs +224 -0
  274. package/tools/test-native-refresh-reentry.mjs +106 -0
  275. package/tools/test-native-report.mjs +27 -20
  276. package/tools/test-native-review.mjs +75 -0
  277. package/tools/test-native-runtime.mjs +120 -0
  278. package/tools/test-native-ship-models.mjs +176 -0
  279. package/tools/test-native-ship-walkthrough.mjs +96 -0
  280. package/tools/test-native-slash-commands.mjs +125 -0
  281. package/tools/test-native-slash-commands.py +242 -0
  282. package/tools/test-native-swe-host.mjs +224 -55
  283. package/tools/test-native-tasks.mjs +77 -0
  284. package/tools/test-native-terminal-performance.py +887 -0
  285. package/tools/test-native-terminal-readiness.sh +85 -0
  286. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  287. package/tools/test-native-visual-clear-installed.mjs +48 -0
  288. package/tools/test-owned-host-controls.mjs +238 -0
  289. package/tools/test-owned-opencode-runtime.mjs +35 -0
  290. package/tools/test-owned-visual-clear-env.py +120 -0
  291. package/tools/test-owned-visual-clear.py +373 -0
  292. package/tools/verify-draft-runtime-gate.mjs +138 -0
  293. package/tools/verify-owned-runtime-release.mjs +114 -0
  294. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,71 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createRequire } from 'node:module';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const { createJiti } = require('jiti');
7
+ const jiti = createJiti(import.meta.url);
8
+ const { createShipRpcHandlers } = await jiti.import('../plugins/carthagent/src/server/ship-rpc.ts');
9
+
10
+ function setup() {
11
+ const calls = [];
12
+ const active = { id: 'ship-credential', type: 'credential', method: 'oauth', integrationID: 'experiential-labs' };
13
+ const ctx = { integration: {
14
+ connection: {
15
+ active: async (id) => { calls.push(['active', id]); return active; },
16
+ resolve: async (connection) => { calls.push(['resolve', connection.id]); return { type: 'oauth', methodID: 'carthagent-device', access: 'fixture-token' }; },
17
+ },
18
+ transform: async (fn) => fn({ remove: (id) => calls.push(['remove', id]) }),
19
+ } };
20
+ const cloud = {
21
+ account: async (input) => { calls.push(['account', input]); return { email: 'fixture@example.test', sessions: [] }; },
22
+ usage: async (input) => { calls.push(['usage', input]); return { data: [{ alias: 'fixture-model', inputTokens: 12, outputTokens: 8 }] }; },
23
+ checkout: async (input) => { calls.push(['checkout', input]); return { url: 'https://billing.example.test/checkout' }; },
24
+ portal: async (input) => { calls.push(['portal', input]); return { url: 'https://billing.example.test/portal' }; },
25
+ revokeCurrent: async (input) => calls.push(['revokeCurrent', input]),
26
+ revokeSession: async (input) => calls.push(['revokeSession', input]),
27
+ };
28
+ return { calls, ctx, cloud, handlers: createShipRpcHandlers(ctx, { cloudClient: cloud, cloudUrl: 'https://ship.example.test' }) };
29
+ }
30
+
31
+ const call = {
32
+ signal: new AbortController().signal,
33
+ error(code, message, data) { throw Object.assign(new Error(message), { code, data }); },
34
+ };
35
+
36
+ test('Ship account and billing RPCs use the active profile credential and injected cloud client', async () => {
37
+ const { calls, handlers } = setup();
38
+ const account = await handlers.shipAccount({}, call);
39
+ assert.equal(account.account.email, 'fixture@example.test');
40
+ assert.deepEqual((await handlers.shipUsage({ limit: 10 }, call)).usage.data[0].alias, 'fixture-model');
41
+ assert.equal((await handlers.shipBilling({ action: 'checkout' }, call)).url, 'https://billing.example.test/checkout');
42
+ assert.equal((await handlers.shipBilling({ action: 'portal' }, call)).url, 'https://billing.example.test/portal');
43
+ assert.deepEqual(calls.filter(([name]) => ['account', 'usage', 'checkout', 'portal'].includes(name)).map(([name, input]) => [name, input.accessToken, input.signal === call.signal]), [
44
+ ['account', 'fixture-token', true],
45
+ ['usage', 'fixture-token', true],
46
+ ['checkout', 'fixture-token', true],
47
+ ['portal', 'fixture-token', true],
48
+ ]);
49
+ assert.ok(calls.some(([name, input]) => name === 'usage' && input.limit === 10));
50
+ assert.ok(calls.some(([name, input]) => name === 'checkout' && input.successUrl === 'https://ship.example.test/account'));
51
+ });
52
+
53
+ test('Ship logout revokes either one session or current token and removes current connection', async () => {
54
+ const { calls, handlers } = setup();
55
+ assert.deepEqual(await handlers.shipLogout({ session: 'session-1' }, call), { revoked: true });
56
+ assert.deepEqual(await handlers.shipLogout({}, call), { revoked: true, credentialID: 'ship-credential' });
57
+ assert.ok(calls.some(([name, input]) => name === 'revokeSession' && input.sessionId === 'session-1'));
58
+ assert.ok(calls.some(([name, input]) => name === 'revokeCurrent' && input.accessToken === 'fixture-token'));
59
+ assert.ok(calls.some(([name, id]) => name === 'remove' && id === 'ship-credential'));
60
+ });
61
+
62
+ test('Ship RPC requires its managed OAuth credential and honors cancellation before cloud traffic', async () => {
63
+ const { ctx, calls, handlers } = setup();
64
+ ctx.integration.connection.resolve = async () => ({ type: 'oauth', methodID: 'other', access: 'wrong' });
65
+ await assert.rejects(handlers.shipAccount({}, call), { code: 'unauthenticated', message: 'An active Carthagent Ship OAuth connection is required.' });
66
+ assert.equal(calls.some(([name]) => name === 'account'), false);
67
+ const controller = new AbortController();
68
+ controller.abort();
69
+ await assert.rejects(handlers.shipBilling({ action: 'portal' }, { signal: controller.signal }));
70
+ assert.equal(calls.some(([name]) => name === 'portal'), false);
71
+ });
@@ -0,0 +1,6 @@
1
+ import { createJiti } from 'jiti';
2
+
3
+ // Keep the native UI manifest and theme-preservation suite in the root test
4
+ // glob so `npm test` and the release quality gate actually execute it.
5
+ const jiti = createJiti(import.meta.url);
6
+ await jiti.import('../plugins/carthagent/tests/commands.test.ts');
@@ -0,0 +1,28 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+
5
+ const source = await readFile(new URL('../plugins/carthagent/src/ui/voyages.tsx', import.meta.url), 'utf8');
6
+
7
+ test('Voyages keeps wrapping summary rows out of the action layout and exposes native profile scrolling', () => {
8
+ assert.match(source, /<scrollbox ref=\{summary\} flexBasis=\{0\} flexGrow=\{1\} minHeight=\{1\}/);
9
+ assert.match(source, /<select flexBasis=\{0\} flexGrow=\{1\} flexShrink=\{0\} minHeight=\{3\}/);
10
+ for (const text of source.matchAll(/<text\b([^>]*)>/g)) assert.match(text[1], /flexShrink=\{0\}/);
11
+ assert.match(source, /<box flexDirection="column" flexShrink=\{0\}>/);
12
+ assert.match(source, /bind: "ctrl\+u"[^\n]*summary\?\.scrollBy\(-1\)/);
13
+ assert.match(source, /bind: "ctrl\+d"[^\n]*summary\?\.scrollBy\(1\)/);
14
+ assert.doesNotMatch(source.slice(source.indexOf('context.keymap.layer'), source.indexOf('let activeDialog')), /mode: "global"/);
15
+ });
16
+
17
+ test('Voyages retains keyboard navigation and every protected summary field inside the scroll viewport', () => {
18
+ const summary = source.slice(source.indexOf('<scrollbox'), source.indexOf('</scrollbox>'));
19
+ for (const field of ['profile.name', 'profile.shipName', 'profile.coins', 'profile.level', 'profile.totalXp', 'profile.km', 'profile.weeklyKm', 'profile.config.skin', 'profile.config.hairStyle', 'profile.config.bottoms', 'profile.config.hull', 'snapshot().pending', 'view().busy', 'view().error', 'view().notice']) {
20
+ assert.ok(summary.includes(field), `${field} must remain readable in the scrolling summary`);
21
+ }
22
+ const choices = source.slice(source.indexOf('<select '));
23
+ assert.match(choices, /name: "pageup", action: "move-up-fast"/);
24
+ assert.match(choices, /name: "pagedown", action: "move-down-fast"/);
25
+ assert.match(choices, /options=\{options\(\)\}/);
26
+ assert.match(choices, /focused=\{context.keymap.mode.current\(\) === "base"\}/);
27
+ assert.match(choices, /onSelect=\{\(_index, option\) => select\(option\?\.value\)\}/);
28
+ });
@@ -0,0 +1,169 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createVoyagesNavigationState } from '../plugins/carthagent/src/ui/voyages-navigation-state.mjs';
5
+
6
+ const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; };
7
+ const account = { profile: { name: 'Captain', shipName: 'Ship', coins: 10, level: 3, totalXp: 200, config: { private: true }, owned: ['secret'] }, items: [{ private: true }], pending: { quoteId: 'private' } };
8
+ const assignments = { accountKey: 'private-account', slate: { day: '2026-10-30', endsAt: '2026-10-31T00:00:00Z', items: [{ id: 'quest-one', definition: { title: 'Protect a boundary', criteria: ['private'] }, status: 'open' }] }, records: [{ request: { evidence: 'private-claim', consent: true } }] };
9
+ function fixture(overrides = {}) {
10
+ const updates = [], calls = [];
11
+ const controller = createVoyagesNavigationState({
12
+ changed: (state) => updates.push(structuredClone(state)),
13
+ profile: async (signal) => { calls.push(['profile', signal]); return account; },
14
+ daily: async (signal) => { calls.push(['daily', signal]); return assignments; },
15
+ ...overrides,
16
+ });
17
+ return { controller, calls, updates, last: () => updates.at(-1) };
18
+ }
19
+
20
+ test('navigation loads only when selected, reads once per activation and retains no evidence, consent or purchase state', async () => {
21
+ const f = fixture();
22
+ await f.controller.refresh();
23
+ assert.equal(f.calls.length, 0);
24
+ await f.controller.activate(true);
25
+ assert.deepEqual(f.calls.map(([name]) => name), ['profile', 'daily']);
26
+ assert.equal(f.calls[0][1], f.calls[1][1]);
27
+ assert.deepEqual(f.last(), {
28
+ busy: false, error: '',
29
+ profile: { name: 'Captain', shipName: 'Ship', coins: 10, level: 3, totalXp: 200 },
30
+ daily: { day: '2026-10-30', endsAt: '2026-10-31T00:00:00Z', items: [{ id: 'quest-one', title: 'Protect a boundary', status: 'open' }] },
31
+ });
32
+ assert.doesNotMatch(JSON.stringify(f.last()), /private|consent|evidence|quoteId|owned|criteria/);
33
+ await f.controller.activate(true);
34
+ assert.equal(f.calls.length, 2);
35
+ f.controller.dispose();
36
+ });
37
+
38
+ test('switching back to Projects synchronously clears protected summaries and aborts without routes or mutations', async () => {
39
+ const f = fixture();
40
+ await f.controller.activate(true);
41
+ const signal = f.calls[0][1];
42
+ f.controller.activate(false);
43
+ assert.equal(signal.aborted, true);
44
+ assert.deepEqual(f.last(), { busy: false, error: '', profile: undefined, daily: undefined });
45
+ await f.controller.refresh();
46
+ assert.equal(f.calls.length, 2);
47
+ await f.controller.activate(true);
48
+ assert.equal(f.calls.length, 4);
49
+ f.controller.dispose();
50
+ });
51
+
52
+ test('hidden pane cannot resurrect a late authenticated response', async () => {
53
+ const profile = deferred(), daily = deferred();
54
+ const f = fixture({ profile: () => profile.promise, daily: () => daily.promise });
55
+ const pending = f.controller.activate(true);
56
+ assert.equal(f.last().busy, true);
57
+ f.controller.activate(false);
58
+ profile.resolve(account); daily.resolve(assignments);
59
+ await pending;
60
+ assert.equal(f.last().profile, undefined);
61
+ assert.equal(f.last().daily, undefined);
62
+ f.controller.dispose();
63
+ });
64
+
65
+ test('either authentication failure hides both account-bound summaries and never renders server messages', async () => {
66
+ for (const boundary of ['profile', 'daily']) {
67
+ const f = fixture({ [boundary]: async () => { throw { type: boundary === 'profile' ? 'voyagesError' : 'dailyQuestError', data: { code: 'authentication_required' }, message: 'private token' }; } });
68
+ await f.controller.activate(true);
69
+ assert.equal(f.last().profile, undefined);
70
+ assert.equal(f.last().daily, undefined);
71
+ assert.equal(f.last().error, 'authentication_required');
72
+ assert.doesNotMatch(JSON.stringify(f.updates), /private token/);
73
+ f.controller.dispose();
74
+ }
75
+ });
76
+
77
+ test('untrusted error codes and transport messages cannot leak through the rail', async () => {
78
+ for (const error of [new Error('private message'), { type: 'dailyQuestError', data: { code: 'private\u001b[31m' } }, { type: 'voyagesError', data: { code: 'x'.repeat(65) } }]) {
79
+ const f = fixture({ daily: async () => { throw error; } });
80
+ await f.controller.activate(true);
81
+ assert.match(f.last().error, /^(voyages_request_failed|voyages_error)$/);
82
+ assert.doesNotMatch(JSON.stringify(f.updates), /private/);
83
+ f.controller.dispose();
84
+ }
85
+ });
86
+
87
+ test('refresh clears stale account data before either new response is available', async () => {
88
+ let gate;
89
+ const f = fixture({ profile: async () => gate ? gate.promise : account });
90
+ await f.controller.activate(true);
91
+ gate = deferred();
92
+ const pending = f.controller.refresh();
93
+ assert.equal(f.last().busy, true);
94
+ assert.equal(f.last().profile, undefined);
95
+ assert.equal(f.last().daily, undefined);
96
+ gate.resolve(account);
97
+ await pending;
98
+ assert.equal(f.last().profile.name, 'Captain');
99
+ f.controller.dispose();
100
+ });
101
+
102
+ test('account/location invalidation aborts old requests and only publishes the fresh pair', async () => {
103
+ const old = deferred();
104
+ let count = 0;
105
+ const signals = [];
106
+ const f = fixture({ profile: (signal) => { signals.push(signal); return ++count === 1 ? old.promise : Promise.resolve({ profile: { ...account.profile, name: 'New captain' } }); } });
107
+ const previous = f.controller.activate(true);
108
+ const next = f.controller.contextChanged();
109
+ assert.equal(signals[0].aborted, true);
110
+ await next;
111
+ old.resolve(account);
112
+ await previous;
113
+ assert.equal(f.last().profile.name, 'New captain');
114
+ f.controller.dispose();
115
+ });
116
+
117
+ test('disconnect clears synchronously, does not auto-reconnect, and permits an explicit read-only refresh', async () => {
118
+ const f = fixture();
119
+ await f.controller.activate(true);
120
+ f.controller.disconnect();
121
+ assert.equal(f.last().profile, undefined);
122
+ assert.equal(f.last().daily, undefined);
123
+ assert.equal(f.calls.length, 2);
124
+ await f.controller.refresh();
125
+ assert.equal(f.calls.length, 4);
126
+ f.controller.dispose();
127
+ });
128
+
129
+ test('duplicate refresh while busy is ignored; disposal revokes every late callback', async () => {
130
+ const old = deferred();
131
+ const f = fixture({ profile: () => old.promise });
132
+ const pending = f.controller.activate(true);
133
+ await f.controller.refresh();
134
+ f.controller.dispose();
135
+ const length = f.updates.length;
136
+ old.resolve(account);
137
+ await pending;
138
+ await f.controller.refresh();
139
+ await f.controller.activate(true);
140
+ await f.controller.contextChanged();
141
+ assert.equal(f.updates.length, length);
142
+ assert.equal(f.last().profile, undefined);
143
+ });
144
+
145
+ test('timeout clears busy and fails closed without an unhandled promise', async () => {
146
+ const timeout = new AbortController();
147
+ const f = fixture({ timeoutSignal: () => timeout.signal, daily: (signal) => new Promise((_resolve, reject) => signal.addEventListener('abort', () => reject(signal.reason), { once: true })) });
148
+ const pending = f.controller.activate(true);
149
+ timeout.abort(new Error('private transport information'));
150
+ await pending;
151
+ assert.equal(f.last().busy, false);
152
+ assert.equal(f.last().profile, undefined);
153
+ assert.equal(f.last().daily, undefined);
154
+ assert.doesNotMatch(JSON.stringify(f.updates), /private/);
155
+ f.controller.dispose();
156
+ });
157
+
158
+ test('native extension is optional; navigation cannot itself accept consent or purchase', async () => {
159
+ const root = new URL('../plugins/carthagent/src/', import.meta.url);
160
+ const [tui, pane, adapter, daily, voyages] = await Promise.all(['tui.tsx', 'ui/voyages-navigation.tsx', 'ui/native-navigation.ts', 'ui/daily-quests.tsx', 'ui/voyages.tsx'].map((file) => readFile(new URL(file, root), 'utf8')));
161
+ assert.match(tui, /if \(navigation\) cleanups.push\(navigation.register/);
162
+ assert.match(adapter, /navigation\?: NativeNavigation/);
163
+ assert.match(pane, /!props.active \|\| context.keymap.mode.current\(\) !== "navigation" \|\| !props.focused/);
164
+ assert.match(pane, /showDescription=\{!props.compact\}/);
165
+ assert.match(pane, /Badges & friends: no native views yet/);
166
+ assert.doesNotMatch(pane, /\.submit\(|\.retry\(|\.purchase\(|\.quote\(|\.customize\(|\.equip\(|dialog\.confirm|consent: true/);
167
+ assert.match(daily, /controller.refresh\(\).then\(\(\) => \{ if \(props.questID\) return controller.select\(props.questID\)/);
168
+ assert.match(voyages, /view\(\).busy \|\| context.keymap.mode.current\(\) !== "base"/);
169
+ });