carthagent 1.0.7 → 1.1.0-preview.12
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 +80 -79
- package/bin/ctg.mjs +14 -732
- 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/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 +314 -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 +177 -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.patch +5137 -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.9.md +130 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- 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 +8 -4
- 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 +298 -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/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 +197 -0
- package/plugins/carthagent/src/server/delivery.ts +217 -106
- 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 +58 -30
- 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 +39 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +492 -0
- package/plugins/carthagent/src/ui/client.ts +125 -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 +232 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
- package/plugins/carthagent/src/ui/native-review.mjs +154 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -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 +122 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +127 -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 +60 -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 +258 -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.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 +53 -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 +426 -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-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 +267 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +75 -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 +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -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-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -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 +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +36 -0
- 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 +100 -18
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +83 -61
- 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,426 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { JsonSchema, Schema, SchemaRepresentation } from 'effect';
|
|
8
|
+
import { checkDigest, fingerprint } from '../lib/delivery.mjs';
|
|
9
|
+
import { createReport } from '../lib/reports.mjs';
|
|
10
|
+
import { selectReport } from '../lib/workspace.mjs';
|
|
11
|
+
|
|
12
|
+
const { createJiti } = createRequire(import.meta.url)('jiti');
|
|
13
|
+
const jiti = createJiti(import.meta.url);
|
|
14
|
+
const { DailySource } = await jiti.import('../plugins/carthagent/src/daily-source-rpc.ts');
|
|
15
|
+
const { createDailySourceRpcHandlers, registerDailySourceRpc } = await jiti.import('../plugins/carthagent/src/server/daily-source-rpc.ts');
|
|
16
|
+
const call = { signal: new AbortController().signal, error(type, message, data) { throw Object.assign(new Error(message), { type, data }); } };
|
|
17
|
+
const rejected = (promise, code) => assert.rejects(promise, error => {
|
|
18
|
+
assert.equal(error.type, 'dailySourceError');
|
|
19
|
+
assert.equal(error.message, code);
|
|
20
|
+
assert.deepEqual(error.data, { code });
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
const check = id => ({ id, kind: 'test', argv: ['never-execute-this', 'sensitive-argv'], timeoutSeconds: 20 });
|
|
24
|
+
|
|
25
|
+
function fixture(t, checks = [check('unit')]) {
|
|
26
|
+
// Keep the baseline canonical; explicit cases below exercise native ancestor
|
|
27
|
+
// aliases such as macOS /var -> /private/var, distinct from linked leaves.
|
|
28
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), 'daily-source-')));
|
|
29
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
30
|
+
const root = join(base, 'project'); mkdirSync(root);
|
|
31
|
+
writeFileSync(join(root, 'source.txt'), 'actual source');
|
|
32
|
+
const sessionID = 'native-session';
|
|
33
|
+
const current = fingerprint(root, ['.']);
|
|
34
|
+
const plan = {
|
|
35
|
+
goal: 'Actual plan goal', artifacts: ['.'], steps: ['Implement'],
|
|
36
|
+
acceptance: [{ requirement: 'Actual requirement', checks: checks.map(item => item.id) }], checks,
|
|
37
|
+
};
|
|
38
|
+
const state = { runId: 'native-run', revision: 3, status: 'implementing', plan, evidence: {} };
|
|
39
|
+
const session = { id: sessionID, projectID: 'native-project', location: { directory: root } };
|
|
40
|
+
const nativeWorktrees = [];
|
|
41
|
+
const sessions = new Map([[sessionID, session]]);
|
|
42
|
+
let registrations = 0, disposed = 0;
|
|
43
|
+
const ctx = {
|
|
44
|
+
location: { directory: root, project: { id: 'native-project', canonical: root } },
|
|
45
|
+
session: { async get({ sessionID: id }, { signal }) { signal.throwIfAborted(); const found = sessions.get(id); if (!found) throw new Error(`private path ${root}`); return found; } },
|
|
46
|
+
worktree: { async list({ projectID }, { signal }) { assert.equal(projectID, 'native-project'); signal.throwIfAborted(); return nativeWorktrees.map(directory => ({ directory })); } },
|
|
47
|
+
rpc: { async register(definition, handlers) { registrations++; assert.equal(definition.id, DailySource.id); assert.deepEqual(Object.keys(handlers), ['report']); return { async dispose() { disposed++; } }; } },
|
|
48
|
+
// Any attempt to look up credentials, transcripts or write storage must fail.
|
|
49
|
+
get integration() { throw new Error('must not access credentials'); },
|
|
50
|
+
get storage() { throw new Error('must not access storage'); },
|
|
51
|
+
};
|
|
52
|
+
const report = createReport(root, state, sessionID);
|
|
53
|
+
selectReport(root, report.path);
|
|
54
|
+
const save = () => writeFileSync(report.path, JSON.stringify(state));
|
|
55
|
+
const receipt = (id = checks[0].id, overrides = {}) => ({
|
|
56
|
+
passed: true, runId: state.runId, revision: state.revision, executedRevision: state.revision,
|
|
57
|
+
checkDigest: checkDigest(checks.find(item => item.id === id)), fingerprint: current,
|
|
58
|
+
code: 0, timedOut: false, cancelled: false, changedDuringCheck: false, durationMs: 9,
|
|
59
|
+
outputTail: 'actual bounded output tail', logPath: join(base, 'must-not-read.log'), ...overrides,
|
|
60
|
+
});
|
|
61
|
+
const handlers = createDailySourceRpcHandlers(ctx);
|
|
62
|
+
return { base, root, current, sessionID, session, sessions, ctx, state, report, save, receipt, nativeWorktrees, handlers,
|
|
63
|
+
read: () => handlers.report({ sessionID }, call), counts: () => ({ registrations, disposed }) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test('local source uses actual receipt identity, actual goal, no synthetic before/change or log imports', async t => {
|
|
67
|
+
const f = fixture(t, [check('unit'), check('missing'), check('failure')]);
|
|
68
|
+
f.state.evidence.unit = f.receipt('unit');
|
|
69
|
+
f.state.evidence.failure = f.receipt('failure', { passed: false, code: 1, outputTail: 'actual failing assertion' });
|
|
70
|
+
f.state.handoff = { review: 'not a change summary', launch: 'not a summary either', fingerprint: f.current };
|
|
71
|
+
f.state.transcript = 'must-not-return-transcript';
|
|
72
|
+
f.state.reviewCapture = { path: join(f.root, 'must-not-read-capture'), diff: 'must-not-return-full-capture' };
|
|
73
|
+
f.save();
|
|
74
|
+
const { source } = await f.read();
|
|
75
|
+
assert.deepEqual(source, {
|
|
76
|
+
runId: 'native-run', revision: f.current, planRevision: 3, freshness: 'current', goal: 'Actual plan goal', change: '',
|
|
77
|
+
checks: [
|
|
78
|
+
{ id: 'unit', before: 'not_run', after: 'passed', revision: f.current, result: 'actual bounded output tail' },
|
|
79
|
+
{ id: 'missing', before: 'not_run', after: 'not_run', revision: f.current, result: '' },
|
|
80
|
+
{ id: 'failure', before: 'not_run', after: 'failed', revision: f.current, result: 'actual failing assertion' },
|
|
81
|
+
], findings: [], truncated: false,
|
|
82
|
+
});
|
|
83
|
+
for (const forbidden of ['sensitive-argv', 'logPath', f.root, 'must-not-return', 'not a change summary']) assert.equal(JSON.stringify(source).includes(forbidden), false);
|
|
84
|
+
// The current receipt overwrites prior outcomes. Never invent a historical failure.
|
|
85
|
+
f.state.evidence.failure = f.receipt('failure'); f.save();
|
|
86
|
+
assert.equal((await f.read()).source.checks[2].before, 'not_run');
|
|
87
|
+
assert.equal((await f.read()).source.checks[2].after, 'passed');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('stale, mismatched or contradictory passing receipts never become current passing evidence', async t => {
|
|
91
|
+
const f = fixture(t);
|
|
92
|
+
const invalidations = [
|
|
93
|
+
{ fingerprint: '0'.repeat(64) }, { runId: 'other-run' }, { revision: 2 }, { checkDigest: '0'.repeat(64) },
|
|
94
|
+
{ code: 1 }, { timedOut: true }, { cancelled: true }, { changedDuringCheck: true }, { outputLimit: true }, { code: undefined },
|
|
95
|
+
];
|
|
96
|
+
for (const overrides of invalidations) {
|
|
97
|
+
f.state.evidence.unit = f.receipt('unit', overrides); f.save();
|
|
98
|
+
const { source } = await f.read();
|
|
99
|
+
assert.equal(source.freshness, 'stale', JSON.stringify(overrides));
|
|
100
|
+
assert.equal(source.checks[0].after, 'not_run');
|
|
101
|
+
assert.equal(source.checks[0].revision, f.current);
|
|
102
|
+
}
|
|
103
|
+
f.state.evidence.unit = f.receipt(); f.save();
|
|
104
|
+
writeFileSync(join(f.root, 'source.txt'), 'changed actual source');
|
|
105
|
+
const { source } = await f.read();
|
|
106
|
+
assert.equal(source.revision, fingerprint(f.root, ['.']));
|
|
107
|
+
assert.notEqual(source.revision, f.current);
|
|
108
|
+
assert.equal(source.checks[0].after, 'not_run');
|
|
109
|
+
assert.equal(source.freshness, 'stale');
|
|
110
|
+
// The native failure path explicitly records a null fingerprint on scope failure.
|
|
111
|
+
f.state.evidence.unit = f.receipt('unit', { passed: false, fingerprint: null, code: 1 }); f.save();
|
|
112
|
+
assert.equal((await f.read()).source.checks[0].after, 'not_run');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('failed receipts are current-only: old source, mismatched identity and scope errors cannot fabricate a current failure', async t => {
|
|
116
|
+
const f = fixture(t);
|
|
117
|
+
for (const overrides of [
|
|
118
|
+
{ runId: 'other-run' }, { revision: 2 }, { checkDigest: '0'.repeat(64) },
|
|
119
|
+
{ fingerprint: '0'.repeat(64) }, { fingerprint: null }, { fingerprint: undefined },
|
|
120
|
+
{ code: undefined }, { code: 0 }, { changedDuringCheck: true },
|
|
121
|
+
{ fingerprint: null, timedOut: true }, { fingerprint: undefined, cancelled: true },
|
|
122
|
+
]) {
|
|
123
|
+
f.state.evidence.unit = f.receipt('unit', { passed: false, code: 1, outputTail: 'old or unexecuted failure', ...overrides });
|
|
124
|
+
f.save();
|
|
125
|
+
const { source } = await f.read();
|
|
126
|
+
assert.equal(source.checks[0].after, 'not_run', JSON.stringify(overrides));
|
|
127
|
+
assert.equal(source.checks[0].before, 'not_run');
|
|
128
|
+
assert.equal(source.checks[0].revision, f.current);
|
|
129
|
+
assert.equal(source.checks[0].result, '');
|
|
130
|
+
}
|
|
131
|
+
for (const overrides of [{ code: 1 }, { code: -1 }, { code: 0, timedOut: true }, { code: undefined, cancelled: true }, { code: 0, outputLimit: true }]) {
|
|
132
|
+
f.state.evidence.unit = f.receipt('unit', { passed: false, outputTail: 'actual current failure', ...overrides });
|
|
133
|
+
f.save();
|
|
134
|
+
const { source } = await f.read();
|
|
135
|
+
assert.equal(source.checks[0].after, 'failed', JSON.stringify(overrides));
|
|
136
|
+
assert.equal(source.checks[0].revision, f.current);
|
|
137
|
+
assert.equal(source.checks[0].result, 'actual current failure');
|
|
138
|
+
}
|
|
139
|
+
f.state.evidence.unit = f.receipt('unit', { passed: false, code: 1, outputTail: 'failure at A, not B' }); f.save();
|
|
140
|
+
assert.equal((await f.read()).source.checks[0].after, 'failed');
|
|
141
|
+
writeFileSync(join(f.root, 'source.txt'), 'workspace B');
|
|
142
|
+
const { source } = await f.read();
|
|
143
|
+
assert.notEqual(source.revision, f.current);
|
|
144
|
+
assert.equal(source.revision, fingerprint(f.root, ['.']));
|
|
145
|
+
assert.equal(source.checks[0].revision, source.revision);
|
|
146
|
+
assert.equal(source.checks[0].after, 'not_run');
|
|
147
|
+
assert.equal(source.checks[0].result, '');
|
|
148
|
+
assert.equal(source.freshness, 'stale');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('review findings expose only actual model-authored resolution claims without verification labels', async t => {
|
|
152
|
+
const f = fixture(t);
|
|
153
|
+
f.state.reviewFindings = [
|
|
154
|
+
{ id: 'finding-one', description: 'Actual review issue', captureId: 'capture-private', fingerprint: f.current, at: 'then', resolution: { id: 'finding-one', change: 'Actual described repair', evidence: 'Actual model-authored evidence claim', captureId: 'capture-private', fingerprint: f.current, at: 'now' } },
|
|
155
|
+
{ id: 'finding-two', description: 'Unresolved issue' },
|
|
156
|
+
{ id: 'finding-three', description: 'Legacy resolution without evidence', resolution: { id: 'finding-three', change: 'Recorded change only' } },
|
|
157
|
+
];
|
|
158
|
+
f.save();
|
|
159
|
+
assert.deepEqual((await f.read()).source.findings, [
|
|
160
|
+
{ id: 'finding-one', issue: 'Actual review issue', evidence: 'Actual model-authored evidence claim', resolution: 'Actual described repair' },
|
|
161
|
+
{ id: 'finding-two', issue: 'Unresolved issue', evidence: '', resolution: '' },
|
|
162
|
+
{ id: 'finding-three', issue: 'Legacy resolution without evidence', evidence: '', resolution: 'Recorded change only' },
|
|
163
|
+
]);
|
|
164
|
+
const serialized = JSON.stringify((await f.read()).source.findings);
|
|
165
|
+
assert.equal(serialized.includes('capture-private'), false);
|
|
166
|
+
assert.equal(serialized.includes('verified'), false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('selection is bounded, UTF-8 safe and explicitly reports omitted claims/items', async t => {
|
|
170
|
+
const f = fixture(t, Array.from({ length: 13 }, (_, i) => check(`check_${i}`)));
|
|
171
|
+
f.state.plan.goal = '界'.repeat(342); // 1026 UTF-8 bytes, not 342-byte ASCII.
|
|
172
|
+
f.state.evidence.check_0 = f.receipt('check_0', { outputTail: '界'.repeat(341) });
|
|
173
|
+
f.state.evidence.check_1 = f.receipt('check_1', { outputTail: '😀'.repeat(257) });
|
|
174
|
+
f.state.reviewFindings = Array.from({ length: 6 }, (_, i) => ({ id: `finding-${i}`, description: i ? 'Actual issue' : 'x'.repeat(2001), resolution: { id: `finding-${i}`, change: 'Recorded repair', evidence: i ? 'Claimed evidence' : 'x'.repeat(2001) } }));
|
|
175
|
+
f.save();
|
|
176
|
+
const { source } = await f.read();
|
|
177
|
+
assert.equal(source.checks.length, 12);
|
|
178
|
+
assert.equal(source.findings.length, 5);
|
|
179
|
+
assert.equal(source.truncated, true);
|
|
180
|
+
assert.equal(source.goal, '');
|
|
181
|
+
assert.equal(source.checks[0].result, '界'.repeat(341));
|
|
182
|
+
assert.equal(source.checks[1].result, '', 'omit entire claims rather than changing their meaning');
|
|
183
|
+
assert.equal(source.findings[0].issue, '');
|
|
184
|
+
assert.equal(source.findings[0].evidence, '');
|
|
185
|
+
assert.equal(JSON.stringify(source).includes('\ufffd'), false);
|
|
186
|
+
assert.ok(Buffer.byteLength(JSON.stringify(source), 'utf8') < 64 * 1024);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('unrepresentable/control text is omitted rather than silently sanitized into a different claim', async t => {
|
|
190
|
+
const f = fixture(t);
|
|
191
|
+
for (const outputTail of ['prefix\u001b[31mprivate', 'bad\ud800surrogate', 'nul\u0000byte']) {
|
|
192
|
+
f.state.evidence.unit = f.receipt('unit', { outputTail }); f.save();
|
|
193
|
+
const { source } = await f.read();
|
|
194
|
+
assert.equal(source.checks[0].result, '');
|
|
195
|
+
assert.equal(source.truncated, true);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('freshness inspects receipts outside the selection cap and required final-review identity', async t => {
|
|
200
|
+
const checks = Array.from({ length: 13 }, (_, i) => check(`check_${i}`));
|
|
201
|
+
const f = fixture(t, checks);
|
|
202
|
+
for (const item of checks) f.state.evidence[item.id] = f.receipt(item.id);
|
|
203
|
+
f.state.evidence.check_12.fingerprint = '0'.repeat(64); f.save();
|
|
204
|
+
assert.equal((await f.read()).source.freshness, 'stale');
|
|
205
|
+
assert.equal((await f.read()).source.checks.every(item => item.after === 'passed'), true, 'fresh displayed receipts do not hide stale omitted receipts');
|
|
206
|
+
f.state.evidence.check_12 = f.receipt('check_12');
|
|
207
|
+
f.state.plan.steps = [{ id: 'implement', title: 'Implement', dependsOn: [], checks: checks.map(item => item.id) }];
|
|
208
|
+
f.save();
|
|
209
|
+
assert.equal((await f.read()).source.freshness, 'stale', 'missing required review cannot claim current report');
|
|
210
|
+
f.state.reviewEvidence = { runId: f.state.runId, revision: f.state.revision, fingerprint: f.current }; f.save();
|
|
211
|
+
assert.equal((await f.read()).source.freshness, 'current');
|
|
212
|
+
f.state.reviewEvidence.fingerprint = '0'.repeat(64); f.save();
|
|
213
|
+
assert.equal((await f.read()).source.freshness, 'stale');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('only the exact active report for the native session is selected, never latest or storage snapshots', async t => {
|
|
217
|
+
const f = fixture(t);
|
|
218
|
+
const pointer = join(f.root, '.harness', 'active.json');
|
|
219
|
+
rmSync(pointer);
|
|
220
|
+
assert.deepEqual(await f.read(), { source: null }, 'existing reports do not authorize latest-report fallback');
|
|
221
|
+
const foreign = createReport(f.root, { ...f.state, runId: 'foreign-run' }, 'foreign-session');
|
|
222
|
+
selectReport(f.root, foreign.path);
|
|
223
|
+
assert.deepEqual(await f.read(), { source: null });
|
|
224
|
+
selectReport(f.root, f.report.path);
|
|
225
|
+
rmSync(f.report.path);
|
|
226
|
+
assert.deepEqual(await f.read(), { source: null });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('foreign sessions, mismatched IDs, forged task worktrees and arbitrary paths are rejected safely', async t => {
|
|
230
|
+
const f = fixture(t);
|
|
231
|
+
f.sessions.set('foreign-session', { ...f.session, id: 'foreign-session', projectID: 'foreign-project' });
|
|
232
|
+
await rejected(f.handlers.report({ sessionID: 'foreign-session' }, call), 'not_owned');
|
|
233
|
+
f.sessions.set('wrong-id', f.session);
|
|
234
|
+
await rejected(f.handlers.report({ sessionID: 'wrong-id' }, call), 'not_owned');
|
|
235
|
+
await rejected(f.handlers.report({ sessionID: 'unknown-session' }, call), 'invalid_source');
|
|
236
|
+
for (const input of [{ sessionID: '../escape' }, { sessionID: f.sessionID, path: f.report.path }, { sessionID: f.sessionID, includeGoal: true }, null]) await rejected(f.handlers.report(input, call), 'invalid_request');
|
|
237
|
+
const worktree = join(f.base, 'forged-worktree'); mkdirSync(worktree);
|
|
238
|
+
f.session.location.directory = worktree;
|
|
239
|
+
await rejected(f.read(), 'not_owned');
|
|
240
|
+
writeFileSync(join(f.root, '.harness', 'opencode-tasks.json'), JSON.stringify({ version: 1, tasks: [{ sessionID: f.sessionID, worktree }], operations: [] }));
|
|
241
|
+
await rejected(f.read(), 'not_owned', 'forged journal alone must not authorize a worktree');
|
|
242
|
+
f.nativeWorktrees.push(worktree);
|
|
243
|
+
const actual = createReport(worktree, f.state, f.sessionID); selectReport(worktree, actual.path);
|
|
244
|
+
assert.equal((await f.read()).source.runId, f.state.runId);
|
|
245
|
+
// The journal must agree with the native session location even for a known worktree.
|
|
246
|
+
f.session.location.directory = f.root;
|
|
247
|
+
await rejected(f.read(), 'not_owned');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('native ancestor aliases use canonical root/location/session identity, including an absent active report', async t => {
|
|
251
|
+
const f = fixture(t);
|
|
252
|
+
const alias = join(f.base, 'temporary-alias');
|
|
253
|
+
symlinkSync(f.base, alias, 'junction');
|
|
254
|
+
const aliasedRoot = join(alias, 'project');
|
|
255
|
+
assert.equal(realpathSync(aliasedRoot), f.root);
|
|
256
|
+
f.state.evidence.unit = f.receipt(); f.save();
|
|
257
|
+
const baseline = await f.read();
|
|
258
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
259
|
+
f.ctx.location.project.canonical = mask & 1 ? aliasedRoot : f.root;
|
|
260
|
+
f.ctx.location.directory = mask & 2 ? aliasedRoot : f.root;
|
|
261
|
+
f.session.location.directory = mask & 4 ? aliasedRoot : f.root;
|
|
262
|
+
assert.deepEqual(await f.read(), baseline, `native aliases ${mask}`);
|
|
263
|
+
rmSync(join(f.root, '.harness', 'active.json'));
|
|
264
|
+
assert.deepEqual(await f.read(), { source: null });
|
|
265
|
+
selectReport(f.root, f.report.path);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('task and operation worktrees require canonically matching session, journal and native inventory', async t => {
|
|
270
|
+
const f = fixture(t);
|
|
271
|
+
const worktree = join(f.base, 'worktree'); mkdirSync(worktree);
|
|
272
|
+
const foreign = join(f.base, 'foreign'); mkdirSync(foreign);
|
|
273
|
+
const alias = join(f.base, 'namespace'); symlinkSync(f.base, alias, 'junction');
|
|
274
|
+
const foreignAlias = join(f.base, 'foreign-namespace'); symlinkSync(foreign, foreignAlias, 'junction');
|
|
275
|
+
// Both paths have the same suffix, but different canonical targets.
|
|
276
|
+
const foreignWorktree = join(foreign, 'worktree'); mkdirSync(foreignWorktree);
|
|
277
|
+
const aliased = join(alias, 'worktree'), foreignAliased = join(foreignAlias, 'worktree');
|
|
278
|
+
const report = createReport(worktree, f.state, f.sessionID); selectReport(worktree, report.path);
|
|
279
|
+
const journal = join(f.root, '.harness', 'opencode-tasks.json');
|
|
280
|
+
const saveJournal = (kind, directory) => writeFileSync(journal, JSON.stringify({ version: 1, tasks: [], operations: [], [kind]: [{ sessionID: f.sessionID, worktree: directory }] }));
|
|
281
|
+
f.ctx.location.project.canonical = join(alias, 'project');
|
|
282
|
+
f.ctx.location.directory = join(alias, 'project');
|
|
283
|
+
for (const kind of ['tasks', 'operations']) {
|
|
284
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
285
|
+
f.session.location.directory = mask & 1 ? aliased : worktree;
|
|
286
|
+
saveJournal(kind, mask & 2 ? aliased : worktree);
|
|
287
|
+
f.nativeWorktrees.splice(0, Infinity, mask & 4 ? aliased : worktree);
|
|
288
|
+
assert.equal((await f.read()).source.runId, f.state.runId, `${kind} aliases ${mask}`);
|
|
289
|
+
}
|
|
290
|
+
f.session.location.directory = aliased;
|
|
291
|
+
f.nativeWorktrees.splice(0, Infinity, foreignAliased);
|
|
292
|
+
await rejected(f.read(), 'not_owned'); // Journal alone cannot grant authority.
|
|
293
|
+
f.nativeWorktrees.splice(0, Infinity, aliased);
|
|
294
|
+
saveJournal(kind, foreignAliased);
|
|
295
|
+
await rejected(f.read(), 'not_owned'); // Native inventory alone is insufficient.
|
|
296
|
+
saveJournal(kind, aliased);
|
|
297
|
+
f.session.location.directory = foreignAliased;
|
|
298
|
+
await rejected(f.read(), 'not_owned');
|
|
299
|
+
}
|
|
300
|
+
f.session.location.directory = aliased;
|
|
301
|
+
rmSync(journal);
|
|
302
|
+
await rejected(f.read(), 'not_owned', 'inventory without any journal cannot authorize a task worktree');
|
|
303
|
+
f.session.location.directory = join(foreignAlias, 'worktree');
|
|
304
|
+
await rejected(f.read(), 'not_owned', 'an ancestor alias cannot authorize a foreign canonical target');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('linked native directory leaves are rejected even when they resolve to an owned directory', async t => {
|
|
308
|
+
for (const target of ['root', 'location', 'session', 'journal', 'inventory']) {
|
|
309
|
+
await t.test(target, async t => {
|
|
310
|
+
const f = fixture(t);
|
|
311
|
+
const leaf = join(f.base, 'linked-leaf');
|
|
312
|
+
if (['root', 'location', 'session'].includes(target)) {
|
|
313
|
+
symlinkSync(f.root, leaf, 'junction');
|
|
314
|
+
if (target === 'root') f.ctx.location.project.canonical = leaf;
|
|
315
|
+
if (target === 'location') f.ctx.location.directory = leaf;
|
|
316
|
+
if (target === 'session') f.session.location.directory = leaf;
|
|
317
|
+
} else {
|
|
318
|
+
const worktree = join(f.base, 'worktree'); mkdirSync(worktree);
|
|
319
|
+
symlinkSync(worktree, leaf, 'junction');
|
|
320
|
+
f.session.location.directory = worktree;
|
|
321
|
+
writeFileSync(join(f.root, '.harness', 'opencode-tasks.json'), JSON.stringify({ version: 1, tasks: [{ sessionID: f.sessionID, worktree: target === 'journal' ? leaf : worktree }], operations: [] }));
|
|
322
|
+
f.nativeWorktrees.push(target === 'inventory' ? leaf : worktree);
|
|
323
|
+
}
|
|
324
|
+
await rejected(f.read(), 'invalid_source');
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('evidence symlink components and final files stay forbidden under canonical or ancestor-aliased authority', async t => {
|
|
330
|
+
for (const aliased of [false, true]) for (const target of ['report', 'run', 'session', 'harness', 'pointer', 'journal', 'root']) {
|
|
331
|
+
await t.test(`${aliased ? 'aliased' : 'canonical'} ${target}`, async t => {
|
|
332
|
+
const f = fixture(t);
|
|
333
|
+
if (aliased) {
|
|
334
|
+
const alias = join(f.base, 'alias'); symlinkSync(f.base, alias, 'junction');
|
|
335
|
+
f.ctx.location.directory = join(alias, 'project');
|
|
336
|
+
f.ctx.location.project.canonical = f.ctx.location.directory;
|
|
337
|
+
f.session.location.directory = f.ctx.location.directory;
|
|
338
|
+
}
|
|
339
|
+
writeFileSync(join(f.root, '.harness', 'opencode-tasks.json'), JSON.stringify({ version: 1, tasks: [], operations: [] }));
|
|
340
|
+
const replacements = {
|
|
341
|
+
report: f.report.path, run: f.report.dir, session: join(f.root, '.harness', f.sessionID),
|
|
342
|
+
harness: join(f.root, '.harness'), pointer: join(f.root, '.harness', 'active.json'), journal: join(f.root, '.harness', 'opencode-tasks.json'), root: f.root,
|
|
343
|
+
};
|
|
344
|
+
const path = replacements[target];
|
|
345
|
+
// Preserve fully valid content: a followed link must not merely fail later
|
|
346
|
+
// because its target contains malformed evidence or a missing pointer.
|
|
347
|
+
const elsewhere = join(f.base, 'private-target');
|
|
348
|
+
renameSync(path, elsewhere);
|
|
349
|
+
symlinkSync(elsewhere, path, ['report', 'pointer', 'journal'].includes(target) ? 'file' : 'junction');
|
|
350
|
+
await rejected(f.read(), 'invalid_source');
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const f = fixture(t);
|
|
354
|
+
rmSync(f.report.path); mkdirSync(f.report.path);
|
|
355
|
+
await rejected(f.read(), 'invalid_source');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('oversized/corrupt reports, forged run IDs, malformed schemas and unsafe pointers fail closed', async t => {
|
|
359
|
+
const f = fixture(t);
|
|
360
|
+
const original = JSON.stringify(f.state);
|
|
361
|
+
for (const body of ['{corrupt', ' '.repeat(1024 * 1024 + 1), '[]', 'null', '\ud800']) {
|
|
362
|
+
writeFileSync(f.report.path, body);
|
|
363
|
+
await rejected(f.read(), 'invalid_source');
|
|
364
|
+
}
|
|
365
|
+
const mutations = [
|
|
366
|
+
state => { state.runId = 'other-run'; }, state => { state.revision = 0; }, state => { state.revision = 1.5; },
|
|
367
|
+
state => { state.plan.checks = []; }, state => { state.evidence = []; },
|
|
368
|
+
state => { state.evidence.unknown = {}; }, state => { state.status = 'bogus'; },
|
|
369
|
+
state => { state.evidence.unit = f.receipt('unit', { passed: 'true' }); },
|
|
370
|
+
state => { state.evidence.unit = f.receipt('unit', { fingerprint: 'invalid-hash' }); },
|
|
371
|
+
state => { state.evidence.unit = f.receipt('unit', { outputTail: {} }); },
|
|
372
|
+
state => { state.reviewFindings = [{ id: '../private', description: 'bad ID' }]; },
|
|
373
|
+
state => { state.reviewFindings = [{ id: 'finding', description: 'bad resolution', resolution: { id: 'different', change: 'claim' } }]; },
|
|
374
|
+
state => { state.reviewFindings = [{ id: 'same', description: 'one' }, { id: 'same', description: 'two' }]; },
|
|
375
|
+
];
|
|
376
|
+
for (const mutate of mutations) {
|
|
377
|
+
const state = JSON.parse(original); mutate(state);
|
|
378
|
+
writeFileSync(f.report.path, JSON.stringify(state));
|
|
379
|
+
await rejected(f.read(), 'invalid_source');
|
|
380
|
+
}
|
|
381
|
+
writeFileSync(f.report.path, original);
|
|
382
|
+
const pointer = join(f.root, '.harness', 'active.json');
|
|
383
|
+
for (const body of ['{corrupt', ' '.repeat(4097), JSON.stringify({ version: 1, path: '/private/report.json' }), JSON.stringify({ version: 1, path: '.harness/../private/report.json' }), JSON.stringify({ version: 1, path: '.harness/session/run/../report.json' })]) {
|
|
384
|
+
writeFileSync(pointer, body);
|
|
385
|
+
await rejected(f.read(), 'invalid_source');
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('local read and registration have no automatic outbound requests or filesystem mutations', async t => {
|
|
390
|
+
const f = fixture(t);
|
|
391
|
+
f.state.evidence.unit = f.receipt(); f.save();
|
|
392
|
+
const snapshot = directory => readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
393
|
+
const path = join(directory, entry.name);
|
|
394
|
+
return entry.isDirectory() ? snapshot(path) : [[path, readFileSync(path).toString('hex')]];
|
|
395
|
+
});
|
|
396
|
+
const before = snapshot(f.root);
|
|
397
|
+
const originalFetch = globalThis.fetch;
|
|
398
|
+
globalThis.fetch = async () => { throw new Error('unexpected network'); };
|
|
399
|
+
try {
|
|
400
|
+
const registration = await registerDailySourceRpc(f.ctx);
|
|
401
|
+
assert.equal((await f.read()).source.checks[0].after, 'passed');
|
|
402
|
+
await registration.dispose();
|
|
403
|
+
} finally { globalThis.fetch = originalFetch; }
|
|
404
|
+
assert.deepEqual(f.counts(), { registrations: 1, disposed: 1 });
|
|
405
|
+
assert.deepEqual(snapshot(f.root), before);
|
|
406
|
+
const controller = new AbortController(); controller.abort(new Error('cancelled'));
|
|
407
|
+
await assert.rejects(f.handlers.report({ sessionID: f.sessionID }, { ...call, signal: controller.signal }), /cancelled/);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('pinned Effect RPC conversion accepts local source, null, input and safe typed error data', async t => {
|
|
411
|
+
const f = fixture(t);
|
|
412
|
+
f.state.evidence.unit = f.receipt(); f.save();
|
|
413
|
+
const decode = (schema, value) => Schema.decodeUnknownSync(SchemaRepresentation.fromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12(schema)))(value);
|
|
414
|
+
assert.throws(() => decode({ type: 'string', pattern: '^[a-z]+$' }, 'value'), 'exercise the actual pinned conversion unsupported-pattern failure');
|
|
415
|
+
assert.equal(DailySource.id, 'carthagent.daily-source');
|
|
416
|
+
assert.deepEqual(Object.keys(DailySource.methods), ['report']);
|
|
417
|
+
const method = DailySource.methods.report;
|
|
418
|
+
assert.deepEqual(decode(method.input, { sessionID: f.sessionID }), { sessionID: f.sessionID });
|
|
419
|
+
for (const value of [await f.read(), { source: null }]) assert.deepEqual(decode(method.output, value), value);
|
|
420
|
+
assert.deepEqual(decode(method.errors.dailySourceError, { code: 'not_owned' }), { code: 'not_owned' });
|
|
421
|
+
assert.throws(() => decode(method.input, {}));
|
|
422
|
+
const forged = await f.read(); forged.source.checks[0].before = 'failed';
|
|
423
|
+
assert.throws(() => decode(method.output, forged));
|
|
424
|
+
const wrongRevision = await f.read(); wrongRevision.source.revision = 3;
|
|
425
|
+
assert.throws(() => decode(method.output, wrongRevision));
|
|
426
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
5
|
+
import { runOfflineDemo } from '../lib/opencode/demo.mjs';
|
|
6
|
+
|
|
7
|
+
test('UI demo service executes the disposable Node tests and reports their real output', async () => {
|
|
8
|
+
const output = [];
|
|
9
|
+
const io = { log: line => output.push(line), error: line => output.push(line) };
|
|
10
|
+
let invocation;
|
|
11
|
+
const exitCode = await runOfflineDemo({
|
|
12
|
+
io,
|
|
13
|
+
execute: async (argv, options) => {
|
|
14
|
+
invocation = { argv, options };
|
|
15
|
+
return { code: 0, output: 'ℹ tests 2\nℹ pass 2', timedOut: false, cancelled: false };
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
assert.equal(exitCode, 0);
|
|
19
|
+
assert.equal(invocation.argv[0], process.execPath);
|
|
20
|
+
assert.deepEqual(invocation.argv.slice(1), ['--test', 'test/app.test.mjs']);
|
|
21
|
+
assert.equal(invocation.options.timeoutSeconds, 30);
|
|
22
|
+
assert.equal(dirname(resolve(invocation.options.cwd)), resolve(os.tmpdir()));
|
|
23
|
+
assert.equal(basename(invocation.options.cwd).startsWith('carthagent-native-demo-'), true);
|
|
24
|
+
assert.notEqual(resolve(invocation.options.cwd), resolve(process.cwd()), 'fixture runs in a disposable sandbox, not the repository');
|
|
25
|
+
assert.equal('signal' in invocation.options, true);
|
|
26
|
+
const text = output.join('\n');
|
|
27
|
+
assert.match(text, /no provider\/API calls/);
|
|
28
|
+
assert.match(text, /Fixture workspace:/);
|
|
29
|
+
assert.match(text, /tests 2|pass 2|# tests 2/i);
|
|
30
|
+
assert.match(text, /verified only inside this disposable demo fixture/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('UI demo service forwards cancellation and never claims a cancelled fixture passed', async () => {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const output = [];
|
|
36
|
+
const exitCode = await runOfflineDemo({
|
|
37
|
+
io: { log: line => output.push(line), error: line => output.push(line) },
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
execute: async (_argv, options) => {
|
|
40
|
+
assert.equal(options.signal, controller.signal);
|
|
41
|
+
controller.abort(new Error('demo cancelled'));
|
|
42
|
+
return { code: null, output: '', timedOut: false, cancelled: true };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
assert.equal(exitCode, 130);
|
|
46
|
+
assert.match(output.join('\n'), /cancelled; no completion is claimed/);
|
|
47
|
+
assert.doesNotMatch(output.join('\n'), /Verification: isolated Node test suite passed/);
|
|
48
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { runNativeEvaluation, EVALUATION_BUDGET_METADATA } from '../lib/opencode/evaluation.mjs';
|
|
4
|
+
|
|
5
|
+
test('evaluation forwards explicit agent and caller metadata without allowing budget replacement', async () => {
|
|
6
|
+
let admitted;
|
|
7
|
+
const client = {
|
|
8
|
+
server: { info: async () => ({ version: '2.0.32' }) },
|
|
9
|
+
plugin: { list: async () => ({ data: [{ id: 'ctg.evaluation-budget', state: { status: 'active' } }] }) },
|
|
10
|
+
session: { create: async input => { admitted = input; throw new Error('stop after admission capture'); } },
|
|
11
|
+
};
|
|
12
|
+
const result = await runNativeEvaluation({
|
|
13
|
+
client, directory: '/fixture', prompt: 'delivery task', provider: 'fixture', model: 'fixture',
|
|
14
|
+
agent: 'ship', maxTurns: 4, maxCost: 1,
|
|
15
|
+
metadata: { 'carthagent.options': { maxTools: 42 }, custom: 'preserved',
|
|
16
|
+
[EVALUATION_BUDGET_METADATA]: { maxTurns: 1000, maxCost: 999 } },
|
|
17
|
+
});
|
|
18
|
+
assert.equal(result.exitCode, 1);
|
|
19
|
+
assert.equal(admitted.agent, 'ship');
|
|
20
|
+
assert.deepEqual(admitted.metadata, {
|
|
21
|
+
'carthagent.options': { maxTools: 42 }, custom: 'preserved',
|
|
22
|
+
[EVALUATION_BUDGET_METADATA]: { maxTurns: 4, maxCost: 1 },
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -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
|
+
}
|