@botiverse/k-carrier 0.1.7 → 0.2.0
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/NOTICE +5 -2
- package/README.md +72 -26
- package/core/src/artifact/download.ts +23 -1
- package/core/src/artifact/gzip.ts +26 -0
- package/core/src/artifact/source.ts +2 -0
- package/core/src/{createUpgrader.ts → createRunner.ts} +15 -14
- package/core/src/index.ts +12 -3
- package/core/src/launcher/launch.ts +15 -0
- package/core/src/launcher/supervise.ts +170 -0
- package/core/src/lifecycle/commandHost.ts +111 -0
- package/core/src/lifecycle/hostAdapter.ts +28 -16
- package/core/src/operation.ts +47 -25
- package/core/src/operationLifecycle.ts +2 -7
- package/core/src/platform/ops.ts +7 -0
- package/core/src/platform/posix.ts +26 -6
- package/core/src/platform/windows.ts +8 -2
- package/core/src/protocol/runner.ts +81 -0
- package/core/src/provenance/journal.ts +1 -1
- package/core/src/quarantine.ts +167 -0
- package/core/src/runner/cli.ts +27 -0
- package/core/src/runner/execute.ts +68 -0
- package/core/src/txn/engine.ts +41 -85
- package/core/src/txn/fileEffects.ts +15 -1
- package/core/src/txn/hostCallBudget.ts +4 -1
- package/core/src/txn/hostCallUncertain.ts +2 -0
- package/core/src/txn/lock.ts +81 -37
- package/core/src/txn/state.ts +1 -1
- package/core/src/upgrade/drive.ts +31 -2
- package/core/src/upgrade/outcome.ts +1 -1
- package/core/src/upgrade/recover.ts +22 -1
- package/core/src/upgrade/retire.ts +1 -1
- package/core/src/upgrader.ts +7 -8
- package/docs/design.md +173 -0
- package/docs/guide.md +196 -0
- package/docs/harness-design.md +75 -170
- package/docs/integration.md +221 -354
- package/docs/prior-art/design-influences.md +26 -0
- package/docs/prior-art/external-runner-research.md +49 -0
- package/docs/reference.md +209 -0
- package/docs/test-plan.md +89 -92
- package/harness/src/adapter/releaseKnob.ts +1 -1
- package/harness/src/adapter/serviceChecks.ts +5 -5
- package/harness/src/artifact/m1.ts +8 -8
- package/harness/src/artifact/m1Resume.ts +2 -2
- package/harness/src/artifact/m3.ts +25 -104
- package/harness/src/artifact/m3Hosts.ts +9 -61
- package/harness/src/artifact/m4.ts +3 -3
- package/harness/src/artifact/m5.ts +5 -5
- package/harness/src/artifact/m6.ts +6 -6
- package/harness/src/artifact/m6Status.ts +1 -1
- package/harness/src/examples/checks.ts +10 -13
- package/harness/src/fixtures/cliToolSource.ts +166 -0
- package/harness/src/fixtures/externalCrashAdapter.ts +19 -0
- package/harness/src/fixtures/managedHost.ts +100 -0
- package/harness/src/fixtures/serviceSource.ts +181 -0
- package/harness/src/fixtures/supervisedAdapter.ts +57 -0
- package/harness/src/scenario/processScan.ts +3 -1
- package/harness/src/scenario/sandbox.ts +2 -2
- package/harness/src/teeth/artifact.ts +3 -3
- package/harness/src/teeth/examples.ts +1 -1
- package/package.json +5 -3
- package/docs/design-v1.md +0 -246
- package/docs/prior-art.md +0 -150
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { type ToothContext } from "../teeth/registry.ts";
|
|
4
|
-
import { PLAIN_DAEMON_SOURCE } from "
|
|
4
|
+
import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
|
|
5
5
|
import { serveRelease } from "./m1.ts";
|
|
6
6
|
import { runCommand } from "../artifact-factory/run.ts";
|
|
7
7
|
import { processAlive } from "../fake-host/daemon.ts";
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
serviceEnv,
|
|
11
11
|
readIncarnation,
|
|
12
12
|
killIncarnation,
|
|
13
|
-
respawnUntilUp,
|
|
14
13
|
seedService,
|
|
15
14
|
waitDead,
|
|
16
15
|
HOST_SHAPES,
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
import { readState } from "./m1.ts";
|
|
20
19
|
|
|
21
20
|
// ---------------------------------------------------------------------------
|
|
22
|
-
// m3.service-upgrade (
|
|
21
|
+
// m3.service-upgrade (external driver)
|
|
23
22
|
// ---------------------------------------------------------------------------
|
|
24
23
|
|
|
25
24
|
export async function checkM3ServiceUpgrade(
|
|
@@ -52,7 +51,6 @@ async function runUpgradeShape(
|
|
|
52
51
|
});
|
|
53
52
|
const { seedChild, seedPid } = await seedService(shapeCtx, shape, binPath, seed);
|
|
54
53
|
const env = serviceEnv(shapeCtx, shape, target.url);
|
|
55
|
-
let respawned: Awaited<ReturnType<typeof respawnUntilUp>> | null = null;
|
|
56
54
|
try {
|
|
57
55
|
const before = await readIncarnation(shapeCtx);
|
|
58
56
|
assert.ok(before, "a running incarnation must exist before the upgrade");
|
|
@@ -66,12 +64,6 @@ async function runUpgradeShape(
|
|
|
66
64
|
0,
|
|
67
65
|
`self upgrade must exit 0 (${up.stderr.trim()}; serveBadVersion mutation => RED)`,
|
|
68
66
|
);
|
|
69
|
-
if (shape === "respawn") {
|
|
70
|
-
// Tracked, not discarded: if an assertion below fails, an untracked
|
|
71
|
-
// live service keeps the test runner alive and a red degrades into a
|
|
72
|
-
// hang -- the failure mode is then indistinguishable from a wedge.
|
|
73
|
-
respawned = await respawnUntilUp(shapeCtx, env);
|
|
74
|
-
}
|
|
75
67
|
|
|
76
68
|
// signal-sent ≠ dead: the OLD pid must be VERIFIED gone.
|
|
77
69
|
assert.ok(!processAlive(oldPid), `[${shape}] the old incarnation must be verified dead`);
|
|
@@ -91,9 +83,6 @@ async function runUpgradeShape(
|
|
|
91
83
|
assert.equal(state.stableVersion, "2.0.0", `[${shape}] stable slot must hold the new version`);
|
|
92
84
|
assert.equal(state.phase, "promoted", `[${shape}] journal must end at promoted`);
|
|
93
85
|
} finally {
|
|
94
|
-
if (respawned && respawned.child.pid && processAlive(respawned.child.pid)) {
|
|
95
|
-
respawned.child.kill("SIGKILL");
|
|
96
|
-
}
|
|
97
86
|
await killIncarnation(shapeCtx);
|
|
98
87
|
if (processAlive(seedPid)) seedChild.kill("SIGKILL");
|
|
99
88
|
await seed.stop();
|
|
@@ -102,7 +91,7 @@ async function runUpgradeShape(
|
|
|
102
91
|
}
|
|
103
92
|
|
|
104
93
|
// ---------------------------------------------------------------------------
|
|
105
|
-
// m3.service-rollback (
|
|
94
|
+
// m3.service-rollback (external driver)
|
|
106
95
|
// ---------------------------------------------------------------------------
|
|
107
96
|
|
|
108
97
|
export async function checkM3ServiceRollback(
|
|
@@ -135,19 +124,12 @@ async function runRollbackShape(
|
|
|
135
124
|
});
|
|
136
125
|
const { seedChild, seedPid } = await seedService(shapeCtx, shape, binPath, seed);
|
|
137
126
|
const env = serviceEnv(shapeCtx, shape, target.url);
|
|
138
|
-
let respawned: Awaited<ReturnType<typeof respawnUntilUp>> | null = null;
|
|
139
127
|
try {
|
|
140
128
|
const before = await readIncarnation(shapeCtx);
|
|
141
129
|
assert.ok(before, "a running incarnation must exist before the upgrade");
|
|
142
130
|
const oldPid = before!.pid;
|
|
143
131
|
|
|
144
132
|
const up = await runCommand(binPath, ["self", "upgrade"], { env, timeoutMs: 30000 });
|
|
145
|
-
if (shape === "respawn") {
|
|
146
|
-
// Tracked, not discarded: if an assertion below fails, an untracked
|
|
147
|
-
// live service keeps the test runner alive and a red degrades into a
|
|
148
|
-
// hang -- the failure mode is then indistinguishable from a wedge.
|
|
149
|
-
respawned = await respawnUntilUp(shapeCtx, env);
|
|
150
|
-
}
|
|
151
133
|
|
|
152
134
|
const state = await readState(env);
|
|
153
135
|
if (serveGoodVersion) {
|
|
@@ -168,8 +150,6 @@ async function runRollbackShape(
|
|
|
168
150
|
`[${shape}] upgrade to a bad version must exit non-zero (${up.stderr.trim()})`,
|
|
169
151
|
);
|
|
170
152
|
}
|
|
171
|
-
// (the respawn-shape driver exits 0 to request the successor; the
|
|
172
|
-
// outcome is decided by the successor's recovery below)
|
|
173
153
|
assert.equal(state.phase, "rolled-back", `[${shape}] journal must end at rolled-back`);
|
|
174
154
|
assert.equal(state.stableVersion, "1.0.0", `[${shape}] stable slot must hold the old version`);
|
|
175
155
|
assert.equal(state.experimentVersion, null, `[${shape}] experiment slot must be cleared`);
|
|
@@ -187,9 +167,6 @@ async function runRollbackShape(
|
|
|
187
167
|
);
|
|
188
168
|
assert.ok(!processAlive(oldPid), `[${shape}] the pre-upgrade incarnation must stay dead`);
|
|
189
169
|
} finally {
|
|
190
|
-
if (respawned && respawned.child.pid && processAlive(respawned.child.pid)) {
|
|
191
|
-
respawned.child.kill("SIGKILL");
|
|
192
|
-
}
|
|
193
170
|
await killIncarnation(shapeCtx);
|
|
194
171
|
if (processAlive(seedPid)) seedChild.kill("SIGKILL");
|
|
195
172
|
await seed.stop();
|
|
@@ -198,98 +175,42 @@ async function runRollbackShape(
|
|
|
198
175
|
}
|
|
199
176
|
|
|
200
177
|
// ---------------------------------------------------------------------------
|
|
201
|
-
// m3.stuck-driver-
|
|
178
|
+
// m3.stuck-driver-rollback-recovery
|
|
202
179
|
// ---------------------------------------------------------------------------
|
|
203
180
|
|
|
204
|
-
/**
|
|
205
|
-
* A wedged driver (host.stop() never returns) must not wedge the updater:
|
|
206
|
-
* the host-call budget times the driver out, the upgrade lock is released,
|
|
207
|
-
* the owner respawns the successor, and the successor's recovery decides by
|
|
208
|
-
* EVIDENCE (a live v2 with a fresh startId) — never by a "this restart was
|
|
209
|
-
* planned" flag. `skipRecovery` simulates the must-red mutation (the
|
|
210
|
-
* successor never recovers, so the transaction stays pending).
|
|
211
|
-
*/
|
|
181
|
+
/** A fresh external driver must undo interrupted work, never adopt a candidate. */
|
|
212
182
|
export async function checkM3StuckDriverEvidence(
|
|
213
183
|
ctx: ToothContext,
|
|
214
184
|
opts: { skipRecovery?: boolean } = {},
|
|
215
185
|
): Promise<void> {
|
|
216
|
-
const shape: HostShape = "
|
|
186
|
+
const shape: HostShape = "spawn";
|
|
217
187
|
const shapeCtx: ToothContext = { profile: ctx.profile, sandboxDir: path.join(ctx.sandboxDir, shape) };
|
|
218
188
|
const binPath = await buildServiceDaemon(shapeCtx);
|
|
219
|
-
const seed = await serveRelease(shapeCtx, {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
name: "seed",
|
|
223
|
-
source: PLAIN_DAEMON_SOURCE,
|
|
224
|
-
});
|
|
225
|
-
const target = await serveRelease(shapeCtx, {
|
|
226
|
-
version: "2.0.0",
|
|
227
|
-
behavior: "ok",
|
|
228
|
-
name: "target",
|
|
229
|
-
source: PLAIN_DAEMON_SOURCE,
|
|
230
|
-
});
|
|
231
|
-
const { seedChild, seedPid } = await seedService(shapeCtx, shape, binPath, seed);
|
|
232
|
-
const env = serviceEnv(shapeCtx, shape, target.url);
|
|
233
|
-
let oldPid: number | null = null;
|
|
189
|
+
const seed = await serveRelease(shapeCtx, { version: "1.0.0", behavior: "ok", name: "seed", source: PLAIN_DAEMON_SOURCE });
|
|
190
|
+
const target = await serveRelease(shapeCtx, { version: "2.0.0", behavior: "ok", name: "target", source: PLAIN_DAEMON_SOURCE });
|
|
191
|
+
let seedPid: number | undefined;
|
|
234
192
|
try {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// The driver is wedged at stop(): it must time out (host-call budget),
|
|
240
|
-
// release the lock, and exit — never wedge the tooth.
|
|
241
|
-
const stuckEnv = { ...env, K_STUCK_DRIVER: "1" };
|
|
193
|
+
const seeded = await seedService(shapeCtx, shape, binPath, seed);
|
|
194
|
+
seedPid = seeded.seedPid;
|
|
195
|
+
const env = serviceEnv(shapeCtx, shape, target.url);
|
|
242
196
|
const up = await runCommand(binPath, ["self", "upgrade"], {
|
|
243
|
-
env:
|
|
244
|
-
timeoutMs: 30000,
|
|
197
|
+
env: { ...env, K_STUCK_DRIVER: "1" }, timeoutMs: 30000,
|
|
245
198
|
});
|
|
246
|
-
assert.notEqual(up.code, 0, "the wedged driver must fail
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const respawnEnv = { ...env, ...(opts.skipRecovery === true ? { K_SKIP_RECOVERY: "1" } : {}) };
|
|
251
|
-
const { info: successor, child } = await respawnUntilUp(shapeCtx, respawnEnv);
|
|
252
|
-
try {
|
|
253
|
-
if (opts.skipRecovery) {
|
|
254
|
-
// mutation: the successor never recovers — the transaction stays
|
|
255
|
-
// pending at handing-over, so the promoted assertion goes RED
|
|
256
|
-
const st = await readState(env);
|
|
257
|
-
assert.equal(st.phase, "promoted", "the successor's recovery must finish the transaction (skipRecovery => RED)");
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// ② the successor is up (respawned from the experiment bytes)
|
|
262
|
-
assert.equal(successor.version, "2.0.0", "the successor must run the new version");
|
|
263
|
-
assert.ok(processAlive(successor.pid), "the successor must be alive");
|
|
264
|
-
assert.notEqual(successor.startId, before!.startId, "the successor is a fresh incarnation");
|
|
265
|
-
|
|
266
|
-
// ③ the recovery's judgment matched the evidence (v2 + fresh startId):
|
|
267
|
-
// the transaction reached promoted, so the lock WAS released (the
|
|
268
|
-
// recovery acquired it) and the evidence said handover succeeded.
|
|
269
|
-
const state = await readState(env);
|
|
270
|
-
assert.equal(state.phase, "promoted", "recovery must finish at promoted (evidence, not a flag)");
|
|
271
|
-
assert.equal(state.stableVersion, "2.0.0", "stable must hold the new version");
|
|
272
|
-
} finally {
|
|
273
|
-
if (processAlive(successor.pid)) {
|
|
274
|
-
try {
|
|
275
|
-
process.kill(successor.pid, "SIGKILL");
|
|
276
|
-
} catch {
|
|
277
|
-
// already gone
|
|
278
|
-
}
|
|
279
|
-
await waitDead(successor.pid, 5000);
|
|
280
|
-
}
|
|
281
|
-
void child;
|
|
199
|
+
assert.notEqual(up.code, 0, "the wedged driver must fail");
|
|
200
|
+
if (!opts.skipRecovery) {
|
|
201
|
+
const recovery = await runCommand(binPath, ["self", "recover"], { env, timeoutMs: 30000 });
|
|
202
|
+
assert.equal(recovery.code, 0, `external recovery must complete: ${recovery.stderr}`);
|
|
282
203
|
}
|
|
204
|
+
const state = await readState(env);
|
|
205
|
+
assert.equal(state.phase, "rolled-back", "external recovery must finish the transaction by rollback");
|
|
206
|
+
assert.equal(state.stableVersion, "1.0.0");
|
|
207
|
+
assert.equal(state.experimentVersion, null);
|
|
208
|
+
const running = await readIncarnation(shapeCtx);
|
|
209
|
+
assert.ok(running && processAlive(running.pid), "restored stable must be alive");
|
|
210
|
+
assert.equal(running.version, "1.0.0");
|
|
283
211
|
} finally {
|
|
284
212
|
await killIncarnation(shapeCtx);
|
|
285
|
-
if (
|
|
286
|
-
try {
|
|
287
|
-
process.kill(oldPid, "SIGKILL"); // the wedged stop never killed it
|
|
288
|
-
} catch {
|
|
289
|
-
// already gone
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (processAlive(seedPid)) seedChild.kill("SIGKILL");
|
|
213
|
+
if (seedPid && processAlive(seedPid)) { process.kill(seedPid, "SIGKILL"); await waitDead(seedPid, 5000); }
|
|
293
214
|
await seed.stop();
|
|
294
215
|
await target.stop();
|
|
295
216
|
}
|
|
@@ -9,25 +9,12 @@ import { FakeServer } from "../fake-server/server.ts";
|
|
|
9
9
|
import { ArtifactFactory } from "../artifact-factory/factory.ts";
|
|
10
10
|
import { processAlive } from "../fake-host/daemon.ts";
|
|
11
11
|
import { commandForArtifact, runCommand } from "../artifact-factory/run.ts";
|
|
12
|
-
import { PLAIN_DAEMON_SOURCE } from "
|
|
13
|
-
import {
|
|
12
|
+
import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
|
|
13
|
+
import { runnerFactoryUrl, readState } from "./m1.ts";
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* host shapes:
|
|
19
|
-
* - spawn: self-starting — the driver spawns the successor and finishes
|
|
20
|
-
* the transaction itself.
|
|
21
|
-
* - respawn: computer's shape — the driver DIES mid-handover (it cannot
|
|
22
|
-
* start itself; the owner respawns it from the new bytes), and
|
|
23
|
-
* the SUCCESSOR finishes the transaction via recovery, proving
|
|
24
|
-
* the handover by a fresh startId (a flag could be set by the
|
|
25
|
-
* crash path; a different incarnation is something that
|
|
26
|
-
* happened).
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
export type HostShape = "spawn" | "respawn";
|
|
30
|
-
export const HOST_SHAPES: HostShape[] = ["spawn", "respawn"];
|
|
15
|
+
/** External service driver; the resident never owns recovery. */
|
|
16
|
+
export type HostShape = "spawn";
|
|
17
|
+
export const HOST_SHAPES: HostShape[] = ["spawn"];
|
|
31
18
|
|
|
32
19
|
const INCARNATION_FILE = "incarnation.json";
|
|
33
20
|
|
|
@@ -62,20 +49,19 @@ function stateDir(ctx: ToothContext): string {
|
|
|
62
49
|
|
|
63
50
|
export function serviceEnv(
|
|
64
51
|
ctx: ToothContext,
|
|
65
|
-
|
|
52
|
+
_shape: HostShape,
|
|
66
53
|
baseUrl: string,
|
|
67
54
|
): Record<string, string> {
|
|
68
55
|
return {
|
|
69
56
|
K_RELEASE_BASE: baseUrl,
|
|
70
57
|
K_STATE_DIR: stateDir(ctx),
|
|
71
|
-
|
|
72
|
-
K_CORE_UPGRADER: coreUpgraderUrl(),
|
|
58
|
+
K_CORE_UPGRADER: runnerFactoryUrl(),
|
|
73
59
|
// Every process this tooth spawns — and, via inheritance, every process
|
|
74
60
|
// the driver spawns — carries the sandbox marker, so the sandbox
|
|
75
61
|
// teardown's verifyProcessTreeDead is the backstop for any leak: a "red"
|
|
76
62
|
// can never degrade into a hang.
|
|
77
63
|
// Resolved back to the SANDBOX id: ctx here is a per-shape NESTED context,
|
|
78
|
-
// and basename() of that is "spawn"
|
|
64
|
+
// and basename() of that is "spawn" -- a marker no teardown scan
|
|
79
65
|
// matches, which turns the backstop into a scan that always finds zero.
|
|
80
66
|
K_SANDBOX_MARKER: sandboxMarkerFor(ctx.sandboxDir),
|
|
81
67
|
};
|
|
@@ -178,42 +164,7 @@ export async function killIncarnation(ctx: ToothContext): Promise<void> {
|
|
|
178
164
|
}
|
|
179
165
|
}
|
|
180
166
|
|
|
181
|
-
/**
|
|
182
|
-
* The OWNER (respawn shape): the driver exits mid-handover; the owner
|
|
183
|
-
* respawns the service from the new bytes (experiment first), falling back
|
|
184
|
-
* to stable when the new version cannot start, and respawning again when a
|
|
185
|
-
* recovery asks for a successor by exiting. A bounded loop: the service
|
|
186
|
-
* eventually stays up or the owner gives up.
|
|
187
|
-
*/
|
|
188
|
-
export async function respawnUntilUp(
|
|
189
|
-
ctx: ToothContext,
|
|
190
|
-
env: Record<string, string>,
|
|
191
|
-
): Promise<{ info: Incarnation; child: ChildProcess }> {
|
|
192
|
-
const dir = stateDir(ctx);
|
|
193
|
-
for (let attempt = 0; attempt < 6; attempt++) {
|
|
194
|
-
const slot: "experiment" | "stable" = attempt === 0 ? "experiment" : "stable";
|
|
195
|
-
const artifact = path.join(dir, "slots", slot, "artifact.bin");
|
|
196
|
-
try {
|
|
197
|
-
await fs.access(artifact);
|
|
198
|
-
} catch {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
const child = spawn(process.execPath, [artifact], {
|
|
202
|
-
env: { ...process.env, ...env },
|
|
203
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
204
|
-
});
|
|
205
|
-
const info = await tryReady(child, 2000);
|
|
206
|
-
if (info !== null) return { info, child };
|
|
207
|
-
if (child.pid && processAlive(child.pid)) child.kill("SIGKILL");
|
|
208
|
-
}
|
|
209
|
-
throw new Error("owner: the service never came up");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Seed the service world: spawn the v1 service (registers), then a real
|
|
214
|
-
* driver upgrade to 1.0.0 lands stable v1 with a running incarnation
|
|
215
|
-
* (driver-completed on spawn, successor-completed on respawn).
|
|
216
|
-
*/
|
|
167
|
+
/** Seed stable v1 with an external driver and a live service. */
|
|
217
168
|
export async function seedService(
|
|
218
169
|
ctx: ToothContext,
|
|
219
170
|
shape: HostShape,
|
|
@@ -228,9 +179,6 @@ export async function seedService(
|
|
|
228
179
|
|
|
229
180
|
const up = await runCommand(binPath, ["self", "upgrade"], { env, timeoutMs: 30000 });
|
|
230
181
|
assert.equal(up.code, 0, `seed upgrade must exit 0 (${up.stderr.trim()})`);
|
|
231
|
-
if (shape === "respawn") {
|
|
232
|
-
await respawnUntilUp(ctx, env); // the driver died; the owner brings the successor up
|
|
233
|
-
}
|
|
234
182
|
const seeded = await readState(env);
|
|
235
183
|
assert.equal(seeded.stableVersion, "1.0.0", "seed upgrade must land stable 1.0.0");
|
|
236
184
|
const running = await readIncarnation(ctx);
|
|
@@ -16,8 +16,8 @@ import { promises as fs } from "node:fs";
|
|
|
16
16
|
import { type ToothContext } from "../teeth/registry.ts";
|
|
17
17
|
import { FakeServer } from "../fake-server/server.ts";
|
|
18
18
|
import { runCommand } from "../artifact-factory/run.ts";
|
|
19
|
-
import { buildSwapTool,
|
|
20
|
-
import { CLI_TOOL_SOURCE } from "
|
|
19
|
+
import { buildSwapTool, runnerFactoryUrl, readState, serveRelease } from "./m1.ts";
|
|
20
|
+
import { CLI_TOOL_SOURCE } from "../fixtures/cliToolSource.ts";
|
|
21
21
|
import { ArtifactFactory } from "../artifact-factory/factory.ts";
|
|
22
22
|
import { currentPlatformKey } from "../../../core/src/artifact/staticManifestSource.ts";
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ function swapEnv(ctx: ToothContext, baseUrl: string, servers: FakeServer[], extr
|
|
|
25
25
|
return {
|
|
26
26
|
K_RELEASE_BASE: baseUrl,
|
|
27
27
|
K_STATE_DIR: path.join(ctx.sandboxDir, "state"),
|
|
28
|
-
K_CORE_UPGRADER:
|
|
28
|
+
K_CORE_UPGRADER: runnerFactoryUrl(),
|
|
29
29
|
...extra,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* (test-plan M5 rows: 点名面读回一致;面在 allowlist 才可作证;未注册面被引用
|
|
4
4
|
* ⇒ 拒; fail-closed 退役序; 禁投影).
|
|
5
5
|
*
|
|
6
|
-
* These drive
|
|
6
|
+
* These drive createRunner in-process (the library plane — the converge
|
|
7
7
|
* machinery is exactly the kind of internal tooth the black-box plane
|
|
8
8
|
* cannot reach).
|
|
9
9
|
*/
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { type ToothContext } from "../teeth/registry.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { createRunner, type RunnerOptions } from "../../../core/src/createRunner.ts";
|
|
14
14
|
import { staticManifestSource } from "../../../core/src/artifact/staticManifestSource.ts";
|
|
15
15
|
import {
|
|
16
16
|
buildSurfaceAllowlist,
|
|
@@ -66,8 +66,8 @@ function makeUpgrader(
|
|
|
66
66
|
ctx: ToothContext,
|
|
67
67
|
server: FakeServer,
|
|
68
68
|
surfaces: ReadbackSurface[],
|
|
69
|
-
): ReturnType<typeof
|
|
70
|
-
const opts:
|
|
69
|
+
): ReturnType<typeof createRunner> {
|
|
70
|
+
const opts: RunnerOptions = {
|
|
71
71
|
host: simpleHost(),
|
|
72
72
|
source: staticManifestSource({ baseUrl: server.url }),
|
|
73
73
|
policy: "auto",
|
|
@@ -75,7 +75,7 @@ function makeUpgrader(
|
|
|
75
75
|
stateDir: stateDir(ctx),
|
|
76
76
|
lifecycleSurfaces: surfaces,
|
|
77
77
|
};
|
|
78
|
-
return
|
|
78
|
+
return createRunner(opts);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// ---------------------------------------------------------------------------
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* genesis; every reconcile that reaches the transaction records WHO drove
|
|
6
6
|
* it, write-ahead).
|
|
7
7
|
*
|
|
8
|
-
* These drive
|
|
8
|
+
* These drive createRunner in-process (the library plane — the journal
|
|
9
9
|
* is exactly the kind of internal tooth the black-box plane cannot reach).
|
|
10
10
|
* Mutation journals/summaries live in m6Mutations.ts (line budget).
|
|
11
11
|
*/
|
|
@@ -13,7 +13,7 @@ import assert from "node:assert/strict";
|
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import { promises as fs } from "node:fs";
|
|
15
15
|
import { type ToothContext } from "../teeth/registry.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { createRunner, type RunnerOptions } from "../../../core/src/createRunner.ts";
|
|
17
17
|
import type { ReleaseSource } from "../../../core/src/artifact/source.ts";
|
|
18
18
|
import { staticManifestSource } from "../../../core/src/artifact/staticManifestSource.ts";
|
|
19
19
|
import {
|
|
@@ -75,16 +75,16 @@ export async function makeUpgrader(
|
|
|
75
75
|
server: FakeServer,
|
|
76
76
|
journal: ProvenanceJournal | null,
|
|
77
77
|
opts: MakeUpgraderOpts = {},
|
|
78
|
-
): Promise<ReturnType<typeof
|
|
79
|
-
const base: Omit<
|
|
78
|
+
): Promise<ReturnType<typeof createRunner>> {
|
|
79
|
+
const base: Omit<RunnerOptions, "provenance"> = {
|
|
80
80
|
host: opts.host ?? hostReporting("2.0.0"),
|
|
81
81
|
source: opts.source ?? staticManifestSource({ baseUrl: server.url }),
|
|
82
82
|
policy: opts.policy ?? "auto",
|
|
83
|
-
notificationSink: (opts.notificationSink ?? (async () => {})) as
|
|
83
|
+
notificationSink: (opts.notificationSink ?? (async () => {})) as RunnerOptions["notificationSink"],
|
|
84
84
|
stateDir: stateDir(ctx),
|
|
85
85
|
...(opts.installOwnership === undefined ? {} : { installOwnership: opts.installOwnership }),
|
|
86
86
|
};
|
|
87
|
-
return
|
|
87
|
+
return createRunner(journal === null ? base : { ...base, provenance: journal });
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* an unreadable report is its own state, never "never observed"; an
|
|
6
6
|
* observed pass survives a restart).
|
|
7
7
|
*
|
|
8
|
-
* These drive
|
|
8
|
+
* These drive createRunner in-process: status() is the L5 read-back
|
|
9
9
|
* surface, and the teeth pin that it is a READ-BACK, not an invention.
|
|
10
10
|
*/
|
|
11
11
|
import assert from "node:assert/strict";
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
* Examples acceptance checks — the run bodies of the demo teeth
|
|
3
3
|
* (registered in teeth/examples.ts). Each throws on violation.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* (examples/README.md: "if a profile has no green example, the claim does
|
|
7
|
-
* not exist"):
|
|
5
|
+
* These fixtures exercise internal mechanisms, not application integration:
|
|
8
6
|
* - swap-tool: black-box upgrade loop (L0/L0.5/L1') — version command,
|
|
9
7
|
* self-upgrade swaps bytes, next run reports the served version;
|
|
10
8
|
* - service-daemon: process-reality L2/L3 — real spawn, startId-bound
|
|
@@ -31,12 +29,12 @@ import {
|
|
|
31
29
|
checkProbeBindsCurrentIncarnation,
|
|
32
30
|
} from "../fake-host/checks.ts";
|
|
33
31
|
import type { HostDriver } from "../fake-host/inproc.ts";
|
|
34
|
-
import { CLI_TOOL_SOURCE } from "
|
|
35
|
-
import { PLAIN_DAEMON_SOURCE } from "
|
|
32
|
+
import { CLI_TOOL_SOURCE } from "../fixtures/cliToolSource.ts";
|
|
33
|
+
import { PLAIN_DAEMON_SOURCE } from "../fixtures/serviceSource.ts";
|
|
36
34
|
|
|
37
35
|
const RELEASE_BASE_ENV = "K_RELEASE_BASE";
|
|
38
36
|
|
|
39
|
-
/** The swap-tool's explicit target declaration
|
|
37
|
+
/** The swap-tool fixture's explicit black-box target declaration. */
|
|
40
38
|
export const CLI_TOOL_TARGET_TS = `export default { version: ["--version"], selfUpgrade: ["self", "upgrade"] };
|
|
41
39
|
`;
|
|
42
40
|
|
|
@@ -71,9 +69,9 @@ async function fileSha256(p: string): Promise<string> {
|
|
|
71
69
|
return sha256Hex(new Uint8Array(await fs.readFile(p)));
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
/** The swap-tool demo's @botiverse/k-carrier wiring (
|
|
75
|
-
function
|
|
76
|
-
return pathToFileURL(path.join(import.meta.dirname, "../../../core/src/
|
|
72
|
+
/** The swap-tool demo's @botiverse/k-carrier wiring (createRunner module URL). */
|
|
73
|
+
function runnerFactoryUrl(): string {
|
|
74
|
+
return pathToFileURL(path.join(import.meta.dirname, "../../../core/src/createRunner.ts")).href;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
// ---------------------------------------------------------------------------
|
|
@@ -111,7 +109,7 @@ export async function checkCliToolBlackbox(
|
|
|
111
109
|
const up = await runCommand(binPath, ["self", "upgrade"], {
|
|
112
110
|
env: {
|
|
113
111
|
[RELEASE_BASE_ENV]: server.url,
|
|
114
|
-
K_CORE_UPGRADER:
|
|
112
|
+
K_CORE_UPGRADER: runnerFactoryUrl(),
|
|
115
113
|
},
|
|
116
114
|
});
|
|
117
115
|
assert.equal(up.code, 0, `self upgrade must exit 0 (${up.stderr.trim()})`);
|
|
@@ -237,9 +235,8 @@ export async function checkPlainDaemonContract(
|
|
|
237
235
|
const up = await runCommand(binPath, ["self", "upgrade"], {
|
|
238
236
|
env: {
|
|
239
237
|
[RELEASE_BASE_ENV]: server.url,
|
|
240
|
-
K_CORE_UPGRADER:
|
|
238
|
+
K_CORE_UPGRADER: runnerFactoryUrl(),
|
|
241
239
|
K_STATE_DIR: stateDir,
|
|
242
|
-
K_HOST_SHAPE: "spawn",
|
|
243
240
|
},
|
|
244
241
|
timeoutMs: 30000,
|
|
245
242
|
});
|
|
@@ -275,7 +272,7 @@ export async function checkManagedHostAdapter(
|
|
|
275
272
|
const makeHost = async (): Promise<HostDriver> =>
|
|
276
273
|
opts.hostOverride
|
|
277
274
|
? opts.hostOverride()
|
|
278
|
-
: (await import("
|
|
275
|
+
: (await import("../fixtures/managedHost.ts")).createManagedHost(
|
|
279
276
|
path.join(ctx.sandboxDir, "host"),
|
|
280
277
|
);
|
|
281
278
|
// the same contract subset k-harness --adapter runs (§1.7)
|