@botiverse/k-carrier 0.1.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/LICENSE +202 -0
- package/NOTICE +24 -0
- package/README.md +62 -0
- package/core/src/artifact/README.md +1 -0
- package/core/src/artifact/collectStream.ts +49 -0
- package/core/src/artifact/download.ts +300 -0
- package/core/src/artifact/errors.ts +24 -0
- package/core/src/artifact/partialPath.ts +18 -0
- package/core/src/artifact/source.ts +59 -0
- package/core/src/artifact/staticManifestSource.ts +185 -0
- package/core/src/artifact/swap.ts +21 -0
- package/core/src/clock.ts +22 -0
- package/core/src/converge/lifecycle.ts +114 -0
- package/core/src/converge/predicates.ts +57 -0
- package/core/src/converge/report.ts +38 -0
- package/core/src/createUpgrader.ts +300 -0
- package/core/src/drive/README.md +1 -0
- package/core/src/index.ts +17 -0
- package/core/src/invariants.ts +243 -0
- package/core/src/lifecycle/hostAdapter.ts +77 -0
- package/core/src/liveness.ts +80 -0
- package/core/src/platform/README.md +1 -0
- package/core/src/platform/index.ts +11 -0
- package/core/src/platform/ops.ts +53 -0
- package/core/src/platform/posix.ts +99 -0
- package/core/src/platform/windows.ts +73 -0
- package/core/src/policy/README.md +1 -0
- package/core/src/progress.ts +54 -0
- package/core/src/provenance/journal.ts +216 -0
- package/core/src/status/report.ts +76 -0
- package/core/src/status/reportStore.ts +67 -0
- package/core/src/txn/effects.ts +40 -0
- package/core/src/txn/engine.ts +275 -0
- package/core/src/txn/fileEffects.ts +129 -0
- package/core/src/txn/hostCallBudget.ts +28 -0
- package/core/src/txn/lock.ts +124 -0
- package/core/src/txn/state.ts +77 -0
- package/core/src/txn/transitions.ts +49 -0
- package/core/src/upgrade/outcome.ts +53 -0
- package/core/src/upgrade/retire.ts +44 -0
- package/core/src/upgrader.ts +124 -0
- package/docs/design-v1.md +246 -0
- package/docs/harness-design.md +170 -0
- package/docs/integration.md +326 -0
- package/docs/prior-art.md +150 -0
- package/docs/test-plan.md +100 -0
- package/harness/README.md +20 -0
- package/harness/src/adapter/probeChecks.ts +93 -0
- package/harness/src/adapter/releaseKnob.ts +65 -0
- package/harness/src/adapter/serviceChecks.ts +260 -0
- package/harness/src/artifact/checks.ts +232 -0
- package/harness/src/artifact/downloadHoles.ts +300 -0
- package/harness/src/artifact/downloadHolesMutations.ts +170 -0
- package/harness/src/artifact/m1.ts +204 -0
- package/harness/src/artifact/m1Resume.ts +141 -0
- package/harness/src/artifact/m3.ts +296 -0
- package/harness/src/artifact/m3Hosts.ts +241 -0
- package/harness/src/artifact/m4.ts +201 -0
- package/harness/src/artifact/m5.ts +239 -0
- package/harness/src/artifact/m6.ts +274 -0
- package/harness/src/artifact/m6Drive.ts +296 -0
- package/harness/src/artifact/m6DriveMutations.ts +70 -0
- package/harness/src/artifact/m6Mutations.ts +136 -0
- package/harness/src/artifact/m6Status.ts +224 -0
- package/harness/src/artifact-factory/checks.ts +68 -0
- package/harness/src/artifact-factory/demo.ts +70 -0
- package/harness/src/artifact-factory/factory.ts +134 -0
- package/harness/src/artifact-factory/run.ts +107 -0
- package/harness/src/blackbox.ts +284 -0
- package/harness/src/cli.ts +281 -0
- package/harness/src/crash/enumerate.ts +66 -0
- package/harness/src/crash/runner.ts +162 -0
- package/harness/src/examples/checks.ts +286 -0
- package/harness/src/fake-host/checks.ts +188 -0
- package/harness/src/fake-host/daemon-entry.ts +39 -0
- package/harness/src/fake-host/daemon.ts +159 -0
- package/harness/src/fake-host/inproc.ts +256 -0
- package/harness/src/fake-server/manifest.ts +68 -0
- package/harness/src/fake-server/server.ts +205 -0
- package/harness/src/fake-server/store.ts +208 -0
- package/harness/src/fixtures/minimal-adapter.ts +108 -0
- package/harness/src/fixtures/service-adapter.ts +187 -0
- package/harness/src/index.ts +11 -0
- package/harness/src/receipt.ts +83 -0
- package/harness/src/runner.ts +268 -0
- package/harness/src/scenario/processScan.ts +55 -0
- package/harness/src/scenario/sandbox.ts +259 -0
- package/harness/src/scenario/virtualClock.ts +56 -0
- package/harness/src/selfverify/selfVerify.ts +136 -0
- package/harness/src/sim/checks.ts +46 -0
- package/harness/src/sim/corpus.ts +36 -0
- package/harness/src/sim/effectRuntime.ts +113 -0
- package/harness/src/sim/error.ts +14 -0
- package/harness/src/sim/prng.ts +29 -0
- package/harness/src/sim/record.ts +76 -0
- package/harness/src/sim/run.ts +148 -0
- package/harness/src/sim/scheduler.ts +72 -0
- package/harness/src/sim/world.ts +284 -0
- package/harness/src/target.ts +86 -0
- package/harness/src/targetCheck.ts +47 -0
- package/harness/src/teeth/artifact.ts +215 -0
- package/harness/src/teeth/artifactDownload.ts +136 -0
- package/harness/src/teeth/artifactFactory.ts +41 -0
- package/harness/src/teeth/artifactM5.ts +68 -0
- package/harness/src/teeth/artifactM6.ts +264 -0
- package/harness/src/teeth/blackbox.ts +22 -0
- package/harness/src/teeth/checks.ts +278 -0
- package/harness/src/teeth/examples.ts +57 -0
- package/harness/src/teeth/fakeHost.ts +120 -0
- package/harness/src/teeth/index.ts +16 -0
- package/harness/src/teeth/m0.ts +135 -0
- package/harness/src/teeth/registry.ts +267 -0
- package/harness/src/teeth/selfCheck.ts +45 -0
- package/harness/src/teeth/sim.ts +61 -0
- package/package.json +43 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The missing-target fail-closed check (registered as
|
|
3
|
+
* blackbox.missing-target-fails): a binary with NO k.target.ts must be a
|
|
4
|
+
* typed BLACKBOX_TARGET_REQUIRED FAIL — the harness never guesses
|
|
5
|
+
* commands. `withTarget` simulates the must-red mutation (a target file
|
|
6
|
+
* exists, so the failure no longer happens) for known-red driving.
|
|
7
|
+
*/
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { promises as fs } from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { type ToothContext } from "./teeth/registry.ts";
|
|
12
|
+
import { ArtifactFactory } from "./artifact-factory/factory.ts";
|
|
13
|
+
import { FakeServer } from "./fake-server/server.ts";
|
|
14
|
+
import { runBinMode } from "./blackbox.ts";
|
|
15
|
+
import { TARGET_FILE } from "./target.ts";
|
|
16
|
+
|
|
17
|
+
export async function checkMissingTargetFails(
|
|
18
|
+
ctx: ToothContext,
|
|
19
|
+
opts: { withTarget?: boolean } = {},
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const factory = new ArtifactFactory({ cacheDir: path.join(ctx.sandboxDir, "cache") });
|
|
22
|
+
const server = new FakeServer({ storeDir: path.join(ctx.sandboxDir, "store") });
|
|
23
|
+
await server.start();
|
|
24
|
+
try {
|
|
25
|
+
const rel = await factory.makeRelease({ version: "1.0.0", behavior: "ok", store: server.store });
|
|
26
|
+
const appDir = path.join(ctx.sandboxDir, "app");
|
|
27
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
28
|
+
const binPath = path.join(appDir, "mytool");
|
|
29
|
+
await fs.writeFile(binPath, rel.artifactBytes, { mode: 0o755 });
|
|
30
|
+
if (opts.withTarget) {
|
|
31
|
+
await fs.writeFile(
|
|
32
|
+
path.join(appDir, TARGET_FILE),
|
|
33
|
+
`export default { version: ["--version"], selfUpgrade: ["self", "upgrade"] };`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const receipt = await runBinMode({ binPath, profile: "swap" });
|
|
37
|
+
const decl = receipt.checks.find((c) => c.id === "contract.target-declarations");
|
|
38
|
+
assert.ok(
|
|
39
|
+
decl?.status === "fail",
|
|
40
|
+
"a binary without k.target.ts must fail with an actionable typed error (no guessing)",
|
|
41
|
+
);
|
|
42
|
+
assert.equal(receipt.result, "fail");
|
|
43
|
+
assert.match(decl?.error ?? "", /BLACKBOX_TARGET_REQUIRED/);
|
|
44
|
+
} finally {
|
|
45
|
+
await server.stop();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M1 artifact teeth (test-plan M1 L0 rows; the package spec):
|
|
3
|
+
* tampered artifact => refuse install; kill mid-swap => old bytes intact;
|
|
4
|
+
* source refuses rather than guesses. Registration site only — check bodies
|
|
5
|
+
* live in harness/src/artifact/checks.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { registerTooth } from "./registry.ts";
|
|
8
|
+
import {
|
|
9
|
+
checkTamperedArtifactRefused,
|
|
10
|
+
checkKillMidSwapPreservesOld,
|
|
11
|
+
checkSourceFailsClosed,
|
|
12
|
+
} from "../artifact/checks.ts";
|
|
13
|
+
import {
|
|
14
|
+
checkSwapToolUpgradeLoop,
|
|
15
|
+
checkSwapToolRollback,
|
|
16
|
+
} from "../artifact/m1.ts";
|
|
17
|
+
import {
|
|
18
|
+
checkM3ServiceUpgrade,
|
|
19
|
+
checkM3ServiceRollback,
|
|
20
|
+
checkM3StuckDriverEvidence,
|
|
21
|
+
} from "../artifact/m3.ts";
|
|
22
|
+
import { checkDownloadResumesAfterKill } from "../artifact/m1Resume.ts";
|
|
23
|
+
import {
|
|
24
|
+
checkM4ConfirmNoConsentZeroSideEffects,
|
|
25
|
+
checkM4ConsentBindsVersion,
|
|
26
|
+
checkM4NotifyOnlyReportsInstallableVersion,
|
|
27
|
+
} from "../artifact/m4.ts";
|
|
28
|
+
|
|
29
|
+
registerTooth({
|
|
30
|
+
id: "artifact.tamper-refuses-install",
|
|
31
|
+
profiles: ["swap", "service"],
|
|
32
|
+
layers: ["L0"],
|
|
33
|
+
kind: { kind: "invariant" },
|
|
34
|
+
mustRed: [
|
|
35
|
+
{
|
|
36
|
+
mutate: "downloadVerified skips the sha256 verification",
|
|
37
|
+
caughtOnlyBy: {
|
|
38
|
+
alsoCaughtBy: "core/src/artifact download unit tests (SHA256_MISMATCH)",
|
|
39
|
+
whyStillNeeded:
|
|
40
|
+
"the tooth runs the full black-box plane — fake-server tamper API + factory artifact + real HTTP — which the unit test does not",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
run: checkTamperedArtifactRefused,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
registerTooth({
|
|
48
|
+
id: "artifact.atomic-swap-crash-safe",
|
|
49
|
+
profiles: ["swap", "service"],
|
|
50
|
+
layers: ["L0"],
|
|
51
|
+
kind: { kind: "invariant" },
|
|
52
|
+
mustRed: [
|
|
53
|
+
{
|
|
54
|
+
mutate: "atomicWriteFile writes the target in place (no tmp+rename; half-writes visible)",
|
|
55
|
+
caughtOnlyBy: "this", // only this tooth kills a real process mid-swap
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
run: checkKillMidSwapPreservesOld,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
registerTooth({
|
|
62
|
+
id: "artifact.source-fails-closed",
|
|
63
|
+
profiles: ["swap", "service"],
|
|
64
|
+
layers: ["L0"],
|
|
65
|
+
kind: { kind: "invariant" },
|
|
66
|
+
mustRed: [
|
|
67
|
+
{
|
|
68
|
+
mutate: "the release source guesses a target instead of refusing (unknown platform / unservable version)",
|
|
69
|
+
caughtOnlyBy: {
|
|
70
|
+
alsoCaughtBy: "core/src/artifact source unit tests",
|
|
71
|
+
whyStillNeeded:
|
|
72
|
+
"the tooth pins refusal at the real source boundary (platform + named-version), not just one helper",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
run: checkSourceFailsClosed,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
registerTooth({
|
|
80
|
+
id: "m1.swap-tool-upgrade",
|
|
81
|
+
profiles: ["swap"],
|
|
82
|
+
layers: ["L0", "L1p"],
|
|
83
|
+
kind: { kind: "invariant" },
|
|
84
|
+
mustRed: [
|
|
85
|
+
{
|
|
86
|
+
mutate: "the upgrade never reaches the stable slot (next run still reports the old version)",
|
|
87
|
+
caughtOnlyBy: "this", // only this tooth runs the full black-box upgrade through the demo
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
run: checkSwapToolUpgradeLoop,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
registerTooth({
|
|
94
|
+
id: "m1.swap-tool-rollback",
|
|
95
|
+
profiles: ["swap"],
|
|
96
|
+
layers: ["L0", "L1p"],
|
|
97
|
+
kind: { kind: "invariant" },
|
|
98
|
+
mustRed: [
|
|
99
|
+
{
|
|
100
|
+
mutate: "a failing experiment is promoted instead of rolled back",
|
|
101
|
+
caughtOnlyBy: {
|
|
102
|
+
alsoCaughtBy: "core txn engine unit tests (rollback symmetry)",
|
|
103
|
+
whyStillNeeded:
|
|
104
|
+
"the engine tests use in-memory effects; this tooth drives the real binary + real slots through the demo's own upgrade command",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
run: checkSwapToolRollback,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
registerTooth({
|
|
116
|
+
id: "m3.service-upgrade",
|
|
117
|
+
profiles: ["service"],
|
|
118
|
+
layers: ["L0", "L1", "L2", "L3"],
|
|
119
|
+
kind: { kind: "invariant" },
|
|
120
|
+
mustRed: [
|
|
121
|
+
{
|
|
122
|
+
mutate: "the new service is the SAME process as the old (no fresh incarnation; startId reused)",
|
|
123
|
+
caughtOnlyBy: "this", // only this tooth checks the running process's fresh startId end to end
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
mutate: "stop(slot) sends SIGKILL but never verifies the process is gone",
|
|
127
|
+
caughtOnlyBy: "this", // signal-sent != dead; only this tooth asserts the old pid is OS-gone
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
run: checkM3ServiceUpgrade,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
registerTooth({
|
|
134
|
+
id: "m3.service-rollback",
|
|
135
|
+
profiles: ["service"],
|
|
136
|
+
layers: ["L0", "L1", "L2", "L3"],
|
|
137
|
+
kind: { kind: "invariant" },
|
|
138
|
+
mustRed: [
|
|
139
|
+
{
|
|
140
|
+
mutate: "rollback reverts the slots but does not pull the old version back up",
|
|
141
|
+
caughtOnlyBy: "this", // only this tooth asserts the old version is ACTUALLY running after rollback
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
run: checkM3ServiceRollback,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
registerTooth({
|
|
148
|
+
id: "m3.stuck-driver-evidence-recovery",
|
|
149
|
+
profiles: ["service"],
|
|
150
|
+
layers: ["L0", "L1", "L2", "L3"],
|
|
151
|
+
kind: { kind: "invariant" },
|
|
152
|
+
mustRed: [
|
|
153
|
+
{
|
|
154
|
+
mutate: "the successor decides the handover by a flag, not by evidence (or never recovers at all)",
|
|
155
|
+
caughtOnlyBy: "this", // only this tooth wedges the driver and demands evidence-based recovery
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
run: checkM3StuckDriverEvidence,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
registerTooth({
|
|
162
|
+
id: "m1.download-resumes-after-kill",
|
|
163
|
+
profiles: ["swap", "service"],
|
|
164
|
+
layers: ["L0"],
|
|
165
|
+
kind: { kind: "invariant" },
|
|
166
|
+
mustRed: [
|
|
167
|
+
{
|
|
168
|
+
mutate: "an interrupted download restarts from zero (no partial, no Range resume)",
|
|
169
|
+
caughtOnlyBy: "this", // only this tooth kills a real downloader and demands a Range resume
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
run: checkDownloadResumesAfterKill,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
registerTooth({
|
|
176
|
+
id: "m4.confirm-no-consent-zero-side-effects",
|
|
177
|
+
profiles: ["service"],
|
|
178
|
+
layers: ["L0", "L1", "L4"],
|
|
179
|
+
kind: { kind: "invariant" },
|
|
180
|
+
mustRed: [
|
|
181
|
+
{
|
|
182
|
+
mutate: "the confirm gate moves after staging (unapproved bytes reach the disk)",
|
|
183
|
+
caughtOnlyBy: "this", // only this tooth asserts the stateDir is untouched under unapproved confirm
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
run: checkM4ConfirmNoConsentZeroSideEffects,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
registerTooth({
|
|
190
|
+
id: "m4.consent-binds-version",
|
|
191
|
+
profiles: ["service"],
|
|
192
|
+
layers: ["L0", "L1", "L4"],
|
|
193
|
+
kind: { kind: "invariant" },
|
|
194
|
+
mustRed: [
|
|
195
|
+
{
|
|
196
|
+
mutate: "the continuation installs whatever the server serves now, not the approved version",
|
|
197
|
+
caughtOnlyBy: "this", // only this tooth binds consent to the specific offered version
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
run: checkM4ConsentBindsVersion,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
registerTooth({
|
|
204
|
+
id: "m4.notify-only-reports-installable-version",
|
|
205
|
+
profiles: ["service"],
|
|
206
|
+
layers: ["L0", "L1", "L4"],
|
|
207
|
+
kind: { kind: "invariant" },
|
|
208
|
+
mustRed: [
|
|
209
|
+
{
|
|
210
|
+
mutate: "the notification names a version that is not the installable one",
|
|
211
|
+
caughtOnlyBy: "this", // only this tooth pins the notification to the actual pick
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
run: checkM4NotifyOnlyReportsInstallableVersion,
|
|
215
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M1 download-hole teeth (the 8 L0 fixes, one tooth per hole:
|
|
3
|
+
* deadline raced not signalled, Rosetta arch lie, progress without
|
|
4
|
+
* resumeDir, empty body named, stall bounds silence, error classification,
|
|
5
|
+
* stall phase honesty). Registration site only — check bodies live in
|
|
6
|
+
* harness/src/artifact/downloadHoles.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { registerTooth } from "./registry.ts";
|
|
9
|
+
import {
|
|
10
|
+
checkDownloadTimeoutEnforced,
|
|
11
|
+
checkPlatformKeyNativeArch,
|
|
12
|
+
checkDownloadProgressWithoutResumeDir,
|
|
13
|
+
checkDownloadEmptyBodyNamed,
|
|
14
|
+
checkDownloadStallBoundsSilence,
|
|
15
|
+
checkDownloadErrorsClassified,
|
|
16
|
+
checkDownloadStallPhaseHonest,
|
|
17
|
+
} from "../artifact/downloadHoles.ts";
|
|
18
|
+
|
|
19
|
+
registerTooth({
|
|
20
|
+
id: "m1.download-timeout-enforced",
|
|
21
|
+
profiles: ["swap", "service"],
|
|
22
|
+
layers: ["L0"],
|
|
23
|
+
kind: { kind: "invariant" },
|
|
24
|
+
mustRed: [
|
|
25
|
+
{
|
|
26
|
+
mutate:
|
|
27
|
+
"the deadline is only signalled, never raced — a fetch that ignores AbortSignal outlives every timeout (all timeouts silently inert)",
|
|
28
|
+
caughtOnlyBy: "this", // fetchImpl is an adopter-supplied seam; its regression looks exactly like a working download
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
run: checkDownloadTimeoutEnforced,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
registerTooth({
|
|
35
|
+
id: "m1.platform-key-native-arch",
|
|
36
|
+
profiles: ["swap", "service"],
|
|
37
|
+
layers: ["L0"],
|
|
38
|
+
kind: { kind: "invariant" },
|
|
39
|
+
mustRed: [
|
|
40
|
+
{
|
|
41
|
+
mutate:
|
|
42
|
+
"the platform key trusts process.arch — under Rosetta the machine is pinned to the x64 build forever, with nothing looking wrong",
|
|
43
|
+
caughtOnlyBy: "this", // the wrong package is still a fully legal package: no error, just the wrong hardware served
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
mutate:
|
|
47
|
+
"the hardware probe is consulted outside darwin+x64 (shelling out on every platform lookup)",
|
|
48
|
+
caughtOnlyBy: "this",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
run: checkPlatformKeyNativeArch,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
registerTooth({
|
|
55
|
+
id: "m1.download-progress-without-resumedir",
|
|
56
|
+
profiles: ["swap", "service"],
|
|
57
|
+
layers: ["L0"],
|
|
58
|
+
kind: { kind: "invariant" },
|
|
59
|
+
mustRed: [
|
|
60
|
+
{
|
|
61
|
+
mutate:
|
|
62
|
+
"byte progress never fires without a resumeDir (a configured-looking sink that never moves)",
|
|
63
|
+
caughtOnlyBy: "this", // "slow" vs "hung" must stay distinguishable even with no partial to resume
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
run: checkDownloadProgressWithoutResumeDir,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
registerTooth({
|
|
70
|
+
id: "m1.download-empty-body-named",
|
|
71
|
+
profiles: ["swap", "service"],
|
|
72
|
+
layers: ["L0"],
|
|
73
|
+
kind: { kind: "invariant" },
|
|
74
|
+
mustRed: [
|
|
75
|
+
{
|
|
76
|
+
mutate:
|
|
77
|
+
"a no-body response returns zero bytes — the only symptom is a sha256 mismatch against the empty string, hiding the cause",
|
|
78
|
+
caughtOnlyBy: "this", // the failure path must not produce the empty success value
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
mutate:
|
|
82
|
+
"the RESUME path treats a no-body response as an empty prefix to continue from (the cause hides behind a sha256 mismatch there too)",
|
|
83
|
+
caughtOnlyBy: "this", // discovered while writing these teeth: the twin path had no pre-existing mutation to copy
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
run: checkDownloadEmptyBodyNamed,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
registerTooth({
|
|
90
|
+
id: "m1.download-stall-bounds-silence",
|
|
91
|
+
profiles: ["swap", "service"],
|
|
92
|
+
layers: ["L0"],
|
|
93
|
+
kind: { kind: "invariant" },
|
|
94
|
+
mustRed: [
|
|
95
|
+
{
|
|
96
|
+
mutate:
|
|
97
|
+
"the stall budget is a total timeout — a slow-but-progressing download is killed (silence and duration conflated)",
|
|
98
|
+
caughtOnlyBy: "this", // bounding SILENCE follows liveness; a total budget encodes a guess about size/bandwidth
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
mutate: "there is no stall budget at all — a wedged connection holds until the total timeout and never names the stall",
|
|
102
|
+
caughtOnlyBy: "this",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
run: checkDownloadStallBoundsSilence,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
registerTooth({
|
|
109
|
+
id: "m1.download-errors-classified",
|
|
110
|
+
profiles: ["swap", "service"],
|
|
111
|
+
layers: ["L0"],
|
|
112
|
+
kind: { kind: "invariant" },
|
|
113
|
+
mustRed: [
|
|
114
|
+
{
|
|
115
|
+
mutate:
|
|
116
|
+
"an in-memory download error is reported bare — an abort we caused ourselves is indistinguishable from the network breaking",
|
|
117
|
+
caughtOnlyBy: "this", // "we gave up on purpose" must be tellable from "the network died"
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
run: checkDownloadErrorsClassified,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
registerTooth({
|
|
124
|
+
id: "m1.download-stall-phase-honest",
|
|
125
|
+
profiles: ["swap", "service"],
|
|
126
|
+
layers: ["L0"],
|
|
127
|
+
kind: { kind: "invariant" },
|
|
128
|
+
mustRed: [
|
|
129
|
+
{
|
|
130
|
+
mutate:
|
|
131
|
+
"a stall that happened mid-body is reported as 'awaiting response' — the reader looks at the wrong end of the transfer",
|
|
132
|
+
caughtOnlyBy: "this",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
run: checkDownloadStallPhaseHonest,
|
|
136
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact-factory teeth (harness-design §1.77; the package spec:
|
|
3
|
+
* determinism + the ok artifact really runs and reports its version).
|
|
4
|
+
*
|
|
5
|
+
* Registration site only — check bodies live in
|
|
6
|
+
* artifact-factory/checks.ts; importing this module registers the teeth.
|
|
7
|
+
*/
|
|
8
|
+
import { registerTooth } from "./registry.ts";
|
|
9
|
+
import { checkDeterministicBuild, checkOkArtifactRuns } from "../artifact-factory/checks.ts";
|
|
10
|
+
|
|
11
|
+
registerTooth({
|
|
12
|
+
id: "artifact-factory.deterministic-build",
|
|
13
|
+
profiles: ["swap", "service"],
|
|
14
|
+
layers: ["L0"],
|
|
15
|
+
kind: { kind: "invariant" },
|
|
16
|
+
mustRed: [
|
|
17
|
+
{
|
|
18
|
+
mutate: "stamp embeds a time-varying byte (e.g. a build timestamp)",
|
|
19
|
+
caughtOnlyBy: "this", // only this tooth compares two fresh builds byte-for-byte
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
run: checkDeterministicBuild,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
registerTooth({
|
|
26
|
+
id: "artifact-factory.ok-artifact-runs",
|
|
27
|
+
profiles: ["swap", "service"],
|
|
28
|
+
layers: ["L0"],
|
|
29
|
+
kind: { kind: "invariant" },
|
|
30
|
+
mustRed: [
|
|
31
|
+
{
|
|
32
|
+
mutate: "stamp embeds the wrong version string",
|
|
33
|
+
caughtOnlyBy: "this", // determinism tooth still passes (wrong is consistent); only a real run exposes it
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
mutate: "the ok artifact exits non-zero on start",
|
|
37
|
+
caughtOnlyBy: "this", // only this tooth executes the built binary
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
run: checkOkArtifactRuns,
|
|
41
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M5 platform-surface / lifecycle-convergence teeth (test-plan M5 rows).
|
|
3
|
+
* Registration site only — check bodies live in harness/src/artifact/m5.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { registerTooth } from "./registry.ts";
|
|
6
|
+
import {
|
|
7
|
+
checkM5LifecycleSurfaceAllowlist,
|
|
8
|
+
checkM5LifecycleConvergedPromotes,
|
|
9
|
+
checkM5LifecycleProjectionBan,
|
|
10
|
+
checkM5LifecycleFailClosedRetirement,
|
|
11
|
+
} from "../artifact/m5.ts";
|
|
12
|
+
|
|
13
|
+
registerTooth({
|
|
14
|
+
id: "m5.lifecycle-surface-allowlist",
|
|
15
|
+
profiles: ["service"],
|
|
16
|
+
layers: ["L3"],
|
|
17
|
+
kind: { kind: "invariant" },
|
|
18
|
+
mustRed: [
|
|
19
|
+
{
|
|
20
|
+
mutate: "an unregistered (or unreadable) surface vouches for convergence",
|
|
21
|
+
caughtOnlyBy: "this", // only this tooth pins the allowlist trust boundary
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
run: checkM5LifecycleSurfaceAllowlist,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
registerTooth({
|
|
28
|
+
id: "m5.lifecycle-converged-promotes",
|
|
29
|
+
profiles: ["service"],
|
|
30
|
+
layers: ["L3"],
|
|
31
|
+
kind: { kind: "invariant" },
|
|
32
|
+
mustRed: [
|
|
33
|
+
{
|
|
34
|
+
mutate: "a stale surface read-back still allows the promote",
|
|
35
|
+
caughtOnlyBy: "this", // only this tooth pins convergence before promote
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
run: checkM5LifecycleConvergedPromotes,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
registerTooth({
|
|
42
|
+
id: "m5.lifecycle-projection-ban",
|
|
43
|
+
profiles: ["service"],
|
|
44
|
+
layers: ["L3"],
|
|
45
|
+
kind: { kind: "invariant" },
|
|
46
|
+
mustRed: [
|
|
47
|
+
{
|
|
48
|
+
mutate: "a version/metadata string satisfies host_lifecycle_converged",
|
|
49
|
+
caughtOnlyBy: "this", // only this tooth pins the projection ban
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
run: checkM5LifecycleProjectionBan,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
registerTooth({
|
|
56
|
+
id: "m5.lifecycle-fail-closed-retirement",
|
|
57
|
+
profiles: ["service"],
|
|
58
|
+
layers: ["L3"],
|
|
59
|
+
kind: { kind: "invariant" },
|
|
60
|
+
mustRed: [
|
|
61
|
+
{
|
|
62
|
+
mutate: "the legacy lifecycle manager is retired before convergence passed",
|
|
63
|
+
caughtOnlyBy: "this", // only this tooth pins the retirement order
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
run: checkM5LifecycleFailClosedRetirement,
|
|
67
|
+
});
|
|
68
|
+
|