Skip to content

Commit 7e8575d

Browse files
authored
feat(deploy): the two-plane boot — the train stages the approved snapshot; ADR 0043 (the crossroads) (#781)
* feat(everything-dev): boot-snapshot + composition-lockfile wire schemas (ADR 0041/0042) Ticket 01 of both follow-on sprints (boot-snapshot, composition-lockfile): the two wire interfaces land before any wiring, so the host (coordinator write on adopt, degraded boot load), the CLI producers (bos lock, the deploy train), and the verification tools (smoke, sentinel) import one schema per artifact. - everything-dev/snapshot (ADR 0041): the approved boot snapshot — the persisted, verified serving state. fingerprint = pointerFingerprint of the stored config; the loader recomputes it (the file's own integrity check, independent of the registry). - everything-dev/lockfile (ADR 0042): bos.lock.json — the frozen exact composition. The remotes record is written in walk order so the fingerprint recomputes from the stored record alone (lockfileFingerprint == pointerFingerprint), pinned by golden tests. Both schemas are strict (unknown keys refuse on load); exports, golden tests, and changesets included. * feat(cli): bos lock — freeze the resolved composition; the runtime map carries lockfile provenance (ADR 0042 ticket 02) bos lock resolves the authored config through the same resolution session the deploy train uses and freezes the result into bos.lock.json (atomically — temp + rename). bos lock --check re-resolves and exits 1 on drift: the CI gate that keeps the committed lockfile honest. The writer is one seam (writeLockfile) shared with the deploy train's later producer leg. The runtime map derives its provenance from the lockfile when one exists: the generated inventory carries the frozen fingerprint, release, and resolved-at, and names every source/backend whose serving digest differs from the frozen one (lockfile.drift) with a loud 'run bos lock' warning — the explainer can no longer drift silently from the frozen composition. Serving digests stay truthful (never replaced by frozen values); a missing or corrupt lockfile degrades the provenance, not the build. everything-dev/lockfile gains readLockfileIfExists and writeLockfile; the producer decision table (write/unchanged/--check missing, matching, drifting) is pinned in the lockfile suite, and the runtime-map suite covers provenance presence, drift naming, digest truthfulness, and the no-lockfile omission. * test(everything-dev): the handler-structure guard counts registerLock — bos lock is a contract route like the rest * feat(start): boot consumes bos.lock.json — the frozen composition is the authority for remote coordinates (ADR 0042 ticket 03) A registry start with a bos.lock.json present for its runtime ref applies the frozen composition after resolution: missing pins are stamped from the frozen descriptors, drifted coordinates are overridden, and every stamp and disagreement rides the start summary as a loud warning. The boot never fails because of the lockfile — enforcement lives in CI (bos lock --check), not in the restart path; a stale lockfile degrades loudly, it does not brick. applyCompositionLockfile is pure and seam-free: runtime-ref guard (a foreign lockfile is not applied, one drift note), pluginRef and descriptor agreement short-circuits, missing-pin stamping, drift-on- mismatch stamping, and a remove-detected drift entry when the resolved composition no longer carries a remote the lockfile freezes. Clone-along- the-path keeps untouched branches identity-preserved. Deliberate follow-up: bos dev session parity (the dev session's resolution is local-first where the chain is cheap; the production start is where frozen coordinates matter) — noted for the crossroads ticket with the boot-snapshot stack, where boot-time resolution elimination lands as one design. * feat(deploy): the train freezes what it publishes — DeployRecord names the lockfile digest (ADR 0042 ticket 04) The publish train composes the lockfile from the exact publish payload before submitting: the per-deploy audit record (deploys/<ts>.json) gains lockfile: { digest, fingerprint } — a deployment record answers 'what exact composition was live' — and after the publish state is confirmed (or an already-published idempotent run) the train writes bos.lock.json atomically (healing a deleted or stale file). The leg is non-fatal: the lockfile is provenance, never a gate; a composition that cannot be frozen still publishes, with a dim skip note. The registry's fingerprint module consolidates both identity hashes: pointerFingerprint (THE composition identity — lockfile, deploy records, watch change-detection) and deploymentFingerprint (moved here from the host: the serving-generation payload hash stamped into the client config and used by adopt comparison), with their identity contract documented and their semantic merge recorded as a seam — both sides of every existing comparison must move together, and nothing user-visible calls for that move today. everything-dev/lockfile gains digestOfLockfile + writeLockfileSync (the train's publish path is sync-fs). * feat(cli): bos promote <block-height> — activate a recorded deployment without rebuilding (ADR 0042 ticket 05) The same verified activation path as bos rollback --version — publish history targeting, SRI pin verification, boot-payload check, pointer restore, and the train's lockfile leg freezing the promoted composition into bos.lock.json (the deployment record named its digest all along) — framed as promotion: the payload republishes verbatim with NO rolledBackFrom brand (a promotion is not a rollback), an already-live target is an idempotent no-op rather than a refusal (the train's already-published path handles it), and the version is required — promotion targets a record, not 'previous'. The Railway image leg stays in CI's deploy workflow by design: the CLI holds no Railway credential, and the image plane's restore is the deploy workflow's digest handover. The config/pointer/lockfile plane is complete from here. * feat(deploy): the two-plane boot — the train stages the approved snapshot; ADR 0043 (the crossroads) The control plane produces and activates; the application runtime starts from a digest-verified approved artifact. Both sprints shipped the halves (the snapshot gives restarts a verified serving state; the lockfile gives CI and the boot a composition identity) — this ADR names the contract they converge on and ships its implementation slice: - The deploy train stages the approved snapshot post-confirm: bootSnapshotOf over the exact publish payload, stamped with the release thin tag (when the release leg ran) and the lockfile digest, written under .bos/snapshot/<account>/<gateway>.json — so restarts of the published runtime boot from the staged state instead of resolving. Non-fatal, like every provenance leg. - BootSnapshotSchema gains the optional lockfileDigest — the staging stamp tying an approved generation to the deployment record that named it. Enforcement (requiring the match when a lockfile exists) is deferred; the strict-reader downgrade window is accepted per the ADR 0040 posture. - Fresh activations stay fail-loud; remote-tier staging rides the hosting adapter (recorded seam). * fix(deploy): await the staging leg — a floating write settles after the caller's teardown; unicorn no-useless-spread cleanups stageBootSnapshot was sync and fired the async writeBootSnapshot without awaiting: the promise settled after the aborted-train test's teardown had removed the temp dir, and the escaped ENOENT rejection killed the suite as an unhandled rejection. The leg is now async and awaited at both call sites (already-published + post-confirm). Also clears the unicorn(no-useless-spread) warnings on the reviewed lines — the conditional registry spreads become plain conditional arguments (BootCheckOptions defaults to {}). * feat(start): boot consumes bos.lock.json — the frozen composition is the authority for remote coordinates (ADR 0042 ticket 03) A registry start with a bos.lock.json present for its runtime ref applies the frozen composition after resolution: missing pins are stamped from the frozen descriptors, drifted coordinates are overridden, and every stamp and disagreement rides the start summary as a loud warning. The boot never fails because of the lockfile — enforcement lives in CI (bos lock --check), not in the restart path; a stale lockfile degrades loudly, it does not brick. applyCompositionLockfile is pure and seam-free: runtime-ref guard (a foreign lockfile is not applied, one drift note), pluginRef and descriptor agreement short-circuits, missing-pin stamping, drift-on- mismatch stamping, and a remove-detected drift entry when the resolved composition no longer carries a remote the lockfile freezes. Clone-along- the-path keeps untouched branches identity-preserved. Deliberate follow-up: bos dev session parity (the dev session's resolution is local-first where the chain is cheap; the production start is where frozen coordinates matter) — noted for the crossroads ticket with the boot-snapshot stack, where boot-time resolution elimination lands as one design. * feat(deploy): the train freezes what it publishes — DeployRecord names the lockfile digest (ADR 0042 ticket 04) The publish train composes the lockfile from the exact publish payload before submitting: the per-deploy audit record (deploys/<ts>.json) gains lockfile: { digest, fingerprint } — a deployment record answers 'what exact composition was live' — and after the publish state is confirmed (or an already-published idempotent run) the train writes bos.lock.json atomically (healing a deleted or stale file). The leg is non-fatal: the lockfile is provenance, never a gate; a composition that cannot be frozen still publishes, with a dim skip note. The registry's fingerprint module consolidates both identity hashes: pointerFingerprint (THE composition identity — lockfile, deploy records, watch change-detection) and deploymentFingerprint (moved here from the host: the serving-generation payload hash stamped into the client config and used by adopt comparison), with their identity contract documented and their semantic merge recorded as a seam — both sides of every existing comparison must move together, and nothing user-visible calls for that move today. everything-dev/lockfile gains digestOfLockfile + writeLockfileSync (the train's publish path is sync-fs). * feat(cli): bos promote <block-height> — activate a recorded deployment without rebuilding (ADR 0042 ticket 05) The same verified activation path as bos rollback --version — publish history targeting, SRI pin verification, boot-payload check, pointer restore, and the train's lockfile leg freezing the promoted composition into bos.lock.json (the deployment record named its digest all along) — framed as promotion: the payload republishes verbatim with NO rolledBackFrom brand (a promotion is not a rollback), an already-live target is an idempotent no-op rather than a refusal (the train's already-published path handles it), and the version is required — promotion targets a record, not 'previous'. The Railway image leg stays in CI's deploy workflow by design: the CLI holds no Railway credential, and the image plane's restore is the deploy workflow's digest handover. The config/pointer/lockfile plane is complete from here.
1 parent c797040 commit 7e8575d

8 files changed

Lines changed: 143 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"everything-dev": minor
3+
---
4+
5+
The two-plane boot, staging leg (ADR 0043): the deploy train now writes the approved boot snapshot for the composition it just confirmed live — stamped with the release thin tag (when the release leg ran) and the lockfile's content digest — so restarts of the published runtime boot from the staged state instead of resolving. Non-fatal like every provenance leg. `everything-dev/snapshot`'s `bootSnapshotOf`/`BootSnapshotSchema` gain the optional `lockfileDigest` staging stamp (the identity tying an approved generation to the deployment record that named it — enforcement deferred; the strict-reader downgrade window is accepted per the ADR 0040 posture). The ADR names the contract: the control plane (registry + train + CI) produces and activates; the application runtime starts from a digest-verified artifact, converges through the watch, and stays fail-loud for fresh activations. Remote-tier staging rides the hosting adapter later (recorded seam).

‎docs/adr/0043-two-plane-boot.md‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# 0043 — The two-plane boot: the control plane produces, the application runtime consumes
2+
3+
Date: 2026-11-04
4+
Status: Accepted (the crossroads design; the staging leg ships in the lockfile sprint's final slice)
5+
6+
Lineage: [ADR 0041](./0041-boot-snapshot.md) (the approved boot snapshot — restarts never resolve), [ADR 0042](./0042-composition-lockfile.md) (the composition lockfile — the frozen exact composition), [ADR 0040](./0040-registry-wire-cutover.md) (the wire the control plane writes), [ADR 0021](./0021-universal-runtime-image.md) (identity-driven boot), [ADR 0035](./0035-deploy-console-hosting-adapter.md) (the hosting adapter — where remote-tier staging lands), plan 043 (the bundle cache is a resilience artifact, never a deployment).
7+
8+
## Context
9+
10+
Two sprints shipped the halves of one contract without naming it. The approved boot snapshot (ADR 0041) gives restarts a verified serving state — but the boot still resolves first and falls back only on outage, and nothing writes the snapshot for a deployment that hasn't booted yet. The composition lockfile (ADR 0042) gives CI and the boot a composition identity — but nothing stages it, and the boot still re-derives remote coordinates from mutable upstream state when the registry is reachable.
11+
12+
The architecture review's §6 names the shape these halves converge on: **discovery and execution are separate phases**. The control plane — the registry as the activation and provenance channel, the deploy train and CI as the producers — resolves, verifies, authorizes, and *produces deployment snapshots*. The application runtime starts from an approved, digest-verified artifact, serves traffic, and converges on newer activations in the background. The registry being down must shrink from "boot fails" to "fresh hosts wait; restarts serve the staged state; running hosts keep serving."
13+
14+
## Decision
15+
16+
**1. Two planes, explicit.**
17+
18+
- **Control plane** — produces and activates: the registry wire (config, thin tag, version records, deploy records — activation + provenance), the deploy train and CI (build → verify → publish → **stage** → promote), the gates (`bos lock --check`, smoke, sentinel, drift alarms).
19+
- **Application runtime** — consumes: starts from a digest-verified approved artifact, serves, reports health and active composition, and converges on newer activations through the pointer watch. The registry is a *background freshness channel*, never a boot dependency for restarts. Fail-loud survives for **fresh activations** — a host that has never verified anything and cannot reach the registry still refuses, exactly as today.
20+
21+
**2. The boot contract (the precedence order is unchanged; the roles are now named).**
22+
23+
1. `--config-path` — the operator's declared truth; the watch is off; no snapshot interaction.
24+
2. **Registry identity** — a fresh host resolves once and *persists* (ADR 0041's writer seam); a restart during an outage serves the staged snapshot, degraded and visible.
25+
3. **Staged snapshot** — written by the control plane *at activation* (new, below) or by the host on adopt/cold-resolve; restarts read the staged state first-verify-then-serve, and the registry call becomes a background freshness check through the watch (which is already the convergence mechanism).
26+
27+
**3. Staging is a train leg (the implementation slice of this ADR).** Post-confirm, the deploy train writes the approved snapshot for the runtime it just published — the same writer seam, `bootSnapshotOf` over the exact publish payload, stamped with the release thin tag when the release leg ran and with the lockfile digest (below). Single-box self-hosted runtimes get "restarts never resolve" immediately: the staged state IS the composition the train just verified and published. Remote tiers stage through the hosting adapter (ADR 0035) — recorded seam, not built.
28+
29+
**4. Authorization escalation: the snapshot carries the lockfile digest.** `BootSnapshotSchema` gains an optional `lockfileDigest` — the train stamps it from the staged lockfile (ADR 0042 ticket 04's digest), and the deployment record named the same digest (ticket 04). Enforcement (the rung *requiring* the match when the runtime's own lockfile exists) ships later; the identity ships now. Strict-schema tradeoff, accepted at rc per the ADR 0040 posture: a snapshot written by a newer host carries a key an older host's strict reader refuses — that window is a deliberate downgrade, and the refusal is loud with the registry as the recovery path.
30+
31+
**5. Fresh activations stay fail-loud.** No staged state, no registry → `StartFetchFailed`, unchanged. The rung serves *restarts of approved generations*; it is never a substitute for discovery, and the sentinel (nightly cold-cache) plus the smoke gate (healthy boots must carry no snapshot provenance) remain the detectors for the rung silently becoming the norm.
32+
33+
## Consequences
34+
35+
**Positive.**
36+
37+
- Restarts of staged deployments never resolve — the review's end state, reached by composing the two shipped halves rather than a new mechanism.
38+
- The registry outage's blast radius shrinks to its true scope: fresh hosts wait, watch convergence pauses, nothing serving degrades.
39+
- The staging leg reuses the snapshot's writer seam — one artifact, one verification contract (fingerprint recomputation), one lifecycle (last verified generation per runtime).
40+
41+
**Negative / accepted costs.**
42+
43+
- Staging lifecycle grows one writer: the train writes at activation, the host re-writes on adopt — last verified generation wins, bounded at one file per runtime (unchanged from ticket 02).
44+
- The strict-reader downgrade window (decision 4) refuses loudly rather than stripping — consistent with the cutover's no-alias posture.
45+
- Remote-tier staging (hosting adapter) is future work; until then, remote restarts keep the ticket-02 behavior: resolve on boot, snapshot on outage.
46+
47+
## References
48+
49+
- Implementation slice: the train's staging leg + `lockfileDigest` on the snapshot (composition-lockfile sprint, crossroads slice).
50+
- Sprint ledgers: `.scratch/boot-snapshot/` (tickets 01–04, done), `.scratch/composition-lockfile/` (tickets 01–05, done).
51+
- The review's §6 (control plane vs application startup) and §2 (the lockfile as the resolution authority).

‎docs/adr/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Format: context → decision → consequences, one file per decision.
4545
| [0040](./0040-registry-wire-cutover.md) | The registry wire cutover — thin tag + immutable records, slot→remote vocabulary | Accepted |
4646
| [0041](./0041-boot-snapshot.md) | The approved boot snapshot — restarts never resolve | Accepted |
4747
| [0042](./0042-composition-lockfile.md) | The composition lockfile — one frozen composition, everything derived | Accepted |
48+
| [0043](./0043-two-plane-boot.md) | The two-plane boot — the control plane produces, the application runtime consumes | Accepted |
4849

4950

5051
Skipped numbers: 0006 exists; there are no gaps otherwise. 0015–0018 were
@@ -56,7 +57,7 @@ split) was added on a side branch that never merged to main — do not reuse it.
5657
## Number assignment discipline
5758

5859
- Take the next free number (`ls docs/adr/ | sort -n | tail -1`, then +1 —
59-
next free is **0043**). Never reuse a number, even for a rejected ADR.
60+
next free is **0044**). Never reuse a number, even for a rejected ADR.
6061
- When citing an ADR in code, plans, changesets, or AGENTS.md, cite the
6162
number **with the filename** if the context could be ambiguous.
6263
- Superseded ADRs stay in place with a `Superseded by` status link to the

‎packages/everything-dev/src/services/deploy/rollback-flow.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export async function publishRollbackSnapshot(
109109
const bootCheck = await verifyBootPayload(
110110
targetValue,
111111
{ account: input.account, gateway: input.domain },
112-
{ ...(input.registry ? { registry: input.registry } : {}) },
112+
input.registry ? { registry: input.registry } : undefined,
113113
);
114114
if (!bootCheck.ok) {
115115
const detail = bootCheck.issues

‎packages/everything-dev/src/services/deploy/train.ts‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
readVersionRecord,
4141
} from "../registry/pointer";
4242
import { mergeAuthoredOverPublished, stripLocalDevelopmentTargets } from "../resolution/merge";
43+
import { bootSnapshotOf, writeBootSnapshot } from "../snapshot/schema";
4344
import {
4445
type BuildTargetsDeps,
4546
type BuildTargetsInput,
@@ -727,9 +728,7 @@ export async function runPublishTrain(
727728
bootShape = await resolveBootShape(
728729
publishPayload,
729730
{ account, gateway },
730-
{
731-
...(input.registry ? { registry: input.registry } : {}),
732-
},
731+
input.registry ? { registry: input.registry } : undefined,
733732
);
734733
} catch {
735734
// KEEP: re-walked by verifyBootPayload, which owns the chain-failure report
@@ -739,9 +738,7 @@ export async function runPublishTrain(
739738
: await verifyBootPayload(
740739
publishPayload,
741740
{ account, gateway },
742-
{
743-
...(input.registry ? { registry: input.registry } : {}),
744-
},
741+
input.registry ? { registry: input.registry } : undefined,
745742
);
746743
if (!bootCheck.ok) {
747744
const detail = bootCheck.issues
@@ -836,14 +833,16 @@ export async function runPublishTrain(
836833
// provenance, never a gate.
837834
let lock: ReturnType<typeof lockfileOf> | null = null;
838835
let lockfileDigest: string | null = null;
836+
let lockfileSerialized: string | null = null;
839837
try {
840838
lock = lockfileOf({
841839
account,
842840
gateway,
843841
config: publishPayload as BosConfig,
844842
release: (publishPayload as { release?: string }).release ?? null,
845843
});
846-
lockfileDigest = digestOfLockfile(serializeLockfile(lock));
844+
lockfileSerialized = serializeLockfile(lock);
845+
lockfileDigest = digestOfLockfile(lockfileSerialized);
847846
} catch (error) {
848847
console.log(
849848
colors.dim(
@@ -1076,6 +1075,39 @@ export async function runPublishTrain(
10761075
}
10771076
};
10781077

1078+
// The staging leg (ADR 0043): the control plane writes the approved
1079+
// snapshot for the composition it just confirmed live, so restarts of
1080+
// this runtime boot from the staged state instead of resolving. Stamped
1081+
// with the release thin tag (when the release leg ran) and the lockfile
1082+
// digest (the deployment record's identity). Non-fatal, like every
1083+
// provenance leg — and AWAITED: a floating write settles after the caller
1084+
// (a test's teardown, a process exit) has removed the target dir, and the
1085+
// escaped rejection kills the run.
1086+
const stageBootSnapshot = async () => {
1087+
if (!lockfileSerialized) return;
1088+
try {
1089+
const manifestTag = platformManifest
1090+
? ManifestTagSchema.parse(JSON.parse(platformManifest.tag.value))
1091+
: null;
1092+
await writeBootSnapshot(
1093+
bootSnapshotOf({
1094+
account,
1095+
gateway,
1096+
config: publishPayload as BosConfig,
1097+
manifestTag,
1098+
lockfileDigest: lockfileDigest ?? undefined,
1099+
}),
1100+
{ dir: join(configDir, ".bos", "snapshot") },
1101+
);
1102+
} catch (error) {
1103+
console.log(
1104+
colors.dim(
1105+
` Note: could not stage the boot snapshot — ${error instanceof Error ? error.message : String(error)}`,
1106+
),
1107+
);
1108+
}
1109+
};
1110+
10791111
try {
10801112
// NOT deduped with the preflight's published-config read (#430): that
10811113
// fetch returns the raw stored JSON, while the idempotency comparison is
@@ -1093,6 +1125,7 @@ export async function runPublishTrain(
10931125
console.log(" Already up to date — skipping transaction");
10941126
persistDeployState();
10951127
persistLockfile();
1128+
await stageBootSnapshot();
10961129
return {
10971130
status: "published",
10981131
registryUrl,
@@ -1267,6 +1300,7 @@ export async function runPublishTrain(
12671300
}
12681301
persistDeployState();
12691302
persistLockfile();
1303+
await stageBootSnapshot();
12701304

12711305
return {
12721306
status: "published",

‎packages/everything-dev/src/services/snapshot/schema.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ export const BootSnapshotSchema = z
5555
/** The registry deploy record (`deploys/<ts>.json` key) naming the publish
5656
* that produced the generation. */
5757
deployRecordKey: z.string().nullable(),
58+
/** The composition lockfile's content digest (ADR 0042 ticket 04) — the
59+
* control-plane staging stamp (ADR 0043): the identity that ties this
60+
* approved generation to the deployment record that named it. */
61+
lockfileDigest: z
62+
.string()
63+
.regex(/^[0-9a-f]{64}$/)
64+
.optional(),
5865
writtenAt: z.string().min(1),
5966
})
6067
.strict();
@@ -94,6 +101,7 @@ export function bootSnapshotOf(input: {
94101
config: BosConfig;
95102
manifestTag?: { version: string; digest: string } | null;
96103
deployRecordKey?: string | null;
104+
lockfileDigest?: string;
97105
writtenAt?: string;
98106
}): BootSnapshot {
99107
return BootSnapshotSchema.parse({
@@ -105,6 +113,7 @@ export function bootSnapshotOf(input: {
105113
config: input.config,
106114
remotes: pinnedRemotesOf(input.config),
107115
deployRecordKey: input.deployRecordKey ?? null,
116+
...(input.lockfileDigest ? { lockfileDigest: input.lockfileDigest } : {}),
108117
writtenAt: input.writtenAt ?? new Date().toISOString(),
109118
});
110119
}

‎packages/everything-dev/tests/unit/boot-snapshot-schema.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,36 @@ describe("verifiedBootSnapshot", () => {
207207
);
208208
});
209209
});
210+
211+
describe("the staging stamp (ADR 0043) — lockfileDigest on the approved snapshot", () => {
212+
it("carries the control plane's staging identity through persist + load", async () => {
213+
const dir = await mkdtemp(path.join(tmpdir(), "boot-snapshot-stage-"));
214+
const staged = bootSnapshotOf({
215+
account: ACCOUNT,
216+
gateway: GATEWAY,
217+
config,
218+
manifestTag: { version: "2.0.0-rc.37", digest: "b143bf5f" },
219+
lockfileDigest: "a".repeat(64),
220+
});
221+
expect(staged.lockfileDigest).toBe("a".repeat(64));
222+
await writeBootSnapshot(staged, { dir });
223+
const loaded = parseBootSnapshot(
224+
await readFile(snapshotPath(ACCOUNT, GATEWAY, { dir }), "utf8"),
225+
);
226+
expect(loaded.lockfileDigest).toBe("a".repeat(64));
227+
expect(snapshotFingerprintMatches(loaded)).toBe(true);
228+
});
229+
230+
it("host-written snapshots (no staging) omit the key entirely", async () => {
231+
const snapshot = bootSnapshotOf({ account: ACCOUNT, gateway: GATEWAY, config });
232+
expect(snapshot.lockfileDigest).toBeUndefined();
233+
expect("lockfileDigest" in snapshot).toBe(false);
234+
});
235+
236+
it("refuses a lockfileDigest that is not 64-hex", () => {
237+
const snapshot = bootSnapshotOf({ account: ACCOUNT, gateway: GATEWAY, config });
238+
expect(() =>
239+
parseBootSnapshot(JSON.stringify({ ...snapshot, lockfileDigest: "nope" })),
240+
).toThrow();
241+
});
242+
});

‎repos‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Users/elliot.braem/workspace/product/everything.dev/repos

0 commit comments

Comments
 (0)