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.
- 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/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-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/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 +1171 -0
- package/lib/opencode/cli.mjs +252 -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/tui/framing.mjs +1 -80
- 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 +59 -31
- 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 +69 -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 +496 -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/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -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 +31 -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 +71 -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 +245 -18
- 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/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 +43 -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 +876 -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 +87 -63
- 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,65 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { stopFixtureBrowser } from '../tools/native-fixture-browser-cleanup.mjs';
|
|
6
|
+
|
|
7
|
+
test('fixture browser cleanup keeps narrow authority and bounded verification', () => {
|
|
8
|
+
assert.match(stopFixtureBrowser, /Same-Identity/);
|
|
9
|
+
assert.match(stopFixtureBrowser, /CreationDate -eq/);
|
|
10
|
+
assert.match(stopFixtureBrowser, /Contains\(\$env:CTG_NATIVE_FIXTURE_URL\)/);
|
|
11
|
+
assert.match(stopFixtureBrowser, /taskkill.exe \/PID \$root.ProcessId \/T \/F/);
|
|
12
|
+
assert.match(stopFixtureBrowser, /AddSeconds\(5\)/);
|
|
13
|
+
assert.doesNotMatch(stopFixtureBrowser, /catch\s*\{/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Execute the production PowerShell with synthetic CIM snapshots and a mocked
|
|
17
|
+
// taskkill command. No browser is launched and no real process can be killed.
|
|
18
|
+
async function scenario(mode) {
|
|
19
|
+
const harness = `
|
|
20
|
+
$env:CTG_NATIVE_FIXTURE_URL = 'http://127.0.0.1:1234/verify-test'
|
|
21
|
+
$script:reads = 0
|
|
22
|
+
$script:kills = 0
|
|
23
|
+
$root = [pscustomobject]@{ ProcessId = 10; ParentProcessId = 1; CreationDate = [datetime]'2026-01-01'; Name = 'msedge.exe'; CommandLine = $env:CTG_NATIVE_FIXTURE_URL }
|
|
24
|
+
$child = [pscustomobject]@{ ProcessId = 11; ParentProcessId = 10; CreationDate = [datetime]'2026-01-02'; Name = 'msedge.exe'; CommandLine = '--child' }
|
|
25
|
+
$grandchild = [pscustomobject]@{ ProcessId = 12; ParentProcessId = 11; CreationDate = [datetime]'2026-01-03'; Name = 'msedge.exe'; CommandLine = '--grandchild' }
|
|
26
|
+
function Get-CimInstance {
|
|
27
|
+
$script:reads++
|
|
28
|
+
if ($script:reads -eq 1) { return @($grandchild, $child, $root) }
|
|
29
|
+
if ('${mode}' -eq 'already-exited') { return @() }
|
|
30
|
+
if ('${mode}' -eq 'pid-reused') {
|
|
31
|
+
return [pscustomobject]@{ ProcessId = 10; ParentProcessId = 1; CreationDate = [datetime]'2026-02-01'; Name = 'msedge.exe'; CommandLine = $env:CTG_NATIVE_FIXTURE_URL }
|
|
32
|
+
}
|
|
33
|
+
if ($script:reads -eq 2) { return @($root, $child, $grandchild) }
|
|
34
|
+
if ('${mode}' -eq 'live-child') { return @($grandchild) }
|
|
35
|
+
return @()
|
|
36
|
+
}
|
|
37
|
+
function taskkill.exe {
|
|
38
|
+
if (($args -join ' ') -ne '/PID 10 /T /F') { throw 'Unexpected kill target' }
|
|
39
|
+
$script:kills++
|
|
40
|
+
$global:LASTEXITCODE = 1
|
|
41
|
+
'simulated unsupported child termination'
|
|
42
|
+
}
|
|
43
|
+
${stopFixtureBrowser}
|
|
44
|
+
Write-Output "kills=$script:kills"
|
|
45
|
+
`;
|
|
46
|
+
return promisify(execFile)('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', harness], { timeout: 20_000, windowsHide: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const windows = { skip: process.platform !== 'win32' };
|
|
50
|
+
test('partial taskkill error succeeds only after all captured identities disappear', windows, async () => {
|
|
51
|
+
assert.match((await scenario('all-gone')).stdout, /kills=1/);
|
|
52
|
+
});
|
|
53
|
+
test('partial taskkill error fails with diagnostics for a surviving transitive child', windows, async () => {
|
|
54
|
+
await assert.rejects(scenario('live-child'), error => {
|
|
55
|
+
assert.match(error.stderr, /identities still alive: 12/);
|
|
56
|
+
assert.match(error.stderr, /taskkill exit 1/);
|
|
57
|
+
assert.match(error.stderr, /simulated unsupported child termination/);
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
for (const mode of ['already-exited', 'pid-reused']) {
|
|
62
|
+
test(`${mode} snapshot root is never targeted`, windows, async () => {
|
|
63
|
+
assert.match((await scenario(mode)).stdout, /kills=0/);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { resolveFixtureRuntime } from '../tools/native-fixture.mjs';
|
|
8
|
+
|
|
9
|
+
test('owned staging hash mismatch is rejected before product resolver can execute --version', async t => {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), 'ctg-fixture-runtime-'));
|
|
11
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
12
|
+
const platform = `${process.platform === 'win32' ? 'windows' : process.platform}-${process.arch}`;
|
|
13
|
+
const ownedRoot = join(root, 'owned');
|
|
14
|
+
const binary = join(ownedRoot, `cli-${platform}`, 'bin', process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
15
|
+
const marker = join(root, 'version-executed');
|
|
16
|
+
await mkdir(join(ownedRoot, `cli-${platform}`, 'bin'), { recursive: true });
|
|
17
|
+
await writeFile(binary, `#!/bin/sh\nprintf 'opencode v0.0.0-ctg.2.0.32\\n'\nprintf ran > '${marker}'\n`);
|
|
18
|
+
if (process.platform !== 'win32') await chmod(binary, 0o755);
|
|
19
|
+
const digest = createHash('sha256').update(await (await import('node:fs/promises')).readFile(binary)).digest('hex');
|
|
20
|
+
const releasePath = join(root, 'lib/opencode/runtime-release.json');
|
|
21
|
+
await mkdir(join(root, 'lib/opencode'), { recursive: true });
|
|
22
|
+
await writeFile(releasePath, JSON.stringify({
|
|
23
|
+
version: 1,
|
|
24
|
+
releaseVersion: '1.2.3',
|
|
25
|
+
compatibilityVersion: '2.0.32',
|
|
26
|
+
runtimeVersion: '0.0.0-ctg.2.0.32',
|
|
27
|
+
sourceRevision: '843a6c514e606b436a7fe713422ace0a93ad3d61',
|
|
28
|
+
patchSha256: 'b'.repeat(64),
|
|
29
|
+
lockSha256: 'c'.repeat(64),
|
|
30
|
+
assets: { [platform]: {
|
|
31
|
+
name: `opencode-${platform}.tar.gz`, archiveSha256: 'd'.repeat(64),
|
|
32
|
+
executableSha256: digest === 'a'.repeat(64) ? 'b'.repeat(64) : 'a'.repeat(64),
|
|
33
|
+
} },
|
|
34
|
+
}));
|
|
35
|
+
let productResolverCalled = false;
|
|
36
|
+
await assert.rejects(resolveFixtureRuntime({
|
|
37
|
+
packageRoot: root,
|
|
38
|
+
env: { CTG_OWNED_OPENCODE_RUNTIME: ownedRoot },
|
|
39
|
+
productRuntime: {
|
|
40
|
+
async resolveOpenCode() { productResolverCalled = true; throw Error('resolver should not be reached'); },
|
|
41
|
+
async runtimeIdentity() { throw Error('identity should not be reached'); },
|
|
42
|
+
},
|
|
43
|
+
}), /binary SHA-256 mismatch/);
|
|
44
|
+
assert.equal(productResolverCalled, false);
|
|
45
|
+
await assert.rejects(import('node:fs/promises').then(fs => fs.access(marker)), { code: 'ENOENT' });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('product runtime selection is reverified by the trusted helper and preserves separate versions', async t => {
|
|
49
|
+
const root = await mkdtemp(join(tmpdir(), 'ctg-fixture-runtime-selected-'));
|
|
50
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
51
|
+
const platform = `${process.platform === 'win32' ? 'windows' : process.platform}-${process.arch}`;
|
|
52
|
+
const ownedRoot = join(root, 'owned');
|
|
53
|
+
const binary = join(ownedRoot, `cli-${platform}`, 'bin', process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
|
54
|
+
await mkdir(join(ownedRoot, `cli-${platform}`, 'bin'), { recursive: true });
|
|
55
|
+
await writeFile(binary, 'already resolver-verified fixture bytes');
|
|
56
|
+
const expectedManifest = { trusted: true };
|
|
57
|
+
const releasePath = join(root, 'release.json');
|
|
58
|
+
await writeFile(releasePath, JSON.stringify(expectedManifest));
|
|
59
|
+
const options = { sentinel: Symbol('same-options-object') };
|
|
60
|
+
const calls = [];
|
|
61
|
+
const env = {
|
|
62
|
+
CTG_OWNED_OPENCODE_RUNTIME: ownedRoot,
|
|
63
|
+
CTG_TEST_RUNTIME_RELEASE_MANIFEST: releasePath,
|
|
64
|
+
CTG_TEST_RUNTIME_SHA256: 'e'.repeat(64),
|
|
65
|
+
};
|
|
66
|
+
const verified = {
|
|
67
|
+
binary,
|
|
68
|
+
compatibilityVersion: '2.0.32',
|
|
69
|
+
runtimeVersion: '0.0.0-ctg.2.0.32',
|
|
70
|
+
binarySha256: 'f'.repeat(64),
|
|
71
|
+
sourceRevision: '843a6c514e606b436a7fe713422ace0a93ad3d61',
|
|
72
|
+
patchSha256: 'b'.repeat(64),
|
|
73
|
+
lockSha256: 'c'.repeat(64),
|
|
74
|
+
platform,
|
|
75
|
+
};
|
|
76
|
+
let resolverCalled = false;
|
|
77
|
+
const result = await resolveFixtureRuntime({
|
|
78
|
+
packageRoot: root,
|
|
79
|
+
env,
|
|
80
|
+
options,
|
|
81
|
+
productRuntime: {
|
|
82
|
+
async resolveOpenCode(received) {
|
|
83
|
+
resolverCalled = true;
|
|
84
|
+
assert.equal(received, options);
|
|
85
|
+
return binary;
|
|
86
|
+
},
|
|
87
|
+
async runtimeIdentity(receivedBinary, received) {
|
|
88
|
+
assert.equal(receivedBinary, binary);
|
|
89
|
+
assert.equal(received, options);
|
|
90
|
+
return {
|
|
91
|
+
kind: 'ctg-owned', binaryPath: binary, binarySha256: verified.binarySha256,
|
|
92
|
+
version: verified.runtimeVersion, sourceRevision: verified.sourceRevision,
|
|
93
|
+
patchSha256: verified.patchSha256, lockSha256: verified.lockSha256,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
testRuntimeLoader: async input => {
|
|
98
|
+
calls.push(input);
|
|
99
|
+
return verified;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
assert.equal(resolverCalled, true);
|
|
103
|
+
assert.equal(calls.length, 2, 'preflight selected staging bytes and reverify selected product identity');
|
|
104
|
+
assert.equal(calls[0].env.CTG_TEST_RUNTIME_SHA256, 'e'.repeat(64));
|
|
105
|
+
assert.deepEqual(calls[0].expectedManifest, expectedManifest);
|
|
106
|
+
assert.equal(result.binary, binary);
|
|
107
|
+
assert.equal(result.runtimeIdentity.compatibilityVersion, '2.0.32');
|
|
108
|
+
assert.equal(result.runtimeIdentity.runtimeVersion, '0.0.0-ctg.2.0.32');
|
|
109
|
+
assert.equal(result.runtimeIdentity.binarySha256, verified.binarySha256);
|
|
110
|
+
assert.equal(result.runtimeIdentity.kind, 'ctg-owned');
|
|
111
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { fixtureStopDiagnostics, sanitizedNativeOutputTail } from '../tools/native-fixture-stop-diagnostics.mjs';
|
|
4
|
+
|
|
5
|
+
const child = { pid: 21359, exitCode: null, signalCode: null, killed: false };
|
|
6
|
+
const input = { child, output: 'INFO stopping\nERROR shutdown timeout', elapsedMs: 5001.4 };
|
|
7
|
+
|
|
8
|
+
test('native output tail is bounded and emits only fixed metadata, never raw secrets', () => {
|
|
9
|
+
const secrets = ['0123456789abcdef', 'Basic b3BlbmNvZGU6c2VjcmV0', 'Bearer fixture.token.fixture', 'offline-refresh', 'verify-secret', 'unknown-unlabelled-secret'];
|
|
10
|
+
const output = `${'DEBUG old\n'.repeat(3000)}${secrets.map(secret => `ERROR shutdown authorization password=${secret}`).join('\n')}`;
|
|
11
|
+
const tail = sanitizedNativeOutputTail(output);
|
|
12
|
+
assert.equal(tail.length, 20);
|
|
13
|
+
const serialized = JSON.stringify(tail);
|
|
14
|
+
for (const secret of secrets) assert.ok(!serialized.includes(secret));
|
|
15
|
+
assert.deepEqual(tail.at(-1), { level: 'ERROR', markers: ['shutdown', 'error'] });
|
|
16
|
+
assert.ok(serialized.length < 4096);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('failure evidence snapshots child state and queries only exact PID without argv or environment', async () => {
|
|
20
|
+
const evidence = await fixtureStopDiagnostics(input, { platform: 'darwin', run: async (command, args, options) => {
|
|
21
|
+
assert.equal(command, 'ps');
|
|
22
|
+
assert.deepEqual(args, ['-p', '21359', '-o', 'pid=,ppid=,stat=,etime=,time=']);
|
|
23
|
+
assert.deepEqual(options, { timeout: 2000, maxBuffer: 4096, windowsHide: true });
|
|
24
|
+
child.exitCode = 0;
|
|
25
|
+
return { stdout: ' 21359 123 Z+ 00:05 0:00.01\n' };
|
|
26
|
+
} });
|
|
27
|
+
child.exitCode = null;
|
|
28
|
+
assert.equal(evidence.elapsedMs, 5001);
|
|
29
|
+
assert.equal(evidence.child.exitCode, null);
|
|
30
|
+
assert.deepEqual(evidence.processState, { status: 'observed', state: '21359 123 Z+ 00:05 0:00.01' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('query errors and unexpected output cannot leak command output or secrets', async () => {
|
|
34
|
+
for (const run of [
|
|
35
|
+
async () => ({ stdout: 'password=secret' }),
|
|
36
|
+
async () => { throw Object.assign(new Error('secret'), { stderr: 'Bearer secret', code: 1 }); },
|
|
37
|
+
async () => { throw Object.assign(new Error('secret'), { code: 'ENOENT', killed: true }); },
|
|
38
|
+
]) {
|
|
39
|
+
const evidence = await fixtureStopDiagnostics(input, { run });
|
|
40
|
+
assert.ok(!JSON.stringify(evidence).includes('secret'));
|
|
41
|
+
assert.ok(['unrecognized-output', 'query-failed'].includes(evidence.processState.status));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Windows queries exact PID and accepts only fixed boolean state', async () => {
|
|
46
|
+
const evidence = await fixtureStopDiagnostics(input, { platform: 'win32', run: async (command, args) => {
|
|
47
|
+
assert.equal(command, 'powershell.exe');
|
|
48
|
+
assert.match(args.at(-1), /Get-Process -Id 21359 /);
|
|
49
|
+
assert.doesNotMatch(args.at(-1), /CommandLine|Environment/);
|
|
50
|
+
return { stdout: 'pid=21359 responding=True\r\n' };
|
|
51
|
+
} });
|
|
52
|
+
assert.equal(evidence.processState.status, 'observed');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('missing PID skips OS query, empty result records absence, exited child state is retained', async () => {
|
|
56
|
+
const missing = await fixtureStopDiagnostics({ ...input, child: { ...child, pid: undefined } }, { run: () => assert.fail('must not run') });
|
|
57
|
+
assert.equal(missing.processState.status, 'no-child-pid');
|
|
58
|
+
const exited = await fixtureStopDiagnostics({ ...input, child: { ...child, signalCode: 'SIGTERM' } }, { run: async () => ({ stdout: '' }) });
|
|
59
|
+
assert.equal(exited.child.signalCode, 'SIGTERM');
|
|
60
|
+
assert.equal(exited.processState.status, 'not-found');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('real read-only process query observes this exact test process', async () => {
|
|
64
|
+
const evidence = await fixtureStopDiagnostics({ ...input, child: { ...child, pid: process.pid } });
|
|
65
|
+
assert.equal(evidence.processState.status, 'observed');
|
|
66
|
+
assert.match(evidence.processState.state, new RegExp(`^(?:pid=)?${process.pid}\\s`));
|
|
67
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const helpers = join(root, 'tools/native-glyph-helpers.py');
|
|
10
|
+
const candidates = [
|
|
11
|
+
process.env.PYTHON,
|
|
12
|
+
join(root, '.venv', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python'),
|
|
13
|
+
process.platform === 'win32' ? 'python.exe' : 'python3',
|
|
14
|
+
'python',
|
|
15
|
+
].filter(Boolean);
|
|
16
|
+
const resolveExecutable = candidate => {
|
|
17
|
+
if (existsSync(candidate)) return candidate;
|
|
18
|
+
for (const directory of (process.env.PATH || '').split(delimiter)) {
|
|
19
|
+
const path = join(directory, candidate);
|
|
20
|
+
if (existsSync(path)) return path;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
const python = candidates.map(resolveExecutable).find(Boolean);
|
|
25
|
+
const skipPython = python ? false : 'Python was not found; set PYTHON to an interpreter path';
|
|
26
|
+
|
|
27
|
+
test('glyph child environment does not inherit the draft-runtime release token', { skip: skipPython }, () => {
|
|
28
|
+
const result = spawnSync(python, ['-c', `import importlib.util, pathlib
|
|
29
|
+
spec=importlib.util.spec_from_file_location('helpers', ${JSON.stringify(helpers)})
|
|
30
|
+
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
31
|
+
root=pathlib.Path('/tmp/isolated')
|
|
32
|
+
env=module.child_environment(root, root/'config', 'ascii', {'PATH':'/bin', 'CTG_RUNTIME_GITHUB_TOKEN':'sentinel'})
|
|
33
|
+
assert 'CTG_RUNTIME_GITHUB_TOKEN' not in env
|
|
34
|
+
assert env['CTG_TUI_GLYPHS']=='ascii'
|
|
35
|
+
assert env['HOME']==str(root/'home')
|
|
36
|
+
print('PASS')`], { encoding: 'utf8' });
|
|
37
|
+
assert.equal(result.status, 0, result.stderr);
|
|
38
|
+
assert.match(result.stdout, /PASS/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('composer assertion requires the exact ordered Unicode payload on its rendered row', { skip: skipPython }, () => {
|
|
42
|
+
const result = spawnSync(python, ['-c', `import importlib.util
|
|
43
|
+
spec=importlib.util.spec_from_file_location('helpers', ${JSON.stringify(helpers)})
|
|
44
|
+
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
45
|
+
expected=module.USER_TEXT
|
|
46
|
+
assert module.assert_composer_payload('logo █▀▀ OpenCode\\n┃ '+expected+'\\n')
|
|
47
|
+
mutants=[
|
|
48
|
+
'logo '+expected+'\\n┃ CTG_GLYPH_PAYLOAD 漢字 café → • −',
|
|
49
|
+
'logo '+expected+'\\n┃ '+expected.replace('漢','?'),
|
|
50
|
+
'logo '+expected+'\\n┃ '+expected.replace('→ •','• →'),
|
|
51
|
+
'logo '+expected+'\\n┃ '+expected.replace('café','cafe'),
|
|
52
|
+
]
|
|
53
|
+
for screen in mutants:
|
|
54
|
+
try: module.assert_composer_payload(screen)
|
|
55
|
+
except AssertionError: pass
|
|
56
|
+
else: raise AssertionError('mutated composer unexpectedly passed')
|
|
57
|
+
print('PASS')`], { encoding: 'utf8' });
|
|
58
|
+
assert.equal(result.status, 0, result.stderr);
|
|
59
|
+
assert.match(result.stdout, /PASS/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('glyph screen tracks CJK wide cells and combining marks without losing exact composer text', { skip: skipPython }, () => {
|
|
63
|
+
const result = spawnSync(python, ['-c', `import importlib.util
|
|
64
|
+
spec=importlib.util.spec_from_file_location('helpers', ${JSON.stringify(helpers)})
|
|
65
|
+
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
66
|
+
screen=module.GlyphScreen(rows=3, columns=50)
|
|
67
|
+
text=module.USER_TEXT
|
|
68
|
+
screen.feed(('\\x1b[1;1H┃ '+text).encode())
|
|
69
|
+
row=screen.visible().splitlines()[0]
|
|
70
|
+
assert text in row, repr(row)
|
|
71
|
+
assert screen.x == 3 + sum(module.GlyphScreen.char_width(ch) for ch in text)
|
|
72
|
+
assert module.GlyphScreen.char_width('漢') == 2
|
|
73
|
+
assert module.GlyphScreen.char_width('\\u0301') == 0
|
|
74
|
+
print('PASS')`], { encoding: 'utf8' });
|
|
75
|
+
assert.equal(result.status, 0, result.stderr);
|
|
76
|
+
assert.match(result.stdout, /PASS/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('native glyph PTY entrypoint imports its hyphenated sibling helper', {
|
|
80
|
+
skip: skipPython || (process.platform === 'win32' ? 'The PTY entrypoint depends on Unix fcntl; portable helper contracts are tested above' : false),
|
|
81
|
+
}, () => {
|
|
82
|
+
const result = spawnSync(python, ['-c', `import importlib.util, pathlib
|
|
83
|
+
entry=pathlib.Path(${JSON.stringify(join(root, 'tools/native-glyph-pty.py'))})
|
|
84
|
+
spec=importlib.util.spec_from_file_location('native_glyph_pty_entry', entry)
|
|
85
|
+
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
86
|
+
assert module.USER_TEXT == 'CTG_GLYPH_PAYLOAD 漢字 café → • − …'
|
|
87
|
+
assert module.GlyphScreen.char_width('漢') == 2
|
|
88
|
+
assert callable(module.assert_composer_payload)
|
|
89
|
+
print('PASS')`], { encoding: 'utf8' });
|
|
90
|
+
assert.equal(result.status, 0, result.stderr);
|
|
91
|
+
assert.match(result.stdout, /PASS/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
test('glyph screen records fragmented OSC titles for final-binary branding assertions', { skip: skipPython }, () => {
|
|
96
|
+
const result = spawnSync(python, ['-c', `import importlib.util
|
|
97
|
+
spec=importlib.util.spec_from_file_location('helpers', ${JSON.stringify(helpers)})
|
|
98
|
+
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
99
|
+
screen=module.GlyphScreen()
|
|
100
|
+
for chunk in [b'\\x1b]2;Cart', b'hagent\\x07', b'\\x1b]0;CTG | Ship\\x1b', b'\\\\']:
|
|
101
|
+
screen.feed(chunk)
|
|
102
|
+
assert screen.title_updates == ['Carthagent', 'CTG | Ship'], screen.title_updates
|
|
103
|
+
assert screen.visible().strip() == ''
|
|
104
|
+
print('PASS')`], { encoding: 'utf8' });
|
|
105
|
+
assert.equal(result.status, 0, result.stderr);
|
|
106
|
+
assert.match(result.stdout, /PASS/);
|
|
107
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { CTG_RUNTIME_SOURCE, CTG_RUNTIME_PATCH_SHA256, CTG_RUNTIME_LOCK_SHA256 } from '../lib/opencode/runtime.mjs';
|
|
9
|
+
import { createManualProjects } from '../tools/native-manual-projects.mjs';
|
|
10
|
+
import { readManualRuntimeManifest } from '../tools/manual-runtime-manifest.mjs';
|
|
11
|
+
import { readRuntimeReleaseManifest } from '../lib/opencode/runtime.mjs';
|
|
12
|
+
import { prepareManualKit } from '../tools/prepare-native-manual.mjs';
|
|
13
|
+
|
|
14
|
+
test('manual fixtures have green baselines, red feature acceptance, clean git, and no dependencies', async (t) => {
|
|
15
|
+
const temp = await mkdtemp(join(tmpdir(), 'ctg-manual-projects-'));
|
|
16
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
17
|
+
const result = await createManualProjects(join(temp, 'projects'), {
|
|
18
|
+
packages: 3,
|
|
19
|
+
});
|
|
20
|
+
const env = { ...process.env };
|
|
21
|
+
delete env.NODE_TEST_CONTEXT;
|
|
22
|
+
for (const name of ['tiny', 'product', 'scale']) {
|
|
23
|
+
const cwd = join(result.root, name);
|
|
24
|
+
const manifest = JSON.parse(
|
|
25
|
+
await readFile(join(cwd, 'package.json'), 'utf8'),
|
|
26
|
+
);
|
|
27
|
+
assert.equal(manifest.dependencies, undefined);
|
|
28
|
+
const baseline = spawnSync(
|
|
29
|
+
process.execPath,
|
|
30
|
+
['--test', 'test/*.test.mjs'],
|
|
31
|
+
{ cwd, env, encoding: 'utf8' },
|
|
32
|
+
);
|
|
33
|
+
assert.equal(baseline.status, 0, baseline.stdout + baseline.stderr);
|
|
34
|
+
const acceptance = spawnSync(
|
|
35
|
+
process.execPath,
|
|
36
|
+
['--test', 'acceptance/*.test.mjs'],
|
|
37
|
+
{ cwd, env, encoding: 'utf8' },
|
|
38
|
+
);
|
|
39
|
+
assert.equal(
|
|
40
|
+
acceptance.status,
|
|
41
|
+
1,
|
|
42
|
+
'new-feature acceptance must fail against the baseline',
|
|
43
|
+
);
|
|
44
|
+
const status = spawnSync('git', ['status', '--porcelain'], {
|
|
45
|
+
cwd,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
});
|
|
48
|
+
assert.equal(status.status, 0, status.stderr);
|
|
49
|
+
assert.equal(status.stdout, '');
|
|
50
|
+
assert.ok(result.projects[name].files > 3);
|
|
51
|
+
}
|
|
52
|
+
const sentinel = join(result.root, 'keep.txt');
|
|
53
|
+
await writeFile(sentinel, 'keep');
|
|
54
|
+
await assert.rejects(createManualProjects(result.root), { code: 'EEXIST' });
|
|
55
|
+
assert.equal(await readFile(sentinel, 'utf8'), 'keep');
|
|
56
|
+
await assert.rejects(
|
|
57
|
+
createManualProjects(join(temp, 'invalid'), { packages: 0 }),
|
|
58
|
+
/integer from 2 to 1024/,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('manual kit preparation refuses existing directories and removes only its own failed staging', async (t) => {
|
|
63
|
+
const temp = await mkdtemp(join(tmpdir(), 'ctg-manual-kit-safety-'));
|
|
64
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
65
|
+
const sentinel = join(temp, 'keep.txt');
|
|
66
|
+
await writeFile(sentinel, 'keep');
|
|
67
|
+
await assert.rejects(prepareManualKit(temp, { assetsDirectory: temp }), {
|
|
68
|
+
code: 'EEXIST',
|
|
69
|
+
});
|
|
70
|
+
assert.equal(await readFile(sentinel, 'utf8'), 'keep');
|
|
71
|
+
const runtimeManifest = join(temp, 'runtime-release.json');
|
|
72
|
+
const manifest = JSON.parse(await readFile(new URL('../lib/opencode/runtime-release.json', import.meta.url), 'utf8'));
|
|
73
|
+
manifest.releaseVersion = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
74
|
+
Object.assign(manifest, { sourceRevision: CTG_RUNTIME_SOURCE, patchSha256: CTG_RUNTIME_PATCH_SHA256, lockSha256: CTG_RUNTIME_LOCK_SHA256 });
|
|
75
|
+
const target = `${process.platform === 'win32' ? 'windows' : process.platform}-${process.arch}`;
|
|
76
|
+
manifest.assets = { [target]: manifest.assets[target] };
|
|
77
|
+
const bytes = JSON.stringify(manifest);
|
|
78
|
+
await writeFile(runtimeManifest, bytes);
|
|
79
|
+
const manifestSha256 = createHash('sha256').update(bytes).digest('hex');
|
|
80
|
+
const missing = join(temp, 'failed-kit');
|
|
81
|
+
await assert.rejects(
|
|
82
|
+
prepareManualKit(missing, { assetsDirectory: temp, runtimeManifest, manifestSha256 }),
|
|
83
|
+
/Could not acquire/,
|
|
84
|
+
);
|
|
85
|
+
await assert.rejects(readFile(join(missing, 'manual-kit.json')), {
|
|
86
|
+
code: 'ENOENT',
|
|
87
|
+
});
|
|
88
|
+
assert.equal(await readFile(sentinel, 'utf8'), 'keep');
|
|
89
|
+
await assert.rejects(
|
|
90
|
+
prepareManualKit(join(temp, 'invalid'), { packages: 0 }),
|
|
91
|
+
/integer from 2 to 1024/,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
test('packaged manual kits retain published pins after the development recipe advances', async () => {
|
|
97
|
+
const bytes = await readFile(new URL('../lib/opencode/runtime-release.json', import.meta.url));
|
|
98
|
+
const published = JSON.parse(bytes);
|
|
99
|
+
const current = await readManualRuntimeManifest();
|
|
100
|
+
assert.deepEqual(current.manifest, published);
|
|
101
|
+
assert.equal(current.manifest.patchSha256, CTG_RUNTIME_PATCH_SHA256);
|
|
102
|
+
assert.equal(current.manifestSha256, createHash('sha256').update(bytes).digest('hex'));
|
|
103
|
+
await assert.rejects(readManualRuntimeManifest({ manifestSha256: '0'.repeat(64) }), /checksum mismatch/i);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('default manual preparation reaches verified acquisition without requiring development provenance', async t => {
|
|
107
|
+
const temp = await mkdtemp(join(tmpdir(), 'ctg-manual-published-'));
|
|
108
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
109
|
+
const destination = join(temp, 'kit');
|
|
110
|
+
await assert.rejects(prepareManualKit(destination, { assetsDirectory: temp }), /Could not acquire/);
|
|
111
|
+
await assert.rejects(readFile(join(destination, 'manual-kit.json')), { code: 'ENOENT' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('explicit manual candidates cannot borrow the packaged-release recipe exception', async t => {
|
|
115
|
+
const temp = await mkdtemp(join(tmpdir(), 'ctg-manual-explicit-release-'));
|
|
116
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
117
|
+
const runtimeManifest = join(temp, 'published-copy.json');
|
|
118
|
+
const manifest = JSON.parse(await readFile(new URL('../lib/opencode/runtime-release.json', import.meta.url), 'utf8'));
|
|
119
|
+
manifest.releaseVersion = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
120
|
+
manifest.patchSha256 = '0'.repeat(64);
|
|
121
|
+
const bytes = JSON.stringify(manifest);
|
|
122
|
+
await writeFile(runtimeManifest, bytes);
|
|
123
|
+
await assert.rejects(readManualRuntimeManifest({ runtimeManifest, manifestSha256: createHash('sha256').update(bytes).digest('hex') }), /patchSha256.*build recipe/i);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('manual candidate overrides require an independent digest and reject altered or incompatible manifests', async t => {
|
|
127
|
+
const temp = await mkdtemp(join(tmpdir(), 'ctg-manual-kit-trust-'));
|
|
128
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
129
|
+
const runtimeManifest = join(temp, 'candidate.json');
|
|
130
|
+
const destination = join(temp, 'kit');
|
|
131
|
+
await writeFile(runtimeManifest, '{}');
|
|
132
|
+
await assert.rejects(prepareManualKit(destination, { runtimeManifest }), /manifest.*SHA-256|digest/i);
|
|
133
|
+
await assert.rejects(prepareManualKit(destination, { runtimeManifest, manifestSha256: '0'.repeat(64) }), /manifest.*SHA-256|checksum|digest/i);
|
|
134
|
+
const manifestSha256 = createHash('sha256').update('{}').digest('hex');
|
|
135
|
+
await assert.rejects(prepareManualKit(destination, { runtimeManifest, manifestSha256 }), /incompatible|invalid/i);
|
|
136
|
+
await assert.rejects(readFile(join(destination, 'manual-kit.json')), { code: 'ENOENT' });
|
|
137
|
+
await writeFile(runtimeManifest, '{"altered":true}');
|
|
138
|
+
await assert.rejects(prepareManualKit(destination, { runtimeManifest, manifestSha256 }), /manifest.*SHA-256|checksum|digest/i);
|
|
139
|
+
await assert.rejects(readFile(join(destination, 'manual-kit.json')), { code: 'ENOENT' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('single-platform manual candidates bind current recipe bytes without weakening complete release manifests', async t => {
|
|
143
|
+
const temp = await mkdtemp(join(tmpdir(), 'ctg-manual-candidate-'));
|
|
144
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
145
|
+
const runtimeManifest = join(temp, 'candidate.json');
|
|
146
|
+
const manifest = JSON.parse(await readFile(new URL('../lib/opencode/runtime-release.json', import.meta.url), 'utf8'));
|
|
147
|
+
manifest.releaseVersion = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
148
|
+
Object.assign(manifest, { sourceRevision: CTG_RUNTIME_SOURCE, patchSha256: CTG_RUNTIME_PATCH_SHA256, lockSha256: CTG_RUNTIME_LOCK_SHA256 });
|
|
149
|
+
const asset = manifest.assets['linux-x64'];
|
|
150
|
+
manifest.assets = { 'linux-x64': asset };
|
|
151
|
+
const save = async () => {
|
|
152
|
+
const bytes = JSON.stringify(manifest);
|
|
153
|
+
await writeFile(runtimeManifest, bytes);
|
|
154
|
+
return { runtimeManifest, manifestSha256: createHash('sha256').update(bytes).digest('hex') };
|
|
155
|
+
};
|
|
156
|
+
const current = await readManualRuntimeManifest(await save());
|
|
157
|
+
assert.deepEqual(Object.keys(current.manifest.assets), ['linux-x64']);
|
|
158
|
+
await assert.rejects(readRuntimeReleaseManifest(runtimeManifest), /incompatible|invalid/i);
|
|
159
|
+
for (const field of ['sourceRevision', 'patchSha256', 'lockSha256']) {
|
|
160
|
+
const original = manifest[field];
|
|
161
|
+
manifest[field] = '0'.repeat(original.length);
|
|
162
|
+
await assert.rejects(readManualRuntimeManifest(await save()), /build recipe/i);
|
|
163
|
+
manifest[field] = original;
|
|
164
|
+
}
|
|
165
|
+
const inherited = Object.fromEntries([['__proto__', { ...asset, name: undefined }]]);
|
|
166
|
+
for (const assets of [{}, { unknown: asset }, { 'linux-x64': { ...asset, executableSha256: 'invalid' } }, inherited]) {
|
|
167
|
+
manifest.assets = assets;
|
|
168
|
+
await assert.rejects(readManualRuntimeManifest(await save()), /invalid|incompatible/i);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { createJiti } = require('jiti');
|
|
8
|
+
const jiti = createJiti(import.meta.url);
|
|
9
|
+
const { runModelCommand } = await jiti.import('../plugins/carthagent/src/ui/model-command.ts');
|
|
10
|
+
|
|
11
|
+
const ship = { providerID: 'experiential-labs', modelID: 'deepseek-v4.1-flash', id: 'deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash' };
|
|
12
|
+
const other = { providerID: 'demo', modelID: 'alpha', id: 'alpha', name: 'Alpha' };
|
|
13
|
+
|
|
14
|
+
function host(overrides = {}) {
|
|
15
|
+
const calls = { native: [], switched: [], selects: [], toasts: [], syncs: 0 };
|
|
16
|
+
return {
|
|
17
|
+
calls,
|
|
18
|
+
value: {
|
|
19
|
+
sync: async () => { calls.syncs++; },
|
|
20
|
+
list: () => [ship, other],
|
|
21
|
+
select: async (options) => { calls.selects.push(options.map((option) => option.title)); return options[0]?.value; },
|
|
22
|
+
switchModel: async (sessionID, model) => { calls.switched.push({ sessionID, model }); },
|
|
23
|
+
openNativePicker: (query) => { calls.native.push(query); },
|
|
24
|
+
providerName: (id) => (id === 'experiential-labs' ? 'Carthagent Ship' : id),
|
|
25
|
+
toast: (message, variant) => { calls.toasts.push({ message, variant }); },
|
|
26
|
+
...overrides,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('pre-session /model opens the native configured-model picker with its argument and never switches a session', async () => {
|
|
32
|
+
const bare = host();
|
|
33
|
+
await runModelCommand(bare.value, { arg: '' });
|
|
34
|
+
assert.deepEqual(bare.calls.native, ['']);
|
|
35
|
+
const named = host();
|
|
36
|
+
await runModelCommand(named.value, { arg: 'deepseek' });
|
|
37
|
+
assert.deepEqual(named.calls.native, ['deepseek']);
|
|
38
|
+
for (const run of [bare, named]) {
|
|
39
|
+
assert.deepEqual(run.calls.switched, []);
|
|
40
|
+
assert.deepEqual(run.calls.selects, []);
|
|
41
|
+
assert.equal(run.calls.toasts.some((toast) => /Open or create a session/.test(toast.message)), false);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('existing-session /model keeps filtering by argument and commits the pick to that session only', async () => {
|
|
46
|
+
const run = host();
|
|
47
|
+
await runModelCommand(run.value, { sessionID: 'ses_existing', arg: 'deepseek' });
|
|
48
|
+
assert.equal(run.calls.syncs, 1);
|
|
49
|
+
assert.deepEqual(run.calls.selects, [['Carthagent Ship/deepseek-v4.1-flash']]);
|
|
50
|
+
assert.deepEqual(run.calls.switched, [{ sessionID: 'ses_existing', model: { providerID: 'experiential-labs', id: 'deepseek-v4.1-flash' } }]);
|
|
51
|
+
assert.deepEqual(run.calls.native, []);
|
|
52
|
+
assert.deepEqual(run.calls.toasts, [{ message: 'Selected Carthagent Ship/deepseek-v4.1-flash.', variant: 'success' }]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('cancelling the existing-session picker changes nothing', async () => {
|
|
56
|
+
const run = host({ select: async () => undefined });
|
|
57
|
+
await runModelCommand(run.value, { sessionID: 'ses_existing', arg: '' });
|
|
58
|
+
assert.deepEqual(run.calls.switched, []);
|
|
59
|
+
assert.deepEqual(run.calls.toasts, []);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('plugin /model dispatch routes through the shared command and the native model.list command', async () => {
|
|
63
|
+
const tui = await readFile(new URL('../plugins/carthagent/src/tui.tsx', import.meta.url), 'utf8');
|
|
64
|
+
assert.match(tui, /runModelCommand\(/);
|
|
65
|
+
assert.match(tui, /keymap\.dispatch\("model\.list", query\)/);
|
|
66
|
+
assert.doesNotMatch(tui, /Open or create a session before selecting its model/);
|
|
67
|
+
});
|