carthagent 1.1.0-preview.12 → 1.1.0-preview.13
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 +69 -13
- package/docs/native-manual-testing.md +27 -5
- package/docs/opencode-owned-runtime.md +3 -3
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/source-preview.md +120 -0
- package/lib/delivery-review.mjs +11 -1
- package/lib/opencode/runtime-release.json +12 -12
- package/lib/opencode/runtime.mjs +5 -4
- package/lib/providers/experiential.mjs +2 -2
- package/package.json +1 -1
- package/plugins/carthagent/src/daily-source-rpc.ts +2 -2
- package/plugins/carthagent/src/server/daily-source-rpc.ts +18 -4
- package/plugins/carthagent/src/server/delivery.ts +10 -6
- package/plugins/carthagent/src/server/providers.ts +1 -1
- package/plugins/carthagent/src/server/ship-guidance.ts +4 -2
- package/plugins/carthagent/src/tui.tsx +17 -13
- package/plugins/carthagent/src/ui/daily-quests.tsx +5 -2
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +2 -0
- package/plugins/carthagent/src/ui/native-review.mjs +7 -2
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +11 -2
- package/plugins/carthagent/src/ui/voyages.tsx +4 -2
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +20 -9
- package/script/ctg/build-owned-runtime.sh +3 -3
- package/tests/delivery-review.test.mjs +11 -0
- package/tests/native-daily-source.test.mjs +27 -2
- package/tests/native-manual-projects.test.mjs +3 -3
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-review-consumer.test.mjs +19 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-plan.test.mjs +13 -3
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/owned-runtime-publish-workflow.test.mjs +6 -6
- package/tests/runtime-development-trust.test.mjs +5 -5
- 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/update.test.mjs +6 -5
- package/tools/benchmark/planning-startup.test.mjs +1 -1
- package/tools/test-native-delivery-host.mjs +2 -2
- package/tools/test-native-host.mjs +4 -2
- package/tools/test-native-public-repo.mjs +1 -1
- package/tools/test-native-ship-walkthrough.mjs +1 -1
- package/tools/test-native-swe-host.mjs +4 -4
|
@@ -13,8 +13,8 @@ export function nativeReviewMode(session, config, explicitMode) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Correlate host executions and admit one current-run kickoff per session. */
|
|
16
|
-
/** @param {{ mode: (sessionID: string) => string, diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, offer: (value: { sessionID: string; runKey: string; writes: number; start: () => Promise<boolean> }) => Promise<unknown>, kickoff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, isLoopActive?: (sessionID: string) => boolean, setLoopActive?: (sessionID: string, active: boolean) => void, signal?: AbortSignal }} options */
|
|
17
|
-
export function createNativeReviewConsumer({ mode, diff, offer, kickoff, isLoopActive = () => false, setLoopActive = () => {}, signal }) {
|
|
16
|
+
/** @param {{ mode: (sessionID: string) => string, diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, offer: (value: { sessionID: string; runKey: string; writes: number; start: () => Promise<boolean> }) => Promise<unknown>, eligible?: (sessionID: string, signal?: AbortSignal) => Promise<boolean>, kickoff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, isLoopActive?: (sessionID: string) => boolean, setLoopActive?: (sessionID: string, active: boolean) => void, signal?: AbortSignal }} options */
|
|
17
|
+
export function createNativeReviewConsumer({ mode, diff, offer, kickoff, eligible = async () => true, isLoopActive = () => false, setLoopActive = () => {}, signal }) {
|
|
18
18
|
const runs = new Map();
|
|
19
19
|
const admitted = new Set();
|
|
20
20
|
const pending = new Set();
|
|
@@ -52,6 +52,11 @@ export function createNativeReviewConsumer({ mode, diff, offer, kickoff, isLoopA
|
|
|
52
52
|
const runKey = `${sessionID}@${run.id}`;
|
|
53
53
|
const currentMode = mode(sessionID);
|
|
54
54
|
if (!(currentMode === "ask" || currentMode === "yes") || writes <= 0 || admitted.has(runKey) || (currentMode === "yes" && loopSessions.has(sessionID))) return;
|
|
55
|
+
// A formally blocked or unfinished delivery is not a reviewable run.
|
|
56
|
+
let reviewable;
|
|
57
|
+
try { reviewable = await eligible(sessionID, signal); }
|
|
58
|
+
catch { return; }
|
|
59
|
+
if (!reviewable || disposed || signal?.aborted || runs.get(sessionID) !== run || admitted.has(runKey)) return;
|
|
55
60
|
admitted.add(runKey);
|
|
56
61
|
|
|
57
62
|
const start = async () => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MouseEvent, SelectRenderable } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
// SelectRenderable only handles keys; map a left click to the fully drawn row under the pointer and activate it.
|
|
4
|
+
// Bind it to mouse-up: an action that opens a dialog on mouse-down is closed by that dialog's backdrop on release.
|
|
5
|
+
export function clickSelect(node: SelectRenderable | undefined, event: MouseEvent) {
|
|
6
|
+
if (!node || event.button !== 0) return;
|
|
7
|
+
const { scrollOffset, linesPerItem, maxVisibleItems } = node as unknown as {
|
|
8
|
+
scrollOffset: number;
|
|
9
|
+
linesPerItem: number;
|
|
10
|
+
maxVisibleItems: number;
|
|
11
|
+
};
|
|
12
|
+
const row = Math.floor((event.y - node.screenY) / Math.max(1, linesPerItem));
|
|
13
|
+
const index = scrollOffset + row;
|
|
14
|
+
if (row < 0 || row >= maxVisibleItems || index >= node.options.length) return;
|
|
15
|
+
node.setSelectedIndex(index);
|
|
16
|
+
node.selectCurrent();
|
|
17
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Show, createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
|
|
3
3
|
import { CliRenderEvents, type ScrollBoxRenderable, type SelectRenderable } from "@opentui/core";
|
|
4
4
|
import { useRenderer } from "@opentui/solid";
|
|
5
5
|
import { usePlugin } from "@opencode/plugin/tui";
|
|
6
|
+
import { clickSelect } from "./select-mouse.js";
|
|
6
7
|
import { dailyQuestsClient, voyagesClient } from "./client.js";
|
|
7
8
|
import { nativeNavigation, type NavigationInput } from "./native-navigation.js";
|
|
8
9
|
import { createVoyagesNavigationState } from "./voyages-navigation-state.mjs";
|
|
@@ -27,6 +28,7 @@ export function VoyagesNavigation(props: NavigationInput) {
|
|
|
27
28
|
let scrollTop = 0;
|
|
28
29
|
let restoreTop: number | undefined;
|
|
29
30
|
const [viewportHeight, setViewportHeight] = createSignal(3);
|
|
31
|
+
const [railWidth, setRailWidth] = createSignal(0);
|
|
30
32
|
createEffect(() => {
|
|
31
33
|
if (!props.active) { scrollTop = scroll?.scrollTop ?? scrollTop; return; }
|
|
32
34
|
if (!view().busy) { restoreTop = scrollTop; renderer.requestRender(); }
|
|
@@ -46,6 +48,7 @@ export function VoyagesNavigation(props: NavigationInput) {
|
|
|
46
48
|
const frame = () => {
|
|
47
49
|
if (!scroll || scroll.isDestroyed || !props.active) return;
|
|
48
50
|
setViewportHeight(Math.max(1, scroll.viewport.height));
|
|
51
|
+
if (actions && !actions.isDestroyed) setRailWidth(actions.width);
|
|
49
52
|
if (restoreTop === undefined || view().busy) return;
|
|
50
53
|
scroll.scrollTop = restoreTop;
|
|
51
54
|
restoreTop = undefined;
|
|
@@ -69,7 +72,13 @@ export function VoyagesNavigation(props: NavigationInput) {
|
|
|
69
72
|
if (nativeNavigation(context)?.current() !== "voyages") return;
|
|
70
73
|
context.ui.router.navigate({ type: "plugin", name, data: { ...(props.sessionID ? { sessionID: props.sessionID } : {}), ...extra } });
|
|
71
74
|
};
|
|
72
|
-
|
|
75
|
+
// Select draws raw text; fit names and descriptions to the rail.
|
|
76
|
+
const fit = (text: string) => {
|
|
77
|
+
const room = railWidth() - 3;
|
|
78
|
+
return room > 0 && text.length > room ? `${text.slice(0, Math.max(0, room - 1))}…` : text;
|
|
79
|
+
};
|
|
80
|
+
const options = () => entries().map((item) => ({ ...item, name: fit(item.name), description: fit(item.description) }));
|
|
81
|
+
const entries = () => [
|
|
73
82
|
{ name: props.compact ? "Q" : "Daily coding quests", description: "Assignments & assessments", value: "daily" },
|
|
74
83
|
...(view().daily?.items.map((quest, index) => ({ name: props.compact ? String(index + 1) : quest.title, description: quest.status, value: `quest:${quest.id}` })) ?? []),
|
|
75
84
|
{ name: props.compact ? "P" : "Profile & wallet", description: "Authoritative profile", value: "profile" },
|
|
@@ -105,7 +114,7 @@ export function VoyagesNavigation(props: NavigationInput) {
|
|
|
105
114
|
<Show when={view().error}><text flexShrink={0} fg={context.theme.text.feedback.warning.base}> {view().error}. Sign in or refresh.</text></Show>
|
|
106
115
|
<Show when={view().daily}><text flexShrink={0} fg={context.theme.text.muted}> UTC {view().daily?.day}</text></Show>
|
|
107
116
|
</Show>
|
|
108
|
-
<select ref={actions} minHeight={1} height={Math.min(18, viewportHeight(), options().length * (props.compact ? 1 : 2))} flexShrink={0} showScrollIndicator showDescription={!props.compact} options={options()} selectedIndex={Math.max(0, options().findIndex((item) => item.value === selection()))} focused={focused()} onChange={(_index, item) => {
|
|
117
|
+
<select ref={actions} onMouseUp={(event) => { if (focused()) clickSelect(actions, event); }} minHeight={1} height={Math.min(18, viewportHeight(), options().length * (props.compact ? 1 : 2))} flexShrink={0} showScrollIndicator showDescription={!props.compact} options={options()} selectedIndex={Math.max(0, options().findIndex((item) => item.value === selection()))} focused={focused()} onChange={(_index, item) => {
|
|
109
118
|
if (!focused() || view().busy || !item) return;
|
|
110
119
|
setSelection(item.value);
|
|
111
120
|
if (actions) scroll?.scrollChildIntoView(actions.id);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { Show, createSignal, onCleanup, onMount } from "solid-js";
|
|
3
|
-
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import type { ScrollBoxRenderable, SelectRenderable } from "@opentui/core";
|
|
4
|
+
import { clickSelect } from "./select-mouse.js";
|
|
4
5
|
import { usePlugin } from "@opencode/plugin/tui";
|
|
5
6
|
import { voyagesClient, type VoyagesSnapshot, type VoyagesProfile, type VoyagesItem, type VoyagesQuote } from "./client.js";
|
|
6
7
|
import { canEquip, createVoyagesState, handleVoyagesEvent, voyagesCatalogLabel } from "./voyages-state.mjs";
|
|
@@ -9,6 +10,7 @@ type ViewState = { snapshot?: VoyagesSnapshot; busy: boolean; error: string; not
|
|
|
9
10
|
|
|
10
11
|
/** All mutations go through the lifetime-guarded controller, including dialogs. */
|
|
11
12
|
export function VoyagesPage(props: { sessionID?: string; section?: "profile" | "equipment" }) {
|
|
13
|
+
let list: SelectRenderable | undefined;
|
|
12
14
|
const context = usePlugin();
|
|
13
15
|
const location = () => (props.sessionID ? context.data.session.get(props.sessionID)?.location : undefined) ?? context.location ?? context.data.location.default();
|
|
14
16
|
const [view, setView] = createSignal<ViewState>({ busy: false, error: "", notice: "" });
|
|
@@ -122,6 +124,6 @@ export function VoyagesPage(props: { sessionID?: string; section?: "profile" | "
|
|
|
122
124
|
<text flexShrink={0} fg={context.theme.text.faint}>Choose Back to return. Cancel in dialogs never saves or purchases.</text>
|
|
123
125
|
</scrollbox>
|
|
124
126
|
<text flexShrink={0} fg={context.theme.text.faint}>↑/↓ Enter · PgUp/Dn choices · Ctrl+U/D profile</text>
|
|
125
|
-
<select flexBasis={0} flexGrow={1} flexShrink={0} minHeight={3} showScrollIndicator keyBindings={[{ name: "pageup", action: "move-up-fast" }, { name: "pagedown", action: "move-down-fast" }]} options={options()} focused={context.keymap.mode.current() === "base"} onSelect={(_index, option) => select(option?.value)} />
|
|
127
|
+
<select flexBasis={0} flexGrow={1} flexShrink={0} minHeight={3} ref={(node) => { list = node; }} onMouseUp={(event) => clickSelect(list, event)} showScrollIndicator keyBindings={[{ name: "pageup", action: "move-up-fast" }, { name: "pagedown", action: "move-down-fast" }]} options={options()} focused={context.keymap.mode.current() === "base"} onSelect={(_index, option) => select(option?.value)} />
|
|
126
128
|
</box>;
|
|
127
129
|
}
|
|
@@ -201,28 +201,39 @@ test("short terminal keyboard navigation keeps the highlighted action on screen"
|
|
|
201
201
|
} finally { f.app.renderer.destroy(); }
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
-
test("mouse
|
|
204
|
+
test("mouse press focuses the rail and release opens the clicked row without acting through a modal", async () => {
|
|
205
205
|
const f = await fixture();
|
|
206
206
|
try {
|
|
207
207
|
const row = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Daily coding quests"));
|
|
208
208
|
expect(row).toBeGreaterThanOrEqual(0);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
for (const mode of ["modal", "palette"]) {
|
|
210
|
+
f.setMode(mode);
|
|
211
|
+
await f.app.renderOnce();
|
|
212
|
+
await f.app.mockMouse.click(5, row);
|
|
213
|
+
await f.app.renderOnce();
|
|
214
|
+
expect(f.mode()).toBe(mode);
|
|
215
|
+
expect(f.routes).toHaveLength(0);
|
|
216
|
+
}
|
|
215
217
|
f.setMode("base");
|
|
216
218
|
f.app.renderer.clearSelection();
|
|
217
219
|
await f.app.renderOnce();
|
|
218
|
-
await f.app.mockMouse.
|
|
220
|
+
await f.app.mockMouse.pressDown(5, row);
|
|
219
221
|
await f.app.renderOnce();
|
|
220
222
|
expect(f.mode()).toBe("navigation");
|
|
221
223
|
expect(f.app.renderer.currentFocusedRenderable).toBeInstanceOf(SelectRenderable);
|
|
222
224
|
expect(f.routes).toHaveLength(0);
|
|
223
|
-
f.app.
|
|
225
|
+
await f.app.mockMouse.release(5, row);
|
|
224
226
|
await f.app.renderOnce();
|
|
225
227
|
expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } }]);
|
|
228
|
+
expect(f.mode()).toBe("base");
|
|
229
|
+
const questRow = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Review a change"));
|
|
230
|
+
expect(questRow).toBeGreaterThanOrEqual(0);
|
|
231
|
+
await f.app.mockMouse.click(5, questRow);
|
|
232
|
+
await f.app.renderOnce();
|
|
233
|
+
expect(f.routes).toEqual([
|
|
234
|
+
{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } },
|
|
235
|
+
{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-two" } },
|
|
236
|
+
]);
|
|
226
237
|
} finally { f.app.renderer.destroy(); }
|
|
227
238
|
});
|
|
228
239
|
|
|
@@ -6,7 +6,7 @@ set -euo pipefail
|
|
|
6
6
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
7
7
|
EXPECTED_SOURCE="843a6c514e606b436a7fe713422ace0a93ad3d61"
|
|
8
8
|
EXPECTED_LOCK="9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d"
|
|
9
|
-
EXPECTED_PATCH="
|
|
9
|
+
EXPECTED_PATCH="ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975"
|
|
10
10
|
TARGET="${1:-opencode-linux-x64}"
|
|
11
11
|
HOST_PLATFORM="$(node -p 'process.platform')"
|
|
12
12
|
HOST_ARCH="$(node -p 'process.arch')"
|
|
@@ -34,7 +34,7 @@ if ! git diff --cached --quiet HEAD --; then
|
|
|
34
34
|
exit 1
|
|
35
35
|
fi
|
|
36
36
|
expected_inputs=(
|
|
37
|
-
"docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
37
|
+
"docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch"
|
|
38
38
|
"script/ctg/build-owned-runtime.sh"
|
|
39
39
|
)
|
|
40
40
|
actual_status="$(git status --porcelain --untracked-files=all)"
|
|
@@ -86,7 +86,7 @@ if [[ "$(bun --version)" != "1.4.2" ]]; then
|
|
|
86
86
|
exit 1
|
|
87
87
|
fi
|
|
88
88
|
|
|
89
|
-
patch_file="$ROOT/docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
89
|
+
patch_file="$ROOT/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch"
|
|
90
90
|
actual_patch="$("${HASH_COMMAND[@]}" "$patch_file" | awk '{print $1}')"
|
|
91
91
|
if [[ "$actual_patch" != "$EXPECTED_PATCH" ]]; then
|
|
92
92
|
echo "Patch checksum mismatch: $actual_patch" >&2
|
|
@@ -137,3 +137,14 @@ test('large diffs retain full receipt instead of silently using output tail', as
|
|
|
137
137
|
assert.ok(readFileSync(capture.path, 'utf8').length > 12000);
|
|
138
138
|
assert.match(capture.diff, /diff --git/);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
test('review accepts assertion aliases and names missing requirements', async t => {
|
|
142
|
+
const { cwd, state, params, dir } = fixture(t);
|
|
143
|
+
const capture = await captureDeliveryReview(state, cwd, dir);
|
|
144
|
+
assert.throws(() => recordDeliveryReview(state, cwd, { ...params(capture.id), coverage: [{ requirement: 'Other', assertions: 'x' }] }), /Missing: \["Correct amount"\]/);
|
|
145
|
+
recordDeliveryReview(state, cwd, { ...params(capture.id), coverage: [{ requirement: 'Correct amount', assertion: 'app amount equals 2' }] });
|
|
146
|
+
assert.equal(state.reviewEvidence.coverage[0].assertions, 'app amount equals 2');
|
|
147
|
+
const again = await captureDeliveryReview(state, cwd, dir);
|
|
148
|
+
recordDeliveryReview(state, cwd, { ...params(again.id), coverage: [{ requirement: 'Correct amount', assertions: ['amount is 2', 'new result is 2'] }] });
|
|
149
|
+
assert.equal(state.reviewEvidence.coverage[0].assertions, 'amount is 2; new result is 2');
|
|
150
|
+
});
|
|
@@ -87,6 +87,29 @@ test('local source uses actual receipt identity, actual goal, no synthetic befor
|
|
|
87
87
|
assert.equal((await f.read()).source.checks[2].after, 'passed');
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test('explicit finish change summary, first-run failed baseline and long output tails are projected', async t => {
|
|
91
|
+
const f = fixture(t, [check('unit'), check('fresh')]);
|
|
92
|
+
f.state.evidence.unit = f.receipt('unit', { outputTail: `${'x'.repeat(900)}\nline two\n${'y'.repeat(300)}\nok 17 - final` });
|
|
93
|
+
f.state.evidence.fresh = f.receipt('fresh');
|
|
94
|
+
f.state.baselines = {
|
|
95
|
+
unit: { passed: false, code: 1, checkDigest: f.receipt('unit').checkDigest, fingerprint: '1'.repeat(64) },
|
|
96
|
+
fresh: { passed: false, code: 1, checkDigest: f.receipt('fresh').checkDigest, fingerprint: f.current },
|
|
97
|
+
};
|
|
98
|
+
f.state.handoff = { change: 'Implemented parseCsv as a state machine', review: 'r', fingerprint: f.current };
|
|
99
|
+
f.save();
|
|
100
|
+
const { source } = await f.read();
|
|
101
|
+
assert.equal(source.change, 'Implemented parseCsv as a state machine');
|
|
102
|
+
assert.equal(source.checks[0].before, 'failed');
|
|
103
|
+
// A failure recorded on the current source is not a before-change result.
|
|
104
|
+
assert.equal(source.checks[1].before, 'not_run');
|
|
105
|
+
assert.match(source.checks[0].result, /^…\n/);
|
|
106
|
+
assert.match(source.checks[0].result, /ok 17 - final$/);
|
|
107
|
+
assert.equal(source.truncated, true);
|
|
108
|
+
assert.ok(Buffer.byteLength(source.checks[0].result) <= 1024);
|
|
109
|
+
f.state.handoff.fingerprint = '2'.repeat(64); f.save();
|
|
110
|
+
assert.equal((await f.read()).source.change, '');
|
|
111
|
+
});
|
|
112
|
+
|
|
90
113
|
test('stale, mismatched or contradictory passing receipts never become current passing evidence', async t => {
|
|
91
114
|
const f = fixture(t);
|
|
92
115
|
const invalidations = [
|
|
@@ -179,7 +202,9 @@ test('selection is bounded, UTF-8 safe and explicitly reports omitted claims/ite
|
|
|
179
202
|
assert.equal(source.truncated, true);
|
|
180
203
|
assert.equal(source.goal, '');
|
|
181
204
|
assert.equal(source.checks[0].result, '界'.repeat(341));
|
|
182
|
-
|
|
205
|
+
// Receipts store an output tail; an oversized tail keeps its end, never a partial start.
|
|
206
|
+
assert.equal(source.checks[1].result.startsWith('…\n'), true);
|
|
207
|
+
assert.ok(Buffer.byteLength(source.checks[1].result, 'utf8') <= 1024);
|
|
183
208
|
assert.equal(source.findings[0].issue, '');
|
|
184
209
|
assert.equal(source.findings[0].evidence, '');
|
|
185
210
|
assert.equal(JSON.stringify(source).includes('\ufffd'), false);
|
|
@@ -419,7 +444,7 @@ test('pinned Effect RPC conversion accepts local source, null, input and safe ty
|
|
|
419
444
|
for (const value of [await f.read(), { source: null }]) assert.deepEqual(decode(method.output, value), value);
|
|
420
445
|
assert.deepEqual(decode(method.errors.dailySourceError, { code: 'not_owned' }), { code: 'not_owned' });
|
|
421
446
|
assert.throws(() => decode(method.input, {}));
|
|
422
|
-
const forged = await f.read(); forged.source.checks[0].before = '
|
|
447
|
+
const forged = await f.read(); forged.source.checks[0].before = 'passed';
|
|
423
448
|
assert.throws(() => decode(method.output, forged));
|
|
424
449
|
const wrongRevision = await f.read(); wrongRevision.source.revision = 3;
|
|
425
450
|
assert.throws(() => decode(method.output, wrongRevision));
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { CTG_RUNTIME_SOURCE, CTG_RUNTIME_PATCH_SHA256,
|
|
8
|
+
import { CTG_RUNTIME_SOURCE, CTG_RUNTIME_PATCH_SHA256, CTG_RUNTIME_LOCK_SHA256 } from '../lib/opencode/runtime.mjs';
|
|
9
9
|
import { createManualProjects } from '../tools/native-manual-projects.mjs';
|
|
10
10
|
import { readManualRuntimeManifest } from '../tools/manual-runtime-manifest.mjs';
|
|
11
11
|
import { readRuntimeReleaseManifest } from '../lib/opencode/runtime.mjs';
|
|
@@ -71,7 +71,7 @@ test('manual kit preparation refuses existing directories and removes only its o
|
|
|
71
71
|
const runtimeManifest = join(temp, 'runtime-release.json');
|
|
72
72
|
const manifest = JSON.parse(await readFile(new URL('../lib/opencode/runtime-release.json', import.meta.url), 'utf8'));
|
|
73
73
|
manifest.releaseVersion = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
74
|
-
Object.assign(manifest, { sourceRevision: CTG_RUNTIME_SOURCE, patchSha256:
|
|
74
|
+
Object.assign(manifest, { sourceRevision: CTG_RUNTIME_SOURCE, patchSha256: CTG_RUNTIME_PATCH_SHA256, lockSha256: CTG_RUNTIME_LOCK_SHA256 });
|
|
75
75
|
const target = `${process.platform === 'win32' ? 'windows' : process.platform}-${process.arch}`;
|
|
76
76
|
manifest.assets = { [target]: manifest.assets[target] };
|
|
77
77
|
const bytes = JSON.stringify(manifest);
|
|
@@ -145,7 +145,7 @@ test('single-platform manual candidates bind current recipe bytes without weaken
|
|
|
145
145
|
const runtimeManifest = join(temp, 'candidate.json');
|
|
146
146
|
const manifest = JSON.parse(await readFile(new URL('../lib/opencode/runtime-release.json', import.meta.url), 'utf8'));
|
|
147
147
|
manifest.releaseVersion = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
148
|
-
Object.assign(manifest, { sourceRevision: CTG_RUNTIME_SOURCE, patchSha256:
|
|
148
|
+
Object.assign(manifest, { sourceRevision: CTG_RUNTIME_SOURCE, patchSha256: CTG_RUNTIME_PATCH_SHA256, lockSha256: CTG_RUNTIME_LOCK_SHA256 });
|
|
149
149
|
const asset = manifest.assets['linux-x64'];
|
|
150
150
|
manifest.assets = { 'linux-x64': asset };
|
|
151
151
|
const save = async () => {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { createJiti } = require('jiti');
|
|
8
|
+
const jiti = createJiti(import.meta.url);
|
|
9
|
+
const { runModelCommand } = await jiti.import('../plugins/carthagent/src/ui/model-command.ts');
|
|
10
|
+
|
|
11
|
+
const ship = { providerID: 'experiential-labs', modelID: 'deepseek-v4.1-flash', id: 'deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash' };
|
|
12
|
+
const other = { providerID: 'demo', modelID: 'alpha', id: 'alpha', name: 'Alpha' };
|
|
13
|
+
|
|
14
|
+
function host(overrides = {}) {
|
|
15
|
+
const calls = { native: [], switched: [], selects: [], toasts: [], syncs: 0 };
|
|
16
|
+
return {
|
|
17
|
+
calls,
|
|
18
|
+
value: {
|
|
19
|
+
sync: async () => { calls.syncs++; },
|
|
20
|
+
list: () => [ship, other],
|
|
21
|
+
select: async (options) => { calls.selects.push(options.map((option) => option.title)); return options[0]?.value; },
|
|
22
|
+
switchModel: async (sessionID, model) => { calls.switched.push({ sessionID, model }); },
|
|
23
|
+
openNativePicker: (query) => { calls.native.push(query); },
|
|
24
|
+
providerName: (id) => (id === 'experiential-labs' ? 'Carthagent Ship' : id),
|
|
25
|
+
toast: (message, variant) => { calls.toasts.push({ message, variant }); },
|
|
26
|
+
...overrides,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('pre-session /model opens the native configured-model picker with its argument and never switches a session', async () => {
|
|
32
|
+
const bare = host();
|
|
33
|
+
await runModelCommand(bare.value, { arg: '' });
|
|
34
|
+
assert.deepEqual(bare.calls.native, ['']);
|
|
35
|
+
const named = host();
|
|
36
|
+
await runModelCommand(named.value, { arg: 'deepseek' });
|
|
37
|
+
assert.deepEqual(named.calls.native, ['deepseek']);
|
|
38
|
+
for (const run of [bare, named]) {
|
|
39
|
+
assert.deepEqual(run.calls.switched, []);
|
|
40
|
+
assert.deepEqual(run.calls.selects, []);
|
|
41
|
+
assert.equal(run.calls.toasts.some((toast) => /Open or create a session/.test(toast.message)), false);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('existing-session /model keeps filtering by argument and commits the pick to that session only', async () => {
|
|
46
|
+
const run = host();
|
|
47
|
+
await runModelCommand(run.value, { sessionID: 'ses_existing', arg: 'deepseek' });
|
|
48
|
+
assert.equal(run.calls.syncs, 1);
|
|
49
|
+
assert.deepEqual(run.calls.selects, [['Carthagent Ship/deepseek-v4.1-flash']]);
|
|
50
|
+
assert.deepEqual(run.calls.switched, [{ sessionID: 'ses_existing', model: { providerID: 'experiential-labs', id: 'deepseek-v4.1-flash' } }]);
|
|
51
|
+
assert.deepEqual(run.calls.native, []);
|
|
52
|
+
assert.deepEqual(run.calls.toasts, [{ message: 'Selected Carthagent Ship/deepseek-v4.1-flash.', variant: 'success' }]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('cancelling the existing-session picker changes nothing', async () => {
|
|
56
|
+
const run = host({ select: async () => undefined });
|
|
57
|
+
await runModelCommand(run.value, { sessionID: 'ses_existing', arg: '' });
|
|
58
|
+
assert.deepEqual(run.calls.switched, []);
|
|
59
|
+
assert.deepEqual(run.calls.toasts, []);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('plugin /model dispatch routes through the shared command and the native model.list command', async () => {
|
|
63
|
+
const tui = await readFile(new URL('../plugins/carthagent/src/tui.tsx', import.meta.url), 'utf8');
|
|
64
|
+
assert.match(tui, /runModelCommand\(/);
|
|
65
|
+
assert.match(tui, /keymap\.dispatch\("model\.list", query\)/);
|
|
66
|
+
assert.doesNotMatch(tui, /Open or create a session before selecting its model/);
|
|
67
|
+
});
|
|
@@ -58,6 +58,25 @@ test('ask confirmation is current-run-bound and one-shot; a later run invalidate
|
|
|
58
58
|
assert.deepEqual(kickoffs, ['session-a']);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
test('a run whose formal delivery is not reviewable is never offered or kicked off', async () => {
|
|
62
|
+
for (const mode of ['ask', 'yes']) {
|
|
63
|
+
const offers = [], kickoffs = [], asked = [];
|
|
64
|
+
const consumer = createNativeReviewConsumer({
|
|
65
|
+
mode: () => mode, diff: makeDiff(), offer: async (offer) => offers.push(offer),
|
|
66
|
+
eligible: async (sessionID) => { asked.push(sessionID); return false; },
|
|
67
|
+
kickoff: async (sessionID) => kickoffs.push(sessionID),
|
|
68
|
+
});
|
|
69
|
+
consumer.onStarted(execution('run-1', 'session-a'));
|
|
70
|
+
await consumer.onSucceeded(execution('run-1', 'session-a'));
|
|
71
|
+
assert.deepEqual([offers.length, kickoffs.length, asked], [0, 0, ['session-a']], mode);
|
|
72
|
+
}
|
|
73
|
+
const offers = [];
|
|
74
|
+
const failing = createNativeReviewConsumer({ mode: () => 'ask', diff: makeDiff(), offer: async (offer) => offers.push(offer), eligible: async () => { throw new Error('rpc down'); }, kickoff: async () => {} });
|
|
75
|
+
failing.onStarted(execution('run-1', 'session-a'));
|
|
76
|
+
await failing.onSucceeded(execution('run-1', 'session-a'));
|
|
77
|
+
assert.equal(offers.length, 0, 'an unknown delivery state fails closed');
|
|
78
|
+
});
|
|
79
|
+
|
|
61
80
|
test('yes kicks off only successful new-change runs and suppresses only that session loop', async () => {
|
|
62
81
|
const loops = new Set();
|
|
63
82
|
const started = [];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { clickSelect } from '../plugins/carthagent/src/ui/select-mouse.ts';
|
|
4
|
+
|
|
5
|
+
const node = (scrollOffset, linesPerItem) => {
|
|
6
|
+
const calls = [];
|
|
7
|
+
return { calls, scrollOffset, linesPerItem, maxVisibleItems: 3, screenY: 10, options: [{}, {}, {}, {}], setSelectedIndex: i => calls.push(['set', i]), selectCurrent: () => calls.push(['select']) };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
test('clicks activate the row under the pointer, accounting for scroll and row height', () => {
|
|
11
|
+
const n = node(1, 2);
|
|
12
|
+
clickSelect(n, { button: 0, y: 13 });
|
|
13
|
+
assert.deepEqual(n.calls, [['set', 2], ['select']]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('clicks outside rows or with other buttons are ignored', () => {
|
|
17
|
+
for (const event of [{ button: 0, y: 9 }, { button: 0, y: 30 }, { button: 0, y: 13 }, { button: 2, y: 10 }]) {
|
|
18
|
+
const n = node(0, 1);
|
|
19
|
+
clickSelect(n, event);
|
|
20
|
+
assert.deepEqual(n.calls, []);
|
|
21
|
+
}
|
|
22
|
+
clickSelect(undefined, { button: 0, y: 10 });
|
|
23
|
+
});
|
|
@@ -5,12 +5,12 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { runInNewContext } from 'node:vm';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256 } from '../lib/opencode/runtime.mjs';
|
|
8
|
+
import { CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256, CTG_RUNTIME_PATCH_SHA256 } from '../lib/opencode/runtime.mjs';
|
|
9
9
|
import ts from 'typescript';
|
|
10
10
|
import { parseOpenCodeArgs } from '../lib/opencode/cli.mjs';
|
|
11
11
|
import { normalizeSessionOptions } from '../lib/opencode/session-options.mjs';
|
|
12
12
|
|
|
13
|
-
const patch = readFileSync(new URL('../docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
13
|
+
const patch = readFileSync(new URL('../docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch', import.meta.url), 'utf8');
|
|
14
14
|
const path = 'packages/tui/src/routes/session/plan-items.ts';
|
|
15
15
|
const section = patch.split(`diff --git a/${path} b/${path}\n`)[1]?.split('\ndiff --git ')[0];
|
|
16
16
|
assert.ok(section, 'the plan graph parser is part of the reproducible runtime patch');
|
|
@@ -67,9 +67,19 @@ test('native CLI preserves bounded-run and advisor controls in session options',
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
test('development runtime source and
|
|
70
|
+
test('development runtime source and preparation recipe pin the actual Ship graph patch bytes', () => {
|
|
71
71
|
const digest = createHash('sha256').update(patch).digest('hex');
|
|
72
72
|
assert.equal(CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256, digest);
|
|
73
|
+
const prepare = readFileSync(new URL('../script/ctg/prepare-development-runtime.sh', import.meta.url), 'utf8');
|
|
74
|
+
assert.ok(prepare.includes(`EXPECTED_PATCH="${digest}"`));
|
|
75
|
+
assert.ok(prepare.includes('docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch'));
|
|
76
|
+
assert.doesNotMatch(prepare, /bun (install|run|build)|gh release|npm publish/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the shared release build recipe stays on the published patch', () => {
|
|
80
|
+
const published = readFileSync(new URL('../docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch', import.meta.url));
|
|
81
|
+
const digest = createHash('sha256').update(published).digest('hex');
|
|
82
|
+
assert.equal(CTG_RUNTIME_PATCH_SHA256, digest);
|
|
73
83
|
const build = readFileSync(new URL('../script/ctg/build-owned-runtime.sh', import.meta.url), 'utf8');
|
|
74
84
|
assert.ok(build.includes(`EXPECTED_PATCH="${digest}"`));
|
|
75
85
|
});
|
|
@@ -40,6 +40,22 @@ function host(t) {
|
|
|
40
40
|
return { ctx, tools, hooks, commands, stores, directory, session, sessions, interrupts }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
test('delivery_review coverage schema accepts assertion aliases models emit', async t => {
|
|
44
|
+
const h = host(t)
|
|
45
|
+
await registerDelivery(h.ctx, { workflow: 'standard' })
|
|
46
|
+
const item = h.tools.get('delivery_review').input.properties.coverage.items
|
|
47
|
+
assert.deepEqual(item.required, ['requirement'])
|
|
48
|
+
assert.deepEqual(item.properties.assertions.anyOf.map(s => s.type), ['string', 'array'])
|
|
49
|
+
assert.equal(item.properties.assertion.type, 'string')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('code-mode catalog always lists the delivery_plan entry point', async t => {
|
|
53
|
+
const h = host(t)
|
|
54
|
+
await registerDelivery(h.ctx, { workflow: 'standard' })
|
|
55
|
+
const pinned = [...h.tools.values()].filter(def => def.options?.pinned).map(def => def.name)
|
|
56
|
+
assert.deepEqual(pinned, ['delivery_plan'])
|
|
57
|
+
})
|
|
58
|
+
|
|
43
59
|
test('OpenCode V2 registration exposes domain tools and returns complete cleanup', async t => {
|
|
44
60
|
const h = host(t)
|
|
45
61
|
const cleanup = await registerDelivery(h.ctx, { workflow: 'standard' })
|
|
@@ -106,7 +106,7 @@ function workflowJob(name) {
|
|
|
106
106
|
return lines.slice(start, end).join('\n');
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const combinedPatchPath = 'docs/patches/opencode-v2.0.32-session-ui-combined-
|
|
109
|
+
const combinedPatchPath = 'docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch';
|
|
110
110
|
|
|
111
111
|
function sha256(bytes) {
|
|
112
112
|
return createHash('sha256').update(bytes).digest('hex');
|
|
@@ -250,7 +250,7 @@ const patch=await readFile(path.join(repo,${JSON.stringify(combinedPatchPath)}))
|
|
|
250
250
|
const expected=recipe.match(/^EXPECTED_PATCH="([^"]+)"$/m)?.[1];
|
|
251
251
|
if(!expected || sha(patch)!==expected) throw new Error('recipe patch digest mismatch: expected='+expected+' actual='+sha(patch)+' path='+${JSON.stringify(combinedPatchPath)});
|
|
252
252
|
const runtimeSource=await readFile(path.join(repo,'lib/opencode/runtime.mjs'),'utf8');
|
|
253
|
-
const runtimePatch=runtimeSource.match(/
|
|
253
|
+
const runtimePatch=runtimeSource.match(/CTG_RUNTIME_PATCH_SHA256 = '([a-f0-9]{64})'/)?.[1];
|
|
254
254
|
if(runtimePatch!==expected) throw new Error('fixture packaged runtime trust root patch digest mismatch');
|
|
255
255
|
const manifest={version:1,releaseVersion:version,compatibilityVersion:'2.0.32',runtimeVersion:'0.0.0-ctg.2.0.32',sourceRevision:'${sourceCommit}',patchSha256:expected,lockSha256:'${lockSha256}',assets:Object.fromEntries(keys.map((key,i)=>[key,{name:names[i],archiveSha256:'0'.repeat(64),executableSha256:'0'.repeat(64)}]))};
|
|
256
256
|
for(let i=0;i<names.length;i++) {
|
|
@@ -403,9 +403,9 @@ test('draft metadata validation has scoped write capability; artifact mutation s
|
|
|
403
403
|
assert.doesNotMatch(assembly, /contents: write/);
|
|
404
404
|
assert.match(publish, /contents: write/);
|
|
405
405
|
assert.match(uploadBlock, /gh release upload/);
|
|
406
|
-
assert.match(workflow, /git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
407
|
-
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
408
|
-
assert.equal((workflow.match(/git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
406
|
+
assert.match(workflow, /git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch > opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch/);
|
|
407
|
+
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch\)"/);
|
|
408
|
+
assert.equal((workflow.match(/git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch > ctg\/docs\/patches\/opencode-v2\.0\.32-session-ui\.patch/g) ?? []).length, 1,
|
|
409
409
|
'candidate assembly must extract exact committed bytes at the pinned historical path');
|
|
410
410
|
assert.match(verifyBlock, /git -C ctg show HEAD:script\/ctg\/build-owned-runtime\.sh/);
|
|
411
411
|
assert.match(verifyBlock, /git -C ctg show "HEAD:\$patch_file" > ctg\/docs\/patches\/opencode-v2\.0\.32-session-ui\.patch/);
|
|
@@ -413,7 +413,7 @@ test('draft metadata validation has scoped write capability; artifact mutation s
|
|
|
413
413
|
assert.match(workflow, /chmod \+x opencode\/script\/ctg\/build-owned-runtime\.sh/);
|
|
414
414
|
assert.match(workflow, /GIT_CONFIG_KEY_0:\s*core\.autocrlf[\s\S]*?GIT_CONFIG_VALUE_0:\s*'false'/);
|
|
415
415
|
assert.match(workflow, /git -C opencode config --local core\.autocrlf false/);
|
|
416
|
-
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-
|
|
416
|
+
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch\)"/);
|
|
417
417
|
});
|
|
418
418
|
|
|
419
419
|
test('review-branch prebuild source is exact dispatch SHA; publish validation is default-branch-only', { skip: requiresUbuntuWorkflowShell, timeout: 60000 }, async (t) => {
|
|
@@ -82,10 +82,10 @@ async function fixture(t, patchSha256 = developmentPatch) {
|
|
|
82
82
|
writeManifest, writeSums, calls, spawns, spawnVersion, spawnImpl, publicOptions };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
test('published preview.
|
|
86
|
-
assert.equal(CTG_RUNTIME_PATCH_SHA256, '
|
|
85
|
+
test('published preview.13 release trust stays valid independently of the development patch pin', async t => {
|
|
86
|
+
assert.equal(CTG_RUNTIME_PATCH_SHA256, 'ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975');
|
|
87
87
|
const published = await readRuntimeReleaseManifest();
|
|
88
|
-
assert.equal(published.releaseVersion, '1.1.0-preview.
|
|
88
|
+
assert.equal(published.releaseVersion, '1.1.0-preview.13');
|
|
89
89
|
const f = await fixture(t, CTG_RUNTIME_PATCH_SHA256);
|
|
90
90
|
assert.deepEqual(await f.runtime.readRuntimeReleaseManifest(f.releasePath), f.release);
|
|
91
91
|
assert.equal(await f.runtime.resolveOpenCode(f.publicOptions), f.binary);
|
|
@@ -98,7 +98,7 @@ test('published preview.12 release trust stays valid independently of the develo
|
|
|
98
98
|
assert.equal(acquisition.expectedCompatibilityVersion, f.runtime.OPENCODE_VERSION);
|
|
99
99
|
assert.equal(acquisition.expectedRuntimeVersion, f.runtime.CTG_RUNTIME_VERSION);
|
|
100
100
|
assert.equal(acquisition.expectedChannel, f.manifest.channel);
|
|
101
|
-
assert.match(acquisition.baseUrl, /\/v1\.1\.0-preview\.
|
|
101
|
+
assert.match(acquisition.baseUrl, /\/v1\.1\.0-preview\.13$/);
|
|
102
102
|
const identity = await f.runtime.runtimeIdentity(f.binary, f.publicOptions);
|
|
103
103
|
assert.equal(identity.kind, 'ctg-owned');
|
|
104
104
|
assert.equal(identity.patchSha256, CTG_RUNTIME_PATCH_SHA256);
|
|
@@ -117,7 +117,7 @@ test('public release manifest cannot promote the development pin or unpinned pro
|
|
|
117
117
|
for (const changes of [
|
|
118
118
|
{ patchSha256: developmentPatch }, { sourceRevision: 'e'.repeat(40) },
|
|
119
119
|
{ lockSha256: 'e'.repeat(64) }, { runtimeVersion: '0.0.0-ctg.2.0.33' },
|
|
120
|
-
{ compatibilityVersion: '2.0.33' }, { releaseVersion: '1.1.0-preview.
|
|
120
|
+
{ compatibilityVersion: '2.0.33' }, { releaseVersion: '1.1.0-preview.14' },
|
|
121
121
|
]) {
|
|
122
122
|
await writeFile(f.releasePath, JSON.stringify({ ...f.release, ...changes }));
|
|
123
123
|
await assert.rejects(f.runtime.resolveOpenCode(f.publicOptions), /Invalid or incompatible.*release manifest/);
|
package/tests/server.test.mjs
CHANGED
|
@@ -79,7 +79,9 @@ test('dashboard status reports an available update from the release cache', asyn
|
|
|
79
79
|
// Seed the throttled cache so the check resolves without touching the network.
|
|
80
80
|
const cachePath = join(dir, 'update-check.json');
|
|
81
81
|
writeFileSync(cachePath, JSON.stringify({ checkedAt: Date.now(), release: { version: '99.0.0', tag: 'v99.0.0' } }));
|
|
82
|
-
const
|
|
82
|
+
const channelPath = join(dir, 'update-channel.json');
|
|
83
|
+
writeFileSync(channelPath, JSON.stringify({ version: 1, channel: 'stable' }));
|
|
84
|
+
const server = await serverFixture(t, { CARTHAGENT_UPDATE_CHECK_FILE: cachePath, CARTHAGENT_UPDATE_CHANNEL_FILE: channelPath });
|
|
83
85
|
let body = {};
|
|
84
86
|
for (let i = 0; i < 50 && !body.update; i++) {
|
|
85
87
|
body = await (await fetch(`${server.url}/api/status`, { headers: { 'x-carthagent-token': server.token } })).json();
|
|
@@ -234,7 +234,7 @@ test('update check sends the install headers to the control plane only', async t
|
|
|
234
234
|
const agentDir = tmp(t);
|
|
235
235
|
const seen = [];
|
|
236
236
|
const { release, source } = await fetchLatestRelease({
|
|
237
|
-
cloudUrl: 'https://cloud.test', env: {}, agentDir,
|
|
237
|
+
cloudUrl: 'https://cloud.test', env: {}, agentDir, channel: 'stable',
|
|
238
238
|
fetchImpl: async (url, options) => {
|
|
239
239
|
seen.push({ url, headers: options.headers });
|
|
240
240
|
if (url.includes('/v1/cli/releases/latest')) return { status: 503, json: async () => ({}) };
|