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
package/tests/update.test.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
fetchLatestRelease,
|
|
11
11
|
latestReleaseCached,
|
|
12
12
|
normalizeRelease,
|
|
13
|
+
npmInstallSpec,
|
|
13
14
|
staleGlobalInstallPaths,
|
|
14
15
|
updateCheckEnabled,
|
|
15
16
|
} from '../lib/update.mjs';
|
|
@@ -82,6 +83,16 @@ test('normalizeRelease rejects install specs that do not install the release tag
|
|
|
82
83
|
assert.equal(normalizeRelease(manifest).installSpec, 'github:B-icy/Carthagent#v0.3.0');
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
test('normalizeRelease binds the install ref exactly to the validated version', () => {
|
|
87
|
+
for (const tag of ['main', 'v0.3.1', 'v0.3.0%2Fmain', ' v0.3.0', 'v0.3.0\n']) {
|
|
88
|
+
assert.equal(normalizeRelease({ version: '0.3.0', tag }), null, `rejected tag ${JSON.stringify(tag)}`);
|
|
89
|
+
}
|
|
90
|
+
assert.equal(normalizeRelease({ version: '1.2.3-beta.2', tag_name: 'v1.2.3-beta.2' })?.installSpec,
|
|
91
|
+
'github:B-icy/Carthagent#v1.2.3-beta.2');
|
|
92
|
+
assert.equal(normalizeRelease({ version: '1.2.3-beta.2', tag_name: 'v1.2.3' }), null);
|
|
93
|
+
assert.equal(normalizeRelease({ version: '01.2.3', tag: 'v01.2.3' }), null);
|
|
94
|
+
});
|
|
95
|
+
|
|
85
96
|
test('fetchLatestRelease falls back to GitHub when the manifest fails validation', async () => {
|
|
86
97
|
// A control-plane 200 whose installSpec is rejected must not dead-end the
|
|
87
98
|
// update — the GitHub release still resolves.
|
|
@@ -93,13 +104,30 @@ test('fetchLatestRelease falls back to GitHub when the manifest fails validation
|
|
|
93
104
|
}
|
|
94
105
|
return { status: 200, json: async () => ({ tag_name: 'v0.3.1', published_at: '2026-03-01T00:00:00Z' }) };
|
|
95
106
|
};
|
|
96
|
-
const { release, source } = await fetchLatestRelease({ cloudUrl: 'https://cloud.test', fetchImpl });
|
|
107
|
+
const { release, source } = await fetchLatestRelease({ cloudUrl: 'https://cloud.test', fetchImpl, channel: 'stable' });
|
|
97
108
|
assert.equal(source, 'github');
|
|
98
109
|
assert.equal(release.version, '0.3.1');
|
|
99
110
|
assert.equal(release.installSpec, 'github:B-icy/Carthagent#v0.3.1');
|
|
100
111
|
assert.equal(urls.length, 2);
|
|
101
112
|
});
|
|
102
113
|
|
|
114
|
+
test('fetchLatestRelease forwards cancellation and does not fall back after an abort', async () => {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
let calls = 0;
|
|
117
|
+
await assert.rejects(fetchLatestRelease({
|
|
118
|
+
cloudUrl: 'https://cloud.test',
|
|
119
|
+
channel: 'stable',
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
fetchImpl: async (_url, options) => {
|
|
122
|
+
calls++;
|
|
123
|
+
assert.equal(options.signal.aborted, false);
|
|
124
|
+
controller.abort(new Error('cancelled'));
|
|
125
|
+
throw controller.signal.reason;
|
|
126
|
+
},
|
|
127
|
+
}), /cancelled/);
|
|
128
|
+
assert.equal(calls, 1, 'an aborted control-plane request must not trigger GitHub fallback');
|
|
129
|
+
});
|
|
130
|
+
|
|
103
131
|
// ---- latestReleaseCached
|
|
104
132
|
|
|
105
133
|
test('latestReleaseCached fetches once, then serves the cache', () => withTempDir(async dir => {
|
|
@@ -108,11 +136,11 @@ test('latestReleaseCached fetches once, then serves the cache', () => withTempDi
|
|
|
108
136
|
const fetchImpl = async () => { calls++; return { status: 200, json: async () => manifest }; };
|
|
109
137
|
// Pinned clock: a slow runner must not outrun the cache TTL mid-test.
|
|
110
138
|
const t0 = Date.now();
|
|
111
|
-
const first = await latestReleaseCached({ cachePath, intervalMs: 1000, now: t0, fetchImpl });
|
|
139
|
+
const first = await latestReleaseCached({ cachePath, intervalMs: 1000, now: t0, fetchImpl, channel: 'stable' });
|
|
112
140
|
assert.equal(first.release.version, '0.3.0');
|
|
113
141
|
assert.equal(first.fresh, false);
|
|
114
142
|
assert.equal(calls, 1);
|
|
115
|
-
const second = await latestReleaseCached({ cachePath, intervalMs: 1000, now: t0 + 500, fetchImpl });
|
|
143
|
+
const second = await latestReleaseCached({ cachePath, intervalMs: 1000, now: t0 + 500, fetchImpl, channel: 'stable' });
|
|
116
144
|
assert.equal(second.release.version, '0.3.0');
|
|
117
145
|
assert.equal(second.fresh, true);
|
|
118
146
|
assert.equal(calls, 1);
|
|
@@ -121,13 +149,13 @@ test('latestReleaseCached fetches once, then serves the cache', () => withTempDi
|
|
|
121
149
|
test('latestReleaseCached returns stale data when refresh fails', () => withTempDir(async dir => {
|
|
122
150
|
const cachePath = join(dir, 'update-check.json');
|
|
123
151
|
writeFileSync(cachePath, JSON.stringify({ checkedAt: Date.now() - 60_000, release: manifest }));
|
|
124
|
-
const result = await latestReleaseCached({ cachePath, intervalMs: 1000, fetchImpl: async () => { throw new Error('offline'); } });
|
|
152
|
+
const result = await latestReleaseCached({ cachePath, intervalMs: 1000, channel: 'stable', fetchImpl: async () => { throw new Error('offline'); } });
|
|
125
153
|
assert.equal(result.release.version, '0.3.0');
|
|
126
154
|
assert.equal(result.fresh, false);
|
|
127
155
|
}));
|
|
128
156
|
|
|
129
157
|
test('latestReleaseCached swallows a cold-cache failure', () => withTempDir(async dir => {
|
|
130
|
-
const result = await latestReleaseCached({ cachePath: join(dir, 'update-check.json'), intervalMs: 1000, fetchImpl: async () => { throw new Error('offline'); } });
|
|
158
|
+
const result = await latestReleaseCached({ cachePath: join(dir, 'update-check.json'), intervalMs: 1000, channel: 'stable', fetchImpl: async () => { throw new Error('offline'); } });
|
|
131
159
|
assert.equal(result.release, null);
|
|
132
160
|
}));
|
|
133
161
|
|
|
@@ -147,6 +175,15 @@ test('detectInstallMethod distinguishes clones from packed installs', () => with
|
|
|
147
175
|
assert.equal(detectInstallMethod(dir), 'clone');
|
|
148
176
|
}));
|
|
149
177
|
|
|
178
|
+
test('npmInstallSpec prefers an exact registry version and falls back when lookup fails', async () => {
|
|
179
|
+
const release = { version: '1.2.3', installSpec: 'github:B-icy/Carthagent#v1.2.3' };
|
|
180
|
+
let lookedUp;
|
|
181
|
+
assert.equal(await npmInstallSpec(release, async spec => { lookedUp = spec; return true; }), 'carthagent@1.2.3');
|
|
182
|
+
assert.equal(lookedUp, 'carthagent@1.2.3');
|
|
183
|
+
assert.equal(await npmInstallSpec(release, async () => false), release.installSpec);
|
|
184
|
+
assert.equal(await npmInstallSpec(release, async () => { throw new Error('npm unavailable'); }), release.installSpec);
|
|
185
|
+
});
|
|
186
|
+
|
|
150
187
|
// ---- staleGlobalInstallPaths / cleanStaleGlobalInstall
|
|
151
188
|
|
|
152
189
|
test('staleGlobalInstallPaths flags non-directory carthagent and stash leftovers', () => withTempDir(dir => {
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
import { REQUIRED_RELEASE_ASSETS, verifyDraftRuntimeGate } from '../tools/verify-draft-runtime-gate.mjs';
|
|
9
|
+
|
|
10
|
+
const { version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
11
|
+
const tag = `v${version}`;
|
|
12
|
+
const sha = 'a'.repeat(40);
|
|
13
|
+
const producerReleaseAssets = [
|
|
14
|
+
'opencode-linux-x64.tar.gz',
|
|
15
|
+
'opencode-linux-arm64.tar.gz',
|
|
16
|
+
'opencode-darwin-x64.tar.gz',
|
|
17
|
+
'opencode-darwin-arm64.tar.gz',
|
|
18
|
+
'opencode-windows-x64.tar.gz',
|
|
19
|
+
'runtime-release.json',
|
|
20
|
+
'runtime-release-SHA256SUMS.txt',
|
|
21
|
+
].map((name) => ({ name }));
|
|
22
|
+
const release = { id: 77, tag_name: tag, draft: true, assets: producerReleaseAssets };
|
|
23
|
+
|
|
24
|
+
function mockRun({ releaseValue = release, remoteSha = sha, checkoutSha = sha, annotated = false } = {}) {
|
|
25
|
+
const calls = [];
|
|
26
|
+
const run = (command, args) => {
|
|
27
|
+
calls.push([command, args]);
|
|
28
|
+
if (command === 'git' && args[0] === 'rev-parse') return `${checkoutSha}\n`;
|
|
29
|
+
if (command !== 'gh') throw new Error(`Unexpected fake command ${command} ${args.join(' ')}`);
|
|
30
|
+
if (args[0] === 'api' && args[1] === `repos/B-icy/Carthagent/git/ref/tags/${tag}`) return JSON.stringify({ object: { type: annotated ? 'tag' : 'commit', sha: annotated ? 'c'.repeat(40) : remoteSha } });
|
|
31
|
+
if (args[0] === 'api' && args[1] === `repos/B-icy/Carthagent/git/tags/${'c'.repeat(40)}`) return JSON.stringify({ object: { type: 'commit', sha: remoteSha } });
|
|
32
|
+
if (args[0] === 'api' && args[1] === '--paginate' && args[2] === '--slurp' && args[3] === 'repos/B-icy/Carthagent/releases?per_page=100') return JSON.stringify([[releaseValue]]);
|
|
33
|
+
throw new Error(`Unexpected fake GitHub API call ${args.join(' ')}`);
|
|
34
|
+
};
|
|
35
|
+
return { run, calls };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('accepts only the exact draft and returns its release ID and pinned commit', async () => {
|
|
39
|
+
const { run, calls } = mockRun();
|
|
40
|
+
const result = await verifyDraftRuntimeGate({ phase: 'before', releaseTag: tag, repo: 'B-icy/Carthagent', cwd: '/fixture', run });
|
|
41
|
+
assert.deepEqual(result, { releaseId: 77, sha, version });
|
|
42
|
+
assert.equal(calls.filter(([command]) => command === 'gh').length, 2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('resolves an annotated tag object to the exact commit before acceptance', async () => {
|
|
46
|
+
const { run, calls } = mockRun({ annotated: true });
|
|
47
|
+
const result = await verifyDraftRuntimeGate({ phase: 'before', releaseTag: tag, repo: 'B-icy/Carthagent', cwd: '/fixture', run });
|
|
48
|
+
assert.equal(result.sha, sha);
|
|
49
|
+
assert.equal(calls.some(([, args]) => args?.[1] === `repos/B-icy/Carthagent/git/tags/${'c'.repeat(40)}`), true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('rejects a malformed tag before invoking git or GitHub', async () => {
|
|
53
|
+
const { run, calls } = mockRun();
|
|
54
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'before', releaseTag: `not-${tag}`, repo: 'B-icy/Carthagent', run }), /must be exactly/);
|
|
55
|
+
assert.equal(calls.length, 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('rejects a stale/moved tag before installed acceptance', async () => {
|
|
59
|
+
const { run, calls } = mockRun({ remoteSha: 'b'.repeat(40) });
|
|
60
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'before', releaseTag: tag, repo: 'B-icy/Carthagent', cwd: '/fixture', run }), /but this checkout is/);
|
|
61
|
+
assert.equal(calls.some(([command]) => command === 'git'), true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('rejects a published release before installed acceptance', async () => {
|
|
65
|
+
const { run, calls } = mockRun({ releaseValue: { ...release, draft: false } });
|
|
66
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'before', releaseTag: tag, repo: 'B-icy/Carthagent', run }), /must remain a draft/);
|
|
67
|
+
assert.equal(calls.some(([, args]) => args?.[1]?.includes('/git/ref/')), false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('requires the exact seven-asset producer contract: five archives, manifest, and combined checksums', async () => {
|
|
71
|
+
const expectedAssets = [
|
|
72
|
+
'opencode-linux-x64.tar.gz',
|
|
73
|
+
'opencode-linux-arm64.tar.gz',
|
|
74
|
+
'opencode-darwin-x64.tar.gz',
|
|
75
|
+
'opencode-darwin-arm64.tar.gz',
|
|
76
|
+
'opencode-windows-x64.tar.gz',
|
|
77
|
+
'runtime-release.json',
|
|
78
|
+
'runtime-release-SHA256SUMS.txt',
|
|
79
|
+
];
|
|
80
|
+
assert.deepEqual(REQUIRED_RELEASE_ASSETS, expectedAssets);
|
|
81
|
+
const result = await verifyDraftRuntimeGate({
|
|
82
|
+
phase: 'before', releaseTag: tag, repo: 'B-icy/Carthagent', cwd: '/fixture',
|
|
83
|
+
run: mockRun({ releaseValue: { ...release, assets: expectedAssets.map((name) => ({ name })) } }).run,
|
|
84
|
+
});
|
|
85
|
+
assert.deepEqual(result, { releaseId: 77, sha, version });
|
|
86
|
+
|
|
87
|
+
for (const [label, assets, expected] of [
|
|
88
|
+
['missing manifest', release.assets.filter(({ name }) => name !== 'runtime-release.json'), /exactly one runtime-release\.json asset/],
|
|
89
|
+
['duplicate platform archive', [...release.assets, { name: REQUIRED_RELEASE_ASSETS[0] }], /exactly one opencode-linux-x64\.tar\.gz asset/],
|
|
90
|
+
['unexpected extra asset', [...release.assets, { name: 'unreviewed-runtime.tar.gz' }], /unexpected assets: unreviewed-runtime\.tar\.gz/],
|
|
91
|
+
['renamed checksum asset', release.assets.map((asset) => asset.name === 'runtime-release-SHA256SUMS.txt' ? { name: 'SHA256SUMS.txt' } : asset), /exactly one runtime-release-SHA256SUMS\.txt asset/],
|
|
92
|
+
]) {
|
|
93
|
+
const { run } = mockRun({ releaseValue: { ...release, assets } });
|
|
94
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'before', releaseTag: tag, repo: 'B-icy/Carthagent', run }), expected, label);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('the workflow precheck command fails before acceptance for malformed tag, moved tag, or published release', async (t) => {
|
|
99
|
+
const temp = await mkdtemp(path.join(tmpdir(), 'ctg-draft-gate-fake-gh-'));
|
|
100
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
101
|
+
const requestsLog = path.join(temp, 'gh-requests');
|
|
102
|
+
const releaseFile = path.join(temp, 'release.json');
|
|
103
|
+
const targetFile = path.join(temp, 'target-sha');
|
|
104
|
+
const preloadFile = path.join(temp, 'fake-gh-preload.mjs');
|
|
105
|
+
const currentSha = spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).stdout.trim();
|
|
106
|
+
await writeFile(targetFile, currentSha);
|
|
107
|
+
await writeFile(releaseFile, JSON.stringify(release));
|
|
108
|
+
await writeFile(preloadFile, `
|
|
109
|
+
import fs from 'node:fs';
|
|
110
|
+
import childProcess from 'node:child_process';
|
|
111
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
112
|
+
const originalSpawnSync = childProcess.spawnSync;
|
|
113
|
+
childProcess.spawnSync = function(command, args, options) {
|
|
114
|
+
if (command !== 'gh') return originalSpawnSync.call(this, command, args, options);
|
|
115
|
+
fs.appendFileSync(process.env.FAKE_GH_LOG, JSON.stringify(args) + '\\n');
|
|
116
|
+
let stdout;
|
|
117
|
+
if (args.some(value => value.includes('/releases?per_page=100'))) {
|
|
118
|
+
stdout = JSON.stringify([[JSON.parse(fs.readFileSync(process.env.FAKE_RELEASE_JSON, 'utf8'))]]);
|
|
119
|
+
} else if (args.some(value => value.includes('/git/ref/tags/'))) {
|
|
120
|
+
stdout = JSON.stringify({ object: { type: 'commit', sha: fs.readFileSync(process.env.FAKE_TARGET_SHA, 'utf8').trim() } });
|
|
121
|
+
} else {
|
|
122
|
+
return { status: 2, stdout: '', stderr: 'Unexpected fake GitHub API call', error: undefined };
|
|
123
|
+
}
|
|
124
|
+
return { status: 0, stdout, stderr: '', error: undefined };
|
|
125
|
+
};
|
|
126
|
+
syncBuiltinESMExports();
|
|
127
|
+
`);
|
|
128
|
+
const tool = fileURLToPath(new URL('../tools/verify-draft-runtime-gate.mjs', import.meta.url));
|
|
129
|
+
const invoke = (tagValue) => spawnSync(process.execPath, [tool, 'before', tagValue], {
|
|
130
|
+
cwd: path.resolve('.'), encoding: 'utf8',
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import=${pathToFileURL(preloadFile).href}`.trim(),
|
|
134
|
+
GITHUB_REPOSITORY: 'B-icy/Carthagent', GITHUB_OUTPUT: path.join(temp, 'github-output'),
|
|
135
|
+
FAKE_GH_LOG: requestsLog, FAKE_RELEASE_JSON: releaseFile, FAKE_TARGET_SHA: targetFile,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const malformed = invoke('not-a-version');
|
|
140
|
+
assert.notEqual(malformed.status, 0);
|
|
141
|
+
await assert.rejects(readFile(requestsLog));
|
|
142
|
+
|
|
143
|
+
await writeFile(targetFile, 'b'.repeat(40));
|
|
144
|
+
const stale = invoke(tag);
|
|
145
|
+
assert.notEqual(stale.status, 0);
|
|
146
|
+
assert.match(stale.stderr, /but this checkout is/);
|
|
147
|
+
|
|
148
|
+
await writeFile(targetFile, currentSha);
|
|
149
|
+
await writeFile(releaseFile, JSON.stringify({ ...release, draft: false }));
|
|
150
|
+
const published = invoke(tag);
|
|
151
|
+
assert.notEqual(published.status, 0);
|
|
152
|
+
assert.match(published.stderr, /must remain a draft/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('trusted inline bootstrap accepts a main tag and rejects non-main dispatch and a foreign tag', async (t) => {
|
|
156
|
+
const workflow = (await readFile(new URL('../.github/workflows/accept-draft-release.yml', import.meta.url), 'utf8')).replace(/\r\n/g, '\n');
|
|
157
|
+
const bootstrap = workflow.match(/- name: Require main dispatch and a release tag descended from origin\/main[\s\S]*?\n run: \|\n((?: .*\n)+)/)?.[1];
|
|
158
|
+
const postcheck = workflow.match(/- name: Reverify release tag ancestry against approved main after acceptance[\s\S]*?\n run: \|\n((?: .*\n?)+?)(?=\n - |\n\s*$)/)?.[1];
|
|
159
|
+
assert.ok(bootstrap, 'trusted ancestry bootstrap block must be extractable');
|
|
160
|
+
assert.ok(postcheck, 'post-acceptance trusted ancestry block must be extractable');
|
|
161
|
+
|
|
162
|
+
const temp = await mkdtemp(path.join(tmpdir(), 'ctg-main-ancestry-'));
|
|
163
|
+
t.after(() => rm(temp, { recursive: true, force: true }));
|
|
164
|
+
const source = path.join(temp, 'source');
|
|
165
|
+
const bare = path.join(temp, 'origin.git');
|
|
166
|
+
const output = path.join(temp, 'github-output');
|
|
167
|
+
const fixtureVersion = '1.0.3';
|
|
168
|
+
const fixtureTag = `v${fixtureVersion}`;
|
|
169
|
+
await mkdir(source);
|
|
170
|
+
const git = (cwd, ...args) => execFileSync('git', args, { cwd, stdio: 'pipe' }).toString().trim();
|
|
171
|
+
git(source, 'init', '-b', 'main');
|
|
172
|
+
git(source, 'config', 'user.email', 'gate-fixture@example.invalid');
|
|
173
|
+
git(source, 'config', 'user.name', 'Gate Fixture');
|
|
174
|
+
await writeFile(path.join(source, 'package.json'), JSON.stringify({ version: fixtureVersion }));
|
|
175
|
+
git(source, 'add', 'package.json');
|
|
176
|
+
git(source, 'commit', '-m', 'approved main release commit');
|
|
177
|
+
const approvedSha = git(source, 'rev-parse', 'HEAD');
|
|
178
|
+
git(source, 'tag', '-a', fixtureTag, '-m', 'approved annotated draft release', approvedSha);
|
|
179
|
+
execFileSync('git', ['init', '--bare', bare], { stdio: 'pipe' });
|
|
180
|
+
git(source, 'remote', 'add', 'origin', bare);
|
|
181
|
+
git(source, 'push', 'origin', 'main', '--tags');
|
|
182
|
+
|
|
183
|
+
const bashPath = (value) => {
|
|
184
|
+
if (process.platform !== 'win32') return value;
|
|
185
|
+
return execFileSync('cygpath', ['-u', value], { encoding: 'utf8' }).trim();
|
|
186
|
+
};
|
|
187
|
+
const invoke = (script, ref, expectedSha = '') => spawnSync('bash', ['-euo', 'pipefail', '-c', script], {
|
|
188
|
+
cwd: source, encoding: 'utf8',
|
|
189
|
+
env: {
|
|
190
|
+
...process.env,
|
|
191
|
+
GITHUB_REF: ref,
|
|
192
|
+
GITHUB_OUTPUT: bashPath(output),
|
|
193
|
+
RELEASE_TAG: fixtureTag,
|
|
194
|
+
TRUSTED_REPOSITORY_URL: bashPath(bare),
|
|
195
|
+
EXPECTED_SHA: expectedSha,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const accepted = invoke(bootstrap, 'refs/heads/main');
|
|
200
|
+
assert.equal(accepted.status, 0, accepted.stderr);
|
|
201
|
+
assert.match(await readFile(output, 'utf8'), new RegExp(`sha=${approvedSha}`));
|
|
202
|
+
assert.equal(invoke(bootstrap, 'refs/heads/release-candidate').status, 1);
|
|
203
|
+
assert.equal(invoke(postcheck, 'refs/heads/main', approvedSha).status, 0);
|
|
204
|
+
|
|
205
|
+
git(source, 'checkout', '-b', 'foreign/release');
|
|
206
|
+
await writeFile(path.join(source, 'foreign.txt'), 'not in main');
|
|
207
|
+
git(source, 'add', 'foreign.txt');
|
|
208
|
+
git(source, 'commit', '-m', 'unapproved foreign release commit');
|
|
209
|
+
git(source, 'tag', '-f', fixtureTag, 'HEAD');
|
|
210
|
+
git(source, 'push', 'origin', `+refs/tags/${fixtureTag}:refs/tags/${fixtureTag}`);
|
|
211
|
+
const rejectedForeign = invoke(bootstrap, 'refs/heads/main');
|
|
212
|
+
assert.equal(rejectedForeign.status, 1, rejectedForeign.stderr);
|
|
213
|
+
assert.match(rejectedForeign.stderr, /must point to a commit descended from approved origin\/main/);
|
|
214
|
+
const rejectedMovedTag = invoke(postcheck, 'refs/heads/main', approvedSha);
|
|
215
|
+
assert.equal(rejectedMovedTag.status, 1, rejectedMovedTag.stderr);
|
|
216
|
+
assert.match(rejectedMovedTag.stderr, /moved during acceptance/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('post-check requires the same release ID and the pre-acceptance tested SHA', async () => {
|
|
220
|
+
const { run: goodRun } = mockRun();
|
|
221
|
+
assert.deepEqual(await verifyDraftRuntimeGate({ phase: 'after', releaseTag: tag, expectedSha: sha, expectedReleaseId: 77, repo: 'B-icy/Carthagent', cwd: '/fixture', run: goodRun }), {
|
|
222
|
+
releaseId: 77, sha, version,
|
|
223
|
+
});
|
|
224
|
+
const { run: movedRun } = mockRun({ remoteSha: 'b'.repeat(40) });
|
|
225
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'after', releaseTag: tag, expectedSha: sha, expectedReleaseId: 77, repo: 'B-icy/Carthagent', cwd: '/fixture', run: movedRun }), /but this checkout is/);
|
|
226
|
+
const { run: idRun } = mockRun({ releaseValue: { ...release, id: 78 } });
|
|
227
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'after', releaseTag: tag, expectedSha: sha, expectedReleaseId: 77, repo: 'B-icy/Carthagent', run: idRun }), /identity changed/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('immediate pre-acceptance recheck pins the original SHA and release ID', async () => {
|
|
231
|
+
const { run } = mockRun();
|
|
232
|
+
assert.deepEqual(await verifyDraftRuntimeGate({ phase: 'recheck', releaseTag: tag, expectedSha: sha, expectedReleaseId: 77, repo: 'B-icy/Carthagent', cwd: '/fixture', run }), {
|
|
233
|
+
releaseId: 77, sha, version,
|
|
234
|
+
});
|
|
235
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'recheck', releaseTag: tag, expectedSha: 'b'.repeat(40), expectedReleaseId: 77, repo: 'B-icy/Carthagent', cwd: '/fixture', run }), /changed during acceptance/);
|
|
236
|
+
await assert.rejects(verifyDraftRuntimeGate({ phase: 'recheck', releaseTag: tag, expectedSha: sha, expectedReleaseId: 76, repo: 'B-icy/Carthagent', cwd: '/fixture', run }), /identity changed/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('draft workflow grants required push capability but performs only read operations', async () => {
|
|
240
|
+
const workflow = await readFile(new URL('../.github/workflows/accept-draft-release.yml', import.meta.url), 'utf8');
|
|
241
|
+
assert.match(workflow, /permissions:\s*\n\s+contents:\s+read/);
|
|
242
|
+
assert.match(workflow, /jobs:[\s\S]*?permissions:\s*\n\s+contents:\s+write/);
|
|
243
|
+
assert.match(workflow, /draft releases only to callers with push access/i);
|
|
244
|
+
assert.match(workflow, /minimum GITHUB_TOKEN capability/i);
|
|
245
|
+
assert.match(workflow, /token only for GETs/i);
|
|
246
|
+
const acceptance = workflow.match(/- name: Run the current full non-focused installed-package gate through the default resolver([\s\S]*?\n\s+run: npm run test:native-package)/)?.[0];
|
|
247
|
+
assert.ok(acceptance);
|
|
248
|
+
assert.match(acceptance, /CTG_REQUIRE_OWNED_RUNTIME:\s*['"]?1/);
|
|
249
|
+
assert.match(acceptance, /CTG_RUNTIME_GITHUB_TOKEN:\s*\$\{\{ github\.token \}\}/);
|
|
250
|
+
assert.match(acceptance, /GH_TOKEN:\s*['"]{2}/);
|
|
251
|
+
assert.match(workflow, /run: npm run test:native-package/);
|
|
252
|
+
assert.doesNotMatch(workflow, /gh release (?:create|upload|edit)|releases\/assets|curl .*cli\/releases/);
|
|
253
|
+
assert.doesNotMatch(workflow, /gh api\s+(?:--[^\n]*\s+)?--method\s+(?:POST|PATCH|PUT|DELETE)|gh api\s+-X\s+(?:POST|PATCH|PUT|DELETE)/i);
|
|
254
|
+
assert.match(workflow, /npm ci/);
|
|
255
|
+
assert.match(workflow, /npm run quality/);
|
|
256
|
+
assert.match(workflow, /verify-draft-runtime-gate\.mjs before/);
|
|
257
|
+
assert.match(workflow, /verify-draft-runtime-gate\.mjs recheck/);
|
|
258
|
+
assert.match(workflow, /verify-draft-runtime-gate\.mjs after/);
|
|
259
|
+
assert.match(workflow, /verify release tag ancestry against approved main after acceptance/);
|
|
260
|
+
const bootstrapIndex = workflow.indexOf('Require main dispatch and a release tag descended from origin/main');
|
|
261
|
+
const checkoutIndex = workflow.indexOf('uses: actions/checkout@v4');
|
|
262
|
+
const verifierIndex = workflow.indexOf('verify-draft-runtime-gate.mjs before');
|
|
263
|
+
assert.ok(bootstrapIndex >= 0 && bootstrapIndex < checkoutIndex && checkoutIndex < verifierIndex,
|
|
264
|
+
'trusted inline ancestry bootstrap must run before release checkout and tag-sourced verifier');
|
|
265
|
+
assert.match(workflow, /ref:\s*\$\{\{ steps\.trusted-ancestry\.outputs\.sha \}\}/,
|
|
266
|
+
'checkout must use the trusted ancestry-checked SHA, not resolve the mutable tag again');
|
|
267
|
+
const docs = (await readFile(new URL('../docs/opencode-owned-runtime.md', import.meta.url), 'utf8')).replace(/\r\n/g, '\n');
|
|
268
|
+
assert.match(docs, /current full,\s+non-focused installed-package gate/);
|
|
269
|
+
assert.match(docs, /exactly those seven assets/);
|
|
270
|
+
assert.match(docs, /independently reviewed host-controls, glyph,\s+controlled-terminal semantics, and visual-clear\/reopen probes are now composed\s+into that gate/);
|
|
271
|
+
assert.match(docs, /Their live final-artifact results remain pending/);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('published release workflow explicitly empties optional draft credentials for public acceptance', async () => {
|
|
275
|
+
const workflow = await readFile(new URL('../.github/workflows/release.yml', import.meta.url), 'utf8');
|
|
276
|
+
const acceptance = workflow.match(/- name: Accept the installed package with its owned runtime required([\s\S]*?)(?=\n - |\n\s+run:)/)?.[1];
|
|
277
|
+
const verify = workflow.match(/- name: Verify runtime archives against the packaged immutable manifest([\s\S]*?)(?=\n - |\n\s+run:)/)?.[1];
|
|
278
|
+
assert.match(acceptance ?? '', /CTG_RUNTIME_GITHUB_TOKEN:\s*['"]{2}/);
|
|
279
|
+
assert.match(verify ?? '', /CTG_RUNTIME_GITHUB_TOKEN:\s*['"]{2}/);
|
|
280
|
+
});
|