carthagent 1.0.7 → 1.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/cloud-onboarding-acceptance.md +65 -0
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/launch-readiness-live-results.md +65 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +175 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -0
- package/docs/release-notes-beta.2.md +60 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/cloud/client.mjs +13 -7
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1188 -0
- package/lib/opencode/cli.mjs +253 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +22 -5
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/providers/names.mjs +1 -1
- package/lib/tui/framing.mjs +1 -80
- package/lib/tui/models.mjs +1 -1
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +65 -36
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +506 -0
- package/plugins/carthagent/src/ui/client.ts +127 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +64 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/cloud-client.test.mjs +2 -2
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -1
- package/tests/experiential-extension.test.mjs +2 -2
- package/tests/experiential.test.mjs +1 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +55 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +525 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +33 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +83 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +248 -20
- package/tests/opencode-runtime.test.mjs +597 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/tui.test.mjs +2 -2
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +45 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +882 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +91 -65
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
quote: async (input) => { calls.push(['quote', input]); return { introEligible: false, terms: 'Builder is $10/month until cancelled.' }; },
|
|
24
|
+
checkout: async (input) => { calls.push(['checkout', input]); return { url: 'https://billing.example.test/checkout' }; },
|
|
25
|
+
portal: async (input) => { calls.push(['portal', input]); return { url: 'https://billing.example.test/portal' }; },
|
|
26
|
+
revokeCurrent: async (input) => calls.push(['revokeCurrent', input]),
|
|
27
|
+
revokeSession: async (input) => calls.push(['revokeSession', input]),
|
|
28
|
+
};
|
|
29
|
+
return { calls, ctx, cloud, handlers: createShipRpcHandlers(ctx, { cloudClient: cloud, cloudUrl: 'https://ship.example.test' }) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const call = {
|
|
33
|
+
signal: new AbortController().signal,
|
|
34
|
+
error(code, message, data) { throw Object.assign(new Error(message), { code, data }); },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
test('Ship account and billing RPCs use the active profile credential and injected cloud client', async () => {
|
|
38
|
+
const { calls, handlers } = setup();
|
|
39
|
+
const account = await handlers.shipAccount({}, call);
|
|
40
|
+
assert.equal(account.account.email, 'fixture@example.test');
|
|
41
|
+
assert.deepEqual((await handlers.shipUsage({ limit: 10 }, call)).usage.data[0].alias, 'fixture-model');
|
|
42
|
+
assert.equal((await handlers.shipBilling({ action: 'checkout' }, call)).url, 'https://billing.example.test/checkout');
|
|
43
|
+
assert.equal((await handlers.shipBilling({ action: 'portal' }, call)).url, 'https://billing.example.test/portal');
|
|
44
|
+
assert.deepEqual(calls.filter(([name]) => ['account', 'usage', 'checkout', 'portal'].includes(name)).map(([name, input]) => [name, input.accessToken, input.signal === call.signal]), [
|
|
45
|
+
['account', 'fixture-token', true],
|
|
46
|
+
['usage', 'fixture-token', true],
|
|
47
|
+
['checkout', 'fixture-token', true],
|
|
48
|
+
['portal', 'fixture-token', true],
|
|
49
|
+
]);
|
|
50
|
+
assert.ok(calls.some(([name, input]) => name === 'usage' && input.limit === 10));
|
|
51
|
+
assert.ok(calls.some(([name, input]) => name === 'checkout' && input.successUrl === 'https://ship.example.test/v1/browser/billing/complete' && input.priceId === 'builder'));
|
|
52
|
+
await handlers.shipBilling({ action: 'checkout', plan: 'max' }, call);
|
|
53
|
+
assert.equal(calls.at(-1)[1].priceId, 'max');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('native plan review uses authenticated server eligibility without creating checkout', async () => {
|
|
57
|
+
const { calls, handlers } = setup();
|
|
58
|
+
const result = await handlers.shipBilling({ action: 'quote', plan: 'builder' }, call);
|
|
59
|
+
assert.equal(result.quote.introEligible, false);
|
|
60
|
+
assert.equal(result.quote.terms, 'Builder is $10/month until cancelled.');
|
|
61
|
+
assert.ok(calls.some(([name, input]) => name === 'quote' && input.accessToken === 'fixture-token' && input.plan === 'builder' && input.signal === call.signal));
|
|
62
|
+
assert.equal(calls.some(([name]) => name === 'checkout'), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('Ship logout revokes either one session or current token and removes current connection', async () => {
|
|
66
|
+
const { calls, handlers } = setup();
|
|
67
|
+
assert.deepEqual(await handlers.shipLogout({ session: 'session-1' }, call), { revoked: true });
|
|
68
|
+
assert.deepEqual(await handlers.shipLogout({}, call), { revoked: true, credentialID: 'ship-credential' });
|
|
69
|
+
assert.ok(calls.some(([name, input]) => name === 'revokeSession' && input.sessionId === 'session-1'));
|
|
70
|
+
assert.ok(calls.some(([name, input]) => name === 'revokeCurrent' && input.accessToken === 'fixture-token'));
|
|
71
|
+
assert.ok(calls.some(([name, id]) => name === 'remove' && id === 'ship-credential'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Ship RPC requires its managed OAuth credential and honors cancellation before cloud traffic', async () => {
|
|
75
|
+
const { ctx, calls, handlers } = setup();
|
|
76
|
+
ctx.integration.connection.resolve = async () => ({ type: 'oauth', methodID: 'other', access: 'wrong' });
|
|
77
|
+
await assert.rejects(handlers.shipAccount({}, call), { code: 'unauthenticated', message: 'An active Carthagent Cloud OAuth connection is required.' });
|
|
78
|
+
assert.equal(calls.some(([name]) => name === 'account'), false);
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
controller.abort();
|
|
81
|
+
await assert.rejects(handlers.shipBilling({ action: 'portal' }, { signal: controller.signal }));
|
|
82
|
+
assert.equal(calls.some(([name]) => name === 'portal'), false);
|
|
83
|
+
});
|
|
@@ -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
|
+
});
|