@parity/product-lab 0.1.1 → 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/CHANGELOG.md +22 -0
- package/README.md +27 -0
- package/adapters/browser/playwright/README.md +2 -0
- package/adapters/browser/playwright/src/page-frame-capture.js +10 -2
- package/adapters/chain/papi/README.md +3 -1
- package/adapters/environment/engine/README.md +22 -4
- package/adapters/environment/engine/src/engine/engine-config.d.ts +1 -0
- package/adapters/environment/engine/src/engine/fork-bundle.d.ts +1 -1
- package/adapters/environment/engine/src/engine/fork-bundle.js +2 -2
- package/adapters/environment/engine/src/engine/fork-refresh.d.ts +4 -7
- package/adapters/environment/engine/src/engine/fork-refresh.js +52 -23
- package/adapters/environment/engine/src/engine/published-bundle.d.ts +19 -15
- package/adapters/environment/engine/src/engine/published-bundle.js +44 -51
- package/adapters/environment/engine/src/saved-fork/saved-fork-tooling.d.ts +1 -1
- package/adapters/environment/engine/src/saved-fork/saved-fork-tooling.js +5 -2
- package/adapters/environment/engine/src/saved-fork/saved-fork.js +1 -1
- package/adapters/environment/engine/src/setup/engine-build.d.ts +4 -4
- package/adapters/environment/engine/src/setup/engine-build.js +14 -4
- package/adapters/environment/engine/src/setup/patch-catalogue.d.ts +6 -0
- package/adapters/environment/engine/src/setup/patch-catalogue.js +5 -0
- package/adapters/environment/engine/src/setup/prepare-engine.d.ts +6 -5
- package/adapters/environment/engine/src/setup/prepare-engine.js +27 -10
- package/adapters/features/cucumber/README.md +4 -0
- package/adapters/features/cucumber/src/steps/register-feature-steps.js +15 -1
- package/adapters/system/node/README.md +3 -3
- package/adapters/video/ffmpeg/README.md +4 -0
- package/adapters/video/ffmpeg/src/ffmpeg-video-editor.d.ts +1 -1
- package/adapters/video/ffmpeg/src/ffmpeg-video-editor.js +19 -6
- package/core/application/README.md +30 -1
- package/core/application/src/features/direct-feature-steps.d.ts +23 -0
- package/core/application/src/features/direct-feature-steps.js +25 -0
- package/core/application/src/features/feature-recording.d.ts +6 -0
- package/core/application/src/features/feature-recording.js +3 -0
- package/core/application/src/features/feature-scenario.d.ts +10 -8
- package/core/application/src/features/feature-scenario.js +11 -15
- package/core/application/src/features/feature-step-times.d.ts +11 -1
- package/core/application/src/features/feature-step-times.js +20 -6
- package/core/application/src/features/lab-feature-session.d.ts +3 -1
- package/core/application/src/features/lab-feature-session.js +5 -2
- package/core/application/src/features/run-feature-steps.d.ts +3 -1
- package/core/application/src/features/run-feature-steps.js +4 -2
- package/core/application/src/lab-runtime-use-cases.js +5 -1
- package/core/application/src/recordings/demo-markup.d.ts +8 -0
- package/core/application/src/recordings/demo-markup.js +11 -1
- package/core/application/src/recordings/demo-recording.d.ts +0 -2
- package/core/application/src/recordings/demo-recording.js +40 -23
- package/core/application/src/recordings/frame-sources.d.ts +20 -1
- package/core/application/src/recordings/frame-sources.js +89 -0
- package/core/application/src/recordings/record-runs.d.ts +7 -1
- package/core/application/src/recordings/record-runs.js +66 -11
- package/core/application/src/recordings/recording-draft.d.ts +4 -0
- package/core/application/src/recordings/recording-draft.js +13 -4
- package/core/domain/README.md +14 -0
- package/core/domain/src/recordings/demo-cut.js +6 -4
- package/core/domain/src/recordings/demo-holds.d.ts +12 -0
- package/core/domain/src/recordings/demo-holds.js +31 -0
- package/core/domain/src/recordings/index.d.ts +1 -0
- package/core/domain/src/recordings/index.js +1 -0
- package/core/domain/src/recordings/recording-data.d.ts +9 -1
- package/core/domain/src/recordings/recording-settings.d.ts +5 -1
- package/core/domain/src/recordings/recording-settings.js +7 -0
- package/core/domain/src/recordings/run-recording.d.ts +2 -0
- package/core/domain/src/recordings/run-recording.js +8 -1
- package/core/ports/README.md +6 -0
- package/core/ports/src/driven/recording/demo-ports.d.ts +2 -0
- package/core/ports/src/driven/recording/frame-capture.d.ts +4 -0
- package/core/ports/src/driving/demo.d.ts +2 -0
- package/core/ports/src/driving/feature-session.d.ts +7 -0
- package/entrypoints/cli/README.md +6 -3
- package/entrypoints/cli/src/demo-command.d.ts +1 -1
- package/entrypoints/cli/src/demo-command.js +10 -5
- package/entrypoints/mcp/README.md +3 -0
- package/entrypoints/panel/README.md +6 -3
- package/entrypoints/panel/src/visible/detached-identities.js +5 -1
- package/entrypoints/panel/src/visible/window-frames.js +14 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @parity/product-lab
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 36bf358: Breaking change: `publishedBundle` now reads only the release asset record, requires a valid SHA-256 digest, and returns no snapshot. `PublishedBundle.digest` is required and `snapshot` is optional. Read bite time, blocks and runtimes from the verified installed or staged manifest after downloading. This removes the dependency on where `manifest.json` appears in the archive.
|
|
8
|
+
|
|
9
|
+
Refresh compares verified bite times before changing saved state. Background checks and explicit refresh download only when the published digest is new or unknown; an already staged matching bundle boots without another download. Older or equal downloads are not staged beside the installed bundle. The digest is a cache key, not an integrity check; existing manifest, snapshot-size, decompression and marker checks still apply. `PRODUCT_LAB_FORK_BUNDLE=keep` still downloads nothing.
|
|
10
|
+
|
|
11
|
+
An installed bundle with a missing or stale completion marker learns the digest after one download when its manifest matches and its snapshot sizes and decompression pass verification. Incomplete bundles remain unknown and continue downloading.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 759dc0b: Allow repository users to prepare Engine with the optional node-workspace fix, so previewnet collators find binaries in a custom run workspace. Keep stock Engine as the default and verify the selected patch before reuse and launch, including saved forks. Limit node base-path recovery to node launchers.
|
|
16
|
+
|
|
17
|
+
## 0.1.2
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- d9721de: Keep direct and custom feature steps in recordings, with their timing and after-step images. Default frames-only runs keep capture non-blocking. Video runs wait for document load completion and exact screenshot stability over 200 ms before taking after-step images, and add a 2-second tail per scenario before cleanup. Stability checking defaults to a 3-second cap, configurable with `PRODUCT_LAB_RECORDINGS_SETTLE_MS`; the effective cap reserves 1000 ms for the final image (half the capture timeout below 2000 ms), so the default capture timeout clamps requested stability waits to at most 4000 ms. At the cap, a fresh image is kept with `settled: false` and `waitedMs`, without failing the step. The recording view and demo captions disclose when a frame was taken at the stability cap. The existing whole-capture timeout still applies. Recording metadata is additive and older recordings remain readable.
|
|
22
|
+
|
|
23
|
+
`product-lab demo` now produces 1280×960 held stills with captions below the app, instead of a moving 1280×720 cut. Each step is held for at least 2 seconds and exports a midpoint PNG. `--max-seconds` no longer shortens the cut. Older recordings without after-state evidence are refused, including with `--plan` and `--slide-only`, instead of substituting unrelated footage.
|
|
24
|
+
|
|
3
25
|
## 0.1.1
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -323,3 +323,30 @@ For Parity's security disclosure process, and Bug Bounty program, feel free to v
|
|
|
323
323
|
|
|
324
324
|
Apache-2.0: see LICENSE. The icons and fonts the lab window carries keep their own licences, in the files beside them
|
|
325
325
|
(`lucide.LICENSE`, `*.OFL.txt`); the packages npm installs with this one keep theirs.
|
|
326
|
+
|
|
327
|
+
## Settled feature demos
|
|
328
|
+
|
|
329
|
+
Feature recordings include direct/custom step timing. Default frames-only capture stays asynchronous. Video recording (`--video`) awaits bounded after-step captures and keeps a 2-second tail per scenario before cleanup.
|
|
330
|
+
|
|
331
|
+
`product-lab demo` produces 1280×960 held stills, with a 1280×720 app above a separate caption band and a decoded midpoint PNG per step. Each hold lasts at least two seconds; `--max-seconds` never shortens it. Older recordings without after-state evidence are refused, including with `--plan` and `--slide-only`. Setup context and older slide fallback imagery are labelled.
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
Video feature after-frames first check that the current document has completed loading, then compare exact PNGs
|
|
335
|
+
across a 200 ms window, sampling at least 100 ms apart. The stability wait is capped at 3000 ms by default
|
|
336
|
+
(`RecordingSettings.settleTimeoutMs`, or `PRODUCT_LAB_RECORDINGS_SETTLE_MS`). At the cap, the recorder takes a
|
|
337
|
+
fresh frame anyway and adds `settled: false` and `waitedMs` to its metadata. Successful checks add `settled: true`;
|
|
338
|
+
`loadComplete` records when the source verified the document. The existing `captureTimeoutMs` still bounds the
|
|
339
|
+
whole capture, including this wait and any final image. The wait never changes the step's own timestamps or outcome.
|
|
340
|
+
|
|
341
|
+
Only feature after-frames with video enabled wait for visual stability, including the final frame after the
|
|
342
|
+
2-second scenario tail. Tool-run captures, before-frames, checkpoints and default frames-only feature captures
|
|
343
|
+
retain their existing asynchronous policy. Scenario cleanup still settles captures already in flight. Custom
|
|
344
|
+
capture sources may ignore the optional load check; absent `loadComplete` means load completion is unknown.
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
The effective stability cap reserves time for the final screenshot: it is clamped to `captureTimeoutMs` minus
|
|
348
|
+
1000 ms, or minus half that timeout when it is shorter than 2000 ms. With the default 5000 ms capture timeout,
|
|
349
|
+
`PRODUCT_LAB_RECORDINGS_SETTLE_MS` accepts positive values but uses at most 4000 ms; the default stays 3000 ms.
|
|
350
|
+
A captured frame with `settled: false` is labelled "Taken at the stability cap" in recording-view notes and in
|
|
351
|
+
the demo image manifest and captions, including reused setup and failure context. Older frames without this
|
|
352
|
+
metadata retain their existing labels.
|
|
@@ -212,3 +212,5 @@ condition on this package's own files; a distribution of axe-core itself keeps i
|
|
|
212
212
|
`newContext({ permissions })` and `newPage({ permissions })` forward explicit permissions to the selected Playwright context. `onPageError(listener)` observes the original error message and returns a disposer; it does not change the app or its error handling. Consumers redact diagnostic strings before persistence. The Chromium controls exercise camera/microphone grants and listener disposal in `tests/playwright.test.ts`. An example's companion launchers receive `SessionContext.browsers` rather than constructing an independent browser.
|
|
213
213
|
|
|
214
214
|
The native RTC connection fixture also closes its own browser when Node cancels its test. The test runner deadline alone does not interrupt an outstanding browser evaluation; the cancellation cleanup keeps a failed connection attempt from leaving that browser and test process alive. The original connection, privacy and ownership assertions remain in place.
|
|
215
|
+
|
|
216
|
+
`PageFrameCapture.capture` accepts optional `loadComplete: true`: it waits until the current document has `readyState === "complete"`, within the supplied capture timeout, and reports `loadComplete: true`. Video feature after-frame checks use this before comparing PNGs; ordinary captures add no load wait. Tests in `tests/page-frame-capture.test.ts` cover ordering and existing capture behavior.
|
|
@@ -56,7 +56,15 @@ export class PageFrameCapture {
|
|
|
56
56
|
else
|
|
57
57
|
return { status: "unavailable", reason: `Identity ${actorId}'s app document cannot paint over elements (no locators)` };
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
let loadComplete;
|
|
60
|
+
const image = async (target) => {
|
|
61
|
+
if (options.loadComplete && "waitForFunction" in target && typeof target.waitForFunction === "function") {
|
|
62
|
+
await target.waitForFunction(() => document.readyState === "complete", undefined, { timeout: shot.timeout });
|
|
63
|
+
loadComplete = true;
|
|
64
|
+
options.signal?.throwIfAborted();
|
|
65
|
+
}
|
|
66
|
+
return screenshots(target) ? new Uint8Array(await target.screenshot(shot)) : framed(target) ? new Uint8Array(await (await target.frameElement()).screenshot(shot)) : undefined;
|
|
67
|
+
};
|
|
60
68
|
let bytes;
|
|
61
69
|
try {
|
|
62
70
|
bytes = await image(page);
|
|
@@ -69,7 +77,7 @@ export class PageFrameCapture {
|
|
|
69
77
|
bytes = await image(again);
|
|
70
78
|
}
|
|
71
79
|
if (bytes)
|
|
72
|
-
return { status: "captured", mediaType: "image/png", bytes, ...settled };
|
|
80
|
+
return { status: "captured", mediaType: "image/png", bytes, ...settled, ...(loadComplete !== undefined ? { loadComplete } : {}) };
|
|
73
81
|
return { status: "unavailable", reason: `Identity ${actorId}'s app document offers no image capture` };
|
|
74
82
|
}
|
|
75
83
|
async accessibility(actorId, options = {}) {
|
|
@@ -38,7 +38,9 @@ alive, when an endpoint stopped answering: Node's own `WebSocket.close()` waits
|
|
|
38
38
|
bound, and polkadot-api's ws-provider drops a socket whose connection timed out (5 s) without closing it. A Notes
|
|
39
39
|
fork run whose live freshness reads timed out held four public sockets for nine minutes.
|
|
40
40
|
`tests/close-unresponsive.test.ts` drives both cases against a loopback endpoint that never answers, and
|
|
41
|
-
`composition/tests/fork-live.test.ts` the live runtime read; with the cut disabled all three fail.
|
|
41
|
+
`composition/tests/fork-live.test.ts` the live runtime read; with the cut disabled all three fail. The endpoint counts
|
|
42
|
+
only the client's WebSocket upgrades, so a local port probe's `GET /` is not taken for a reconnect, and each test closes
|
|
43
|
+
its access even when it fails early.
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
The account watch's reads (`ChainHistory`, since 29 September 2026): `signedExtrinsics(block, accounts)` gives a block's extrinsics one of the accounts signed, with their hashes (the signer is read from each signed version-4 extrinsic's `MultiAddress::Id`, `src/extrinsic-signer.ts`; unsigned and general extrinsics have none), and `pendingExtrinsics(accounts)` the pool's (`author_pendingExtrinsics`; `undefined` when the node does not offer the method, `isMethodUnavailable` in `src/errors.ts`). `poolAwareNonce(account)` (2 October 2026, backlog item 17k) is the node's own next nonce for the account, `system_accountNextIndex` asked by SS58 address: its nonce at the best block plus its transactions in the pool, every sender's and every format the pool holds; `undefined` without the method. Live on the People fast chain (omni-node), three `//Alice` remarks in flight: 6 while the nonce at the best block was 3 and `author_pendingExtrinsics` held 3; a read before the node imported them answers the best block's nonce.
|
|
@@ -23,7 +23,7 @@ use to reach an Engine network. Imported from the original lab.
|
|
|
23
23
|
| `engine/network-template.ts` | engine.json `template: true`: `saveNetworkTemplate` keeps a clean copy of a new network right after its first boot (Engine stopped, before the first resume) in `<workspace>/template/` (its data folder, network.json, versions.json, the session, `template.json`); `restoreNetworkTemplate` (a reset) clones it back as a new generation: a new spawn stamp in `<data>/spawn.json` and the session (the session check and the network lock compare it) and a new run id; its chain state is the first boot's, so nothing created since exists on it. `cloneTree`: `cp -c -R` (APFS copy-on-write) on macOS, `cp --reflink=auto -a` elsewhere, else a plain copy with a warning. The template serves its own workspace's resets only: every saved path names `<workspace>/network` |
|
|
24
24
|
| `engine/network-snapshots.ts` | Fixtures: `networkSnapshots(control)`, the `EnvironmentSnapshots` of a persistent network: `save` records each chain's finalized head, stops the network, copies `<workspace>/network` through the `TreeCloner` port (an APFS clone on macOS) with network.json, versions.json and the session, and resumes; `restore` refuses another kind, another lineage (`networkLineage`: the template's first boot, else the session's run; a network forked again is another lineage) or another workspace (saved paths name it), moves the live network aside, copies the snapshot in and resumes it, putting the moved one back when that fails. Since 30 September 2026 (backlog 1a) `control.files` names more plain files of the workspace to save beside the network (a consumer's own manifests, for example a workspace, an identity pool and its actors) under `files/`; restore puts each back, and one absent when saved stays absent. `networkSnapshotsUnsupported(owned, trees)` says why an attached network offers none. Since 30 September 2026 `networkSnapshots({ upgrade })` offers `EnvironmentSnapshots.upgrade` (a lab that owns the fork passes `savedForkUpdates(...).upgradeToLive`: `fixture refresh` upgrades a restored fixture in place); without it the snapshots say `upgradeUnsupported`. An Engine topology run (`engineEnvironment`) is disposable and reports `snapshotsUnsupported` Since 3 October 2026 (backlog 17h) `save` records `detail.savedAt` once the network has stopped, and with `control.locks` (`{ manager, directory?, log? }`) a restore reclaims the restored network's stale lock that a killed run took after that moment (`reclaimLockPredatedByRestore`), logging `Reclaimed the network lock … the restored state predates it (saved …)` |
|
|
25
25
|
| `engine/network-adoption.ts` | Fixture sharing (30 September 2026): `planNetworkAdoption` reads an imported network snapshot and this workspace's running plan and says what adopting it moves: the saved workspace directory and prepared Engine tooling directory to this checkout's (another tooling hash, another Engine, is refused) and, when the port block differs by one offset, every port of the old block wherever a port is named (plan arguments and environment, session, `engine-ports.json`, isolation and zombienet records, chain spec boot nodes, IPFS addresses; `rewriteText`); it refuses, naming each, another port layout, content-addressed IPFS blocks naming an old loopback port, and any other small file that still names the old workspace or ports. `applyNetworkAdoption` rewrites the snapshot copy; `networkSnapshots` `restore(…, { adopt: true })` plans before anything stops, applies it to its own snapshot copy, keeps the brought lineage in `network/adopted-lineage.json` (`readAdoptedLineage`, read first by `networkLineage`; a template reset replaces the folder and forgets it) and returns what moved. `save` records Engine's bite (`fork-snapshot.json`) and ports as `detail.provenance`. Since backlog 242 the workspace's own files a snapshot carries under `files/` (the environment names them: `networkSnapshots({ files })`) are rewritten too, so an environment's records with their ports and directories move with it. `tests/network-adoption.test.ts`; a dry run on a real saved fork: `tools/verify/live/engine/fixture-adoption.ts` |
|
|
26
|
-
| `engine/finality-stall.ts` | A saved network whose finality never advances after a resume (backlog 76). Seen live: relay validators that restart together can each lose the approval check of a candidate in their first second (`Session params unavailable (no active leaf?)`, upstream behaviour), and when too few are left to approve it (four of six lost) the relay block is never approved and nothing finalizes until the validators restart. `persistentNetwork.start()` recovers within a bound: it reads the validators' logs every second (`lostApprovalChecks`, `unapprovable`: the verdict arrives about 12 s into the start) and also calls a stall from the symptom alone (every chain answers, no finalized head advanced for `stallAfterMs`, default 2 minutes); it keeps what every process logged in `<workspace>/stall-evidence/<time>-start-<n>/` with `stall.json` (`keepStallEvidence`; `history.jsonl` lists every stall and outcome), stops the saved processes and starts them again, at most `stallRecovery.restarts` times (default 2). A recovered stall is reported: the `network-ready` phase says `finality advancing after 1 restart (finality had stalled: …)`, the logger warns, and `network.recovery.stalls` lists each stall (`NetworkStartRecovery`). One that is not recovered throws `FinalityStalledError` (`attempts`, `stalls`, each with its evidence directory), with the processes stopped and the data kept. `tests/finality-stall.test.ts` (the control: a network that never finalizes fails after 3 starts); the live loop is `tools/verify/live/engine/resume-loop.ts` Backlog 143 (5 October 2026): chains that stand while the relay's finalized head advances (read from a relay validator's RPC, `relayEndpoint` in `engine/drain.ts`) are waiting for the validators to prepare the parachains' PVFs, so they get no standing verdict and no restart, said in the start's progress; the start's deadline still bounds them, and a relay that stands too, or cannot be read, gives the verdict as before (`tests/finality-stall.test.ts`, with a frozen relay as its control). |
|
|
26
|
+
| `engine/finality-stall.ts` | A saved network whose finality never advances after a resume (backlog 76). Seen live: relay validators that restart together can each lose the approval check of a candidate in their first second (`Session params unavailable (no active leaf?)`, upstream behaviour), and when too few are left to approve it (four of six lost) the relay block is never approved and nothing finalizes until the validators restart. `persistentNetwork.start()` recovers within a bound: it reads the validators' logs every second (`lostApprovalChecks`, `unapprovable`: the verdict arrives about 12 s into the start) and also calls a stall from the symptom alone (every chain answers, no finalized head advanced for `stallAfterMs`, default 2 minutes); it keeps what every process logged in `<workspace>/stall-evidence/<time>-start-<n>/` with `stall.json` (`keepStallEvidence`; `history.jsonl` lists every stall and outcome), stops the saved processes and starts them again, at most `stallRecovery.restarts` times (default 2). A recovered stall is reported: the `network-ready` phase says `finality advancing after 1 restart (finality had stalled: …)`, the logger warns, and `network.recovery.stalls` lists each stall (`NetworkStartRecovery`). One that is not recovered throws `FinalityStalledError` (`attempts`, `stalls`, each with its evidence directory), with the processes stopped and the data kept. `tests/finality-stall.test.ts` (the control: a network that never finalizes fails after 3 starts); the live loop is `tools/verify/live/engine/resume-loop.ts` Backlog 143 (5 October 2026): chains that stand while the relay's finalized head advances (read from a relay validator's RPC, `relayEndpoint` in `engine/drain.ts`) are waiting for the validators to prepare the parachains' PVFs, so they get no standing verdict and no restart, said in the start's progress; the start's deadline still bounds them, and a relay that stands too, or cannot be read, gives the verdict as before (`tests/finality-stall.test.ts`, with a frozen relay as its control; the test's fake relay answers only JSON-RPC POSTs, so a local port probe's `GET /` gets a 404). |
|
|
27
27
|
| `engine/chain-clock.ts` | A resumed network's chains reach the wall clock before it is ready (backlog 106). A saved chain keeps the time of its last block until it builds a new one, and readiness waits for finality only on the lab's required chains, so a chain that only has to answer (Notes: Asset Hub) could still be at the save's time. That time is signed with: the truAPI CLI host timestamps its dotNS registration with Asset Hub's `Timestamp.Now` at the best block, and the identity backend refuses one more than 600 s old (seen 5 October 2026: gate batch 21's resume of a fork saved hours earlier failed onboarding with `dotns.signedAt is older than the intake freshness bound (600s)`). `persistentNetwork.start()` reads every chain's `Timestamp.Now` and best block after readiness (`waitForChainClocks`, `chainTime`) and waits until each is within 2 minutes (`CHAIN_CLOCK_MAX_LAG_MS`) of the wall clock, the `network-ready` phase naming what it waits for; one still behind after `CHAIN_CLOCK_TIMEOUT_MS` (2 minutes) is not building blocks, and the start fails with `ChainClockBehindError` (`behind`: each chain, its lag and best block), naming why and the way out (restart the network, or a fresh fork). The logger gives each chain's time, its lag at the first read and how long the wait took (`onRead`). `chainClock: false` skips it. `tests/chain-clock.test.ts` (the fake dashboard's `chainLagMs` sets each chain's time) |
|
|
28
28
|
| `engine/approval-pin.ts` | Relay finality pinned at the approval threshold after a session change (backlog 93, 239). A start whose first block changes the session builds two gossip topologies before any peer connects; polkadot-sdk's `SessionGridTopologies::update_authority_ids` gives the peers to the first only, so some approval votes reach nobody until approval-distribution's level-1 aggression (`l1_threshold` 16), and every block then finalizes about 16 blocks late for the rest of the start. `ApprovalPinWatch` and the constants `APPROVAL_PIN_GAP` (14), `APPROVAL_PIN_HOLD_MS` (30 s), `APPROVAL_PIN_RESTARTS` (1): once the relay has finalized past the best head of the start's first read (so a backlog the start began with, as after the pin's own restart without a drain, is finalized first), a best head 14 or more ahead of the finalized one for 30 s restarts the network once (`stallRecovery.pinned`), recorded in `recovery.stalls` as `pinned` ("finality pinned at the approval threshold after a session change; restarted once", with the heads and the gap), its logs kept like a stall's; a second pin fails the start (`FinalityPinnedError`). Watched while the start waits for readiness and, if the relay is at the threshold when it is ready, until it falls below or holds |
|
|
29
29
|
| `engine/node-temp-dir.ts` | A relay validator's TMPDIR fits its PVF sockets (backlog 93). A validator binds its PVF workers' Unix sockets directly in its TMPDIR (`pvf-host-prepare-<10>`); a path of `sun_path`'s size or more is refused (`path must be shorter than SUN_LEN`: 104 bytes on macOS and the BSDs, 108 on Linux, `SUN_PATH_BYTES`), so no candidate is validated, no approval completes and the relay's finality stops at the last block before the first unapproved candidate. Seen 4 October 2026: a fork started with TMPDIR under a lane's `_artifacts` folder (107-byte socket paths) stalled at relay #1,310,664 on a fresh boot and on every resume. `nodeTempDir(network, inherited)`: when the inherited TMPDIR is too long (`pvfSocketsFit`), the network's processes get `TMPDIR=/tmp/pl-pvf-<12 hex of the network directory>` (0700), on the first boot (`OwnedEngineDependency`: Engine, zombie-cli and so every node) and on each resume (`persistentNetwork`: every saved process), with a warning naming the byte counts; it is removed on stop, and the next start of the same network removes one a crashed run left. A TMPDIR that fits changes nothing. `tests/node-temp-dir.test.ts` (both sides of the limit on macOS and Linux), `tests/finality-stall.test.ts` (a resume), `tools/verify/tests/engine-owned-start.test.ts` (a first boot); each fails with the override removed |
|
|
@@ -33,7 +33,7 @@ use to reach an Engine network. Imported from the original lab.
|
|
|
33
33
|
| `engine/network-reset.ts` | `resetPersistentNetwork(directory, { stop, sessionFile, commands })` (29 September 2026): stops the saved network, removes its data and network.json, and restores the first boot's template as a new generation (`method: "template"`, with `stopMs` and the copy); a workspace without a template gets `method: "fork-again"` with the reason, and the caller's next `persistentNetwork` forks again (the old path). Notes' `paseo-copy` reset uses it. The persistent network's `stop()` (`persistent-network.ts`) now stops every saved process at once (`stopTogether`; on Notes' fork 3.8 s and 4.2 s against 4.5 s one after another), each with its own stop policy, every failure reported together (`tests/stop-together.test.ts`) |
|
|
34
34
|
| `engine/runtime-upgrade.ts` | An in-place runtime upgrade of one chain of a running network (relay, People, Asset Hub, Bulletin): `upgradeRuntime(request)` runs Engine's own `ppn upgrade <chain> <wasm> --ws <loopback> --enact-timeout <min>` (with `--allow-same-spec` for the same spec or `withoutChecks`; the caller decides whether Engine's authorization path applies to its network) as a recorded tree of the network's registry through `runEnginePrepare`, writes its code through the injected `FileSystem.writePrivateBytes`, and judges the result on the chain: `observeRuntime(rpc, ws)` before and after (finalized `:code` hash, spec, block, and `System.LastRuntimeUpgrade`, decoded by `decodeLastRuntimeUpgrade`, which says the new runtime's migrations ran). While Engine waits for enactment, the chain is read every 5 s: an authorization dropped with nothing scheduled and the code unchanged (`System.RejectedInvalidAuthorizedUpgrade`: another chain's runtime, a lower spec), which Engine's tool does not notice until its enact timeout, stops the wait at once as a failure. Outcomes: `already-running` (nothing submitted), `upgraded` (the chain runs exactly the code, `migrated` true or false), `failed` (with `unchanged`: the chain still runs its old code). `recordRuntimeUpgrade(files, …)` rewrites a persistent network's saved session (`network.json` and the session file) on purpose, since `checkEngineSession` refuses any runtime change it was not told about, and appends `runtime-upgrades.json` |
|
|
35
35
|
| `engine/fork-bundle.ts` | A fork bundle is reused only once verified complete. `verifyForkBundle(dir)`: `manifest.json` parses, every snapshot it names is there at the size it records (`snapshotBytes`, written by every bite since Engine v0.8.0; else one snapshot per chain) and decompresses to its end (3.5 s for the 642 MB Paseo bundle). `markForkBundle` writes `.product-lab-complete` (the manifest's sha256, each snapshot's size); `markedComplete` holds while both match. `forkBundleState(bundle)`: complete, absent, or incomplete; an unmarked bundle (fetched before this check, or copied in) is verified once and marked. A start's fetch or bite writes into `<bundle>.incoming` (cleared first; `EngineCommand.installs`), and `installIncomingBundle` verifies it, marks it and renames it into place after the command exits 0, so a fetch or bite stopped part way never leaves a bundle that is reused, and a failed one leaves the bundle in place unchanged. `acquireForkBundleLock` (`<bundle>.lock`, through the injected `LockManager`) is held from the bundle's check to the end of the preparatory commands: a second start from one workspace waits, then finds it complete. A start without isolation, where Engine's own `ppn start` fetches a missing bundle, is not covered |
|
|
36
|
-
| `engine/published-bundle.ts` | Engine's published fork bundles (the rolling `PPN_REPO@PPN_BITE_TAG` pre-release of the checkout's `config/versions.env`, `engineReleaseSettings`): `publishedBundle({ files, checkout, network, fetch })` (bounded by `timeoutMs`, default 180 000 ms, with the caller's optional `signal`) reads the asset record from GitHub's API and the
|
|
36
|
+
| `engine/published-bundle.ts` | Engine's published fork bundles (the rolling `PPN_REPO@PPN_BITE_TAG` pre-release of the checkout's `config/versions.env`, `engineReleaseSettings`): `publishedBundle({ files, checkout, network, fetch })` (bounded by `timeoutMs`, default 180 000 ms, with the caller's optional `signal`) reads only the release asset record from GitHub's API and requires a `sha256:` digest with 64 lowercase hex digits; it never requests the archive. `PublishedBundle.digest` is required and `snapshot` is optional and absent from lookup. Read bite time, blocks and runtimes from `currentBundle` or `stagedBundle` after a verified download. `manifestFromTarHead`, `TarManifestScanner` and `MANIFEST_SEARCH_BYTES` remain exported for existing callers but are unused by lookup. Under `PRODUCT_LAB_FORK_BUNDLE=keep` the background check uses its separate release-record-only report; the caller passes `fetch` (the public-endpoint opt-in) and the `FileSystem` system port for the checkout settings and staged bundles. `startPrefetch` downloads it with Engine's own `ppn fork fetch-bundle` into `fork-bundle-<network>.next.partial` at background priority (`backgroundPriority`: macOS `taskpolicy -c utility`, else `nice -n 19`) as a recorded process of the caller's registry; `finishPrefetch` verifies it complete and marks it (`engine/fork-bundle.ts`; an incomplete download is removed), then `completePrefetch` stages it (`.next`, `stagedBundle`, which offers only a verified one); `installStagedBundle` makes it the bundle new forks start from, keeping the old one until `discardPrevious` or `restorePrevious`. `currentBundle`, `bittenAfter`, `prefetchActive`, `bundleLocations` |
|
|
37
37
|
| `engine/fork-freshness.ts`, `engine/fork-refresh.ts`, `engine/fork-updates.ts` | Freshness, `--update-runtime` and `--refresh` for any saved Engine fork (fixture sharing, 30 September 2026, backlog 17c). They were lifted from an example, which now wraps them. State: `forkState` reads what a saved fork runs: its saved session, `runtimes.json` and its bite. `liveEndpoints` reads the public RPCs from Engine's network file; a relative one (previewnet's `relay/alice`, `people`) resolves under the file's `bite.source` as `wss://` (`resolveNetworkRpc`, as Engine's bite does). Freshness line: `freshness` is pure: it gives the age, the blocks behind live, and each chain whose spec or code changed, ending with the update command. The live read is injected as `LiveRuntimeReader`; the composition root's is `readLiveRuntimes`. `--update-runtime`: `updateForkRuntimes` applies each changed live runtime in place through `upgradeRuntime`. The code comes from an injected `LiveCodeFetcher`, checked against its hash. It then records the upgrade and waits `settleBlocks`. It refuses without a live read or a sudo key. Other pieces: `recordForkRuntimes`. `prefetchNewerState` fetches a newer published state in the background and never rejects. `PRODUCT_LAB_FORK_BUNDLE` (29 October 2026, read through the `EnvironmentVariables` port, so `savedForkUpdates` and a lab's own wrapper get it too): `fresh` or unset as described; `keep` reads only the release record from GitHub's API (no asset bytes, no range, nothing staged, no `ppn fork fetch-bundle`) and logs one line with the kept bundle's bite, its age, a newer state already downloaded, and the asset's `updated_at` when more than 6 hours after that bite (an upload within that is the bite's own); a record or local manifest that cannot be read is said, and the lab carries on. Surrounding white space is ignored; any other value logs a warning with the fix and checks nothing. `--refresh` and commands run by hand are the same under both. `tests/fork-bundle-keep.test.ts`. `prepareRefresh` sets the old fork aside for the newest published state; a lab record is carried across with `carry`. `commit` or `rollback` ends it. For a lab's start: `savedForkUpdates(options)` combines these into three calls. Before the start it runs `--refresh` when asked, and runs the live read and line beside the start. `started(target)` runs `--update-runtime` before anything connects, records the runtimes, commits a refresh and prefetches. `failed()` rolls a refresh back. `liveRpc` (3 October 2026): the lab's own RPC per chain for the live reads, over the network file's. `upgradeToLive(target, signal)` serves `networkSnapshots({ upgrade })` for `fixture refresh`. `tests/fork-freshness.test.ts` |
|
|
38
38
|
| `engine/owned-environment.ts` | `ownedEngineEnvironment`: owned Engine + consumer app sessions as one resettable environment; its local `engineSessionEnvironment` forwards optional app surfaces across reset and closes the current sessions and Engine once |
|
|
39
39
|
| `engine/network-plan.ts`, `engine/persisted-data.ts`, `engine/eth-rpc-probe.ts` | The persistent network's phases around `persistentNetwork`, split out of `persistent-network.ts`, which still exports their public names: the saved plan (`NetworkPlan`, `resumePlan`, `isolatedPlan`, `completePlan`, `planFacts`, `ENGINE_PORTS_FILE`); the resume's preconditions (`hasChainDatabase`, `missingPersistedData`, `requirePersistedData`, `restateEngineLocalEnv`, `engineCheckout`, `restateEnginePorts`, `portNames`, `requireFreePorts`; and, in `engine/saved-tooling.ts`, `missingSavedTooling` and `requireSavedTooling`: every command, what the lab's launchers in `<network>.isolation` exec, and the Engine checkout are on this machine); eth-rpc's health after a start (`probeEthRpc`, `EthRpcStatus`) |
|
|
@@ -45,9 +45,9 @@ use to reach an Engine network. Imported from the original lab.
|
|
|
45
45
|
| `topology/engine-plan.ts` | `engineLaunchPlan`: a resolved relay-backed `EngineTopology` as `ppn` commands (fork, `--fresh-bite`, `--upgrade`, `--runtime`, `--binary`, genesis subsets through `genesis-toml --parachains` then `start --toml`) and `config/ports.env` keys |
|
|
46
46
|
| `topology/engine-topology.ts` | `resolveEngineTopology`, `engineOptionsSchema`, `engineProfiles` (full-fork, genesis-subset), `engineFlagOptions`, `ENGINE_FLAGS` (`--engine-arg`, `--engine-env`, `--engine-toml`), `ENGINE_KIND`, `DEFAULT_ENGINE_PORTS`: the adapter's own topology options, defaults, flags and issues (refactor step 5) |
|
|
47
47
|
| `topology/engine-environment-factory.ts` | `EngineEnvironmentFactory`: the adapter's `TopologyAdapterFactory` (kind `engine`, section `engine`), constructed by the composition root when a lab selects `defineLab({ environmentAdapters: { engine: { engineConfig } } })` (`composition/src/adapter-catalogue.ts`), or registered directly with `defineLab({ environmentFactories: [new EngineEnvironmentFactory({ engineConfig })] })` |
|
|
48
|
-
| `setup/prepare-engine.ts`, `setup/engine-build.ts`, `setup/patches.ts`, `setup/patch-catalogue.ts` | Stock Engine v0.11.0 preparation and shipped release pin; `preparedEngine` checks the pin, stock record, checkout HEAD, built CLI and dot CLI. Retired
|
|
48
|
+
| `setup/prepare-engine.ts`, `setup/engine-build.ts`, `setup/patches.ts`, `setup/patch-catalogue.ts` | Stock Engine v0.11.0 preparation and shipped release pin; `preparedEngine` checks the pin, stock record, checkout HEAD, built CLI and dot CLI. The optional repository-only `EngineSource.patch: "node-workspace"` is manifest-checked, applied before build, keyed by digest and reverse-checked before reuse. Retired owned-processes records and requests are refused. Descriptor overlays remain digest-checked through the manifest. |
|
|
49
49
|
| `setup/setup-command.ts` | The command every Engine refusal names, for where this adapter runs (`engineSetupCommand(environment?)`): `bun run setup` in product-lab's checkout, `npx --no-install product-lab fetch --environment <id>` in a product, which has no `bun run setup` (`insideProductLabCheckout`, this adapter's own copy of the CLI's check) |
|
|
50
|
-
| `saved-fork/saved-fork-tooling.ts` | The Engine checkout a saved fork starts from, `<cacheDir>/engine` (16 October 2026): `savedForkEngine` (the start's check, `as-prepared`, its refusal naming `SavedForkSettings.setupCommand`, default `engineSetupCommand`), and `savedForkTooling`, the fork's `EnvironmentProvider.tooling` (id `engine:<cacheDir>/engine`, shared by every saved fork with that cache; kind `engine`): `check` is `savedForkEngine`'s, `prepare` runs `prepareEngine` there for the fork's network with the request's source (`repository`, `ref`, `patched: false` for stock, also the default; `patched: true` is refused) and returns the check of exactly what it asked for. `product-lab fetch` prepares it and `product-lab doctor` reports it in a product. A saved fork started from a stock build writes `enginePatch: "none"` in its own engine.json |
|
|
50
|
+
| `saved-fork/saved-fork-tooling.ts` | The Engine checkout a saved fork starts from, `<cacheDir>/engine` (16 October 2026): `savedForkEngine` (the start's check, `as-prepared`, its refusal naming `SavedForkSettings.setupCommand`, default `engineSetupCommand`), and `savedForkTooling`, the fork's `EnvironmentProvider.tooling` (id `engine:<cacheDir>/engine`, shared by every saved fork with that cache; kind `engine`): `check` is `savedForkEngine`'s, `prepare` runs `prepareEngine` there for the fork's network with the request's source (`repository`, `ref`, `patched: false` for stock, also the default; `patched: true` is refused) and returns the check of exactly what it asked for. `product-lab fetch` prepares it and `product-lab doctor` reports it in a product. A saved fork can reuse a node-workspace build prepared through `prepareEngine`; its status names that patch and its launch config records `enginePatch`, so the launch-time reverse check is retained. A saved fork started from a stock build writes `enginePatch: "none"` in its own engine.json |
|
|
51
51
|
| `testing` subpath | Fake Engine dashboard and chain RPCs for contract tests (`codeHashOffset`, `specVersion` change the served runtime) |
|
|
52
52
|
| `versions/engine-versions.ts` | Phase 6: `engineVersionPlan(topology, resolution, { snapshot, now })` (what Engine honours: runtimes read from the network it forks; refused as unsupported: released or git runtimes, resolved binaries, a runtime override on the same chain, a network runtime on genesis, a published bundle that `SnapshotFreshness` finds stale or on another runtime), `engineVersionCheck(session, plan, snapshot)` (each chain's observed `:code` hash, spec, genesis and bite block against the resolution), `VersionedEngineDependency` (checks every start and reset, stops and throws `EngineVersionMismatch` on other code), `engineVersionsRecord`, `workspaceVersionPlan` (persistent networks). Engine cannot fork at a chosen block: a `deployed` resolution is honoured by runtime, and the bite blocks are recorded beside the resolved block |
|
|
53
53
|
|
|
@@ -121,3 +121,21 @@ a taken port named, and the readiness wait's reasons). With the Node
|
|
|
121
121
|
system adapters: the lock on disk, `tools/verify/tests/scenario-lock.test.ts`; an owned Engine started, awaited and
|
|
122
122
|
stopped against a stand-in checkout, and a stock build passing the pre-start check while legacy patched configurations are refused, `tools/verify/tests/engine-owned-start.test.ts`; an Engine session routing the
|
|
123
123
|
truAPI CLI host, `tools/verify/tests/host-routes.test.ts`.
|
|
124
|
+
|
|
125
|
+
Native-node workspace regression: `node --import tsx --test adapters/environment/engine/tests/node-workspace.test.ts`
|
|
126
|
+
from the repository root. This runs the actual before/after shell launcher extracted
|
|
127
|
+
from the full-context manifest patch, under `env -i` with temporary stub binaries,
|
|
128
|
+
and checks the actual TOML writer. No chain, user workspace or Engine cache is used.
|
|
129
|
+
|
|
130
|
+
Lookup cannot compare bite times before download. Under `fresh`, background checks and explicit `--refresh` download
|
|
131
|
+
only when the published digest is new or unknown, then compare verified bite times before touching saved state.
|
|
132
|
+
A matching installed or staged bundle skips the download; `--refresh` boots a matching staged newer bite. A download
|
|
133
|
+
no newer than the installed bundle is not staged. Markers from older versions remain readable; an identical bundle
|
|
134
|
+
needs one download to record its digest. Missing or stale markers are rebuilt only after the installed manifest,
|
|
135
|
+
snapshot sizes and decompression pass verification. When keeping an equal bite, the digest transfers only if manifest hashes
|
|
136
|
+
match. A changed manifest at the same bite stays unknown and downloads on each start until Engine publishes a newer bite.
|
|
137
|
+
|
|
138
|
+
The marker's optional `assetDigest` is a cache key, not an integrity check. Engine owns and removes the tarball;
|
|
139
|
+
the manifest, snapshot-size, decompression and `manifestSha256` checks still protect bundle completeness. The release
|
|
140
|
+
digest is checked again after downloading, and is recorded only if unchanged; a racing release replacement or a failed
|
|
141
|
+
confirmation costs another download rather than skipping a needed one. `keep` still downloads nothing.
|
|
@@ -66,6 +66,7 @@ export declare const engineConfiguration: z.ZodObject<{
|
|
|
66
66
|
enginePatch: z.ZodDefault<z.ZodEnum<{
|
|
67
67
|
none: "none";
|
|
68
68
|
"owned-processes": "owned-processes";
|
|
69
|
+
"node-workspace": "node-workspace";
|
|
69
70
|
}>>;
|
|
70
71
|
recordProgress: z.ZodDefault<z.ZodBoolean>;
|
|
71
72
|
nodeSupervision: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -17,7 +17,7 @@ export type BundleCheck = {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function verifyForkBundle(directory: string, signal?: AbortSignal): Promise<BundleCheck>;
|
|
19
19
|
/** Record that `directory` was verified complete (written to a temporary name, then renamed: never half a marker). */
|
|
20
|
-
export declare function markForkBundle(directory: string, snapshots: Record<string, number>, source: string): void;
|
|
20
|
+
export declare function markForkBundle(directory: string, snapshots: Record<string, number>, source: string, assetDigest?: string): void;
|
|
21
21
|
/**
|
|
22
22
|
* True when `directory` carries the marker and still matches it: the same manifest, every snapshot at its size. A
|
|
23
23
|
* missing or unparsable marker, manifest or snapshot is "not marked": the caller verifies the bundle again. Any other
|
|
@@ -67,8 +67,8 @@ export async function verifyForkBundle(directory, signal) {
|
|
|
67
67
|
}
|
|
68
68
|
return { complete: true, snapshots };
|
|
69
69
|
}
|
|
70
|
-
export function markForkBundle(directory, snapshots, source) {
|
|
71
|
-
const marker = { manifestSha256: sha256(join(directory, "manifest.json")), snapshots, source, verifiedAt: new Date().toISOString() };
|
|
70
|
+
export function markForkBundle(directory, snapshots, source, assetDigest) {
|
|
71
|
+
const marker = { ...(assetDigest ? { assetDigest } : {}), manifestSha256: sha256(join(directory, "manifest.json")), snapshots, source, verifiedAt: new Date().toISOString() };
|
|
72
72
|
const file = join(directory, BUNDLE_MARKER);
|
|
73
73
|
writeFileSync(`${file}.tmp`, `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
|
|
74
74
|
renameSync(`${file}.tmp`, file);
|
|
@@ -2,10 +2,7 @@ import type { ForkSnapshot } from "@parity/product-lab/domain/versions";
|
|
|
2
2
|
import type { EnvironmentVariables, FileSystem, Logger, OwnedProcesses } from "@parity/product-lab/ports/system";
|
|
3
3
|
import { type ForkUpdateCommands } from "./fork-freshness.js";
|
|
4
4
|
import { type PublishedBundle } from "./published-bundle.js";
|
|
5
|
-
/**
|
|
6
|
-
* How long the check for a newer published state may take: it streams the bundle until its manifest (the 27
|
|
7
|
-
* September bundle: 8.8 MB read, 67 s at the 180 KB/s one machine had then). It runs in the background.
|
|
8
|
-
*/
|
|
5
|
+
/** How long the background release-record lookup may take. Archive bytes are read only by the download. */
|
|
9
6
|
export declare const PUBLISHED_CHECK_MS = 300000;
|
|
10
7
|
/** What a lab start tells about the published state (the CLI line and the lab window's notice). */
|
|
11
8
|
export declare function newerStateNotice(snapshot: Pick<ForkSnapshot, "bittenAt">, ready: boolean, commands?: ForkUpdateCommands): string;
|
|
@@ -29,8 +26,8 @@ export interface PrefetchDeps {
|
|
|
29
26
|
commands?: ForkUpdateCommands;
|
|
30
27
|
}
|
|
31
28
|
/**
|
|
32
|
-
* While the lab runs:
|
|
33
|
-
*
|
|
29
|
+
* While the lab runs: offer a newer local bite for refresh, or download the published asset in the background to
|
|
30
|
+
* compare its verified bite. Never throws; returns when it has decided (the
|
|
34
31
|
* download itself continues).
|
|
35
32
|
*/
|
|
36
33
|
export declare function prefetchNewerState(forkBite: Pick<ForkSnapshot, "bittenAt"> | undefined, deps: PrefetchDeps): Promise<PublishedBundle | undefined>;
|
|
@@ -68,7 +65,7 @@ export interface RefreshDeps<T = unknown> {
|
|
|
68
65
|
}
|
|
69
66
|
/**
|
|
70
67
|
* `--refresh`: set the saved fork aside and install the newest published state as the bundle a first start boots,
|
|
71
|
-
* downloading
|
|
68
|
+
* downloading a new or unknown digest to read its verified bite (offline: use a newer local bundle). Returns undefined when the fork already
|
|
72
69
|
* runs the newest published state.
|
|
73
70
|
*/
|
|
74
71
|
export declare function prepareRefresh<T>(directory: string, deps: RefreshDeps<T>): Promise<RefreshHandle | undefined>;
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { forkState, refreshCommand, shortError } from "./fork-freshness.js";
|
|
6
|
-
import { bittenAfter, bundleLocations, finishPrefetch, currentBundle, discardPrevious, engineReleaseSettings, installStagedBundle, prefetchActive, publishedBundle, restorePrevious, stagedBundle, startPrefetch, } from "./published-bundle.js";
|
|
6
|
+
import { bittenAfter, bundleLocations, bundleAssetDigest, finishPrefetch, currentBundle, discardPrevious, engineReleaseSettings, installStagedBundle, prefetchActive, publishedBundle, restorePrevious, stagedBundle, startPrefetch, } from "./published-bundle.js";
|
|
7
7
|
export const PUBLISHED_CHECK_MS = 300_000;
|
|
8
8
|
export function newerStateNotice(snapshot, ready, commands) {
|
|
9
9
|
const bitten = snapshot.bittenAt.slice(0, 16).replace("T", " ");
|
|
10
10
|
return ready
|
|
11
|
-
? `A newer published state of the network (bitten ${bitten} UTC) is downloaded
|
|
12
|
-
: `A newer published state of the network (bitten ${bitten} UTC) exists; downloading it in the background at low priority so ${refreshCommand(commands)}
|
|
11
|
+
? `A newer published state of the network (bitten ${bitten} UTC) is downloaded; ${refreshCommand(commands)} checks the newest published state before making a new local fork (this fork's identities are discarded).`
|
|
12
|
+
: `A newer published state of the network (bitten ${bitten} UTC) exists; downloading it in the background at low priority so its verified bite is available for ${refreshCommand(commands)}.`;
|
|
13
13
|
}
|
|
14
14
|
export async function prefetchNewerState(forkBite, deps) {
|
|
15
15
|
try {
|
|
@@ -62,7 +62,7 @@ async function reportKeptBundle(forkBite, deps) {
|
|
|
62
62
|
const kept = installed ?? forkBite;
|
|
63
63
|
const parts = [kept ? `bitten ${kept.bittenAt.slice(0, 16).replace("T", " ")} UTC, ${age(kept.bittenAt, Date.now())} old` : "no bundle installed yet"];
|
|
64
64
|
if (staged && (!kept || bittenAfter(staged, kept)))
|
|
65
|
-
parts.push(`a newer state (bitten ${staged.bittenAt.slice(0, 16).replace("T", " ")} UTC) is downloaded
|
|
65
|
+
parts.push(`a newer state (bitten ${staged.bittenAt.slice(0, 16).replace("T", " ")} UTC) is downloaded; ${refreshCommand(deps.commands)} checks the newest published state before making a new fork`);
|
|
66
66
|
parts.push(...problems);
|
|
67
67
|
const line = `Keeping the fork bundle of ${network} on this machine (${FORK_BUNDLE_SETTING}=keep; ${parts.join("; ")})`;
|
|
68
68
|
const asset = `fork-bundle-${network}.tar.gz`;
|
|
@@ -92,6 +92,21 @@ async function reportKeptBundle(forkBite, deps) {
|
|
|
92
92
|
else
|
|
93
93
|
logger.info(`${line}.`);
|
|
94
94
|
}
|
|
95
|
+
function publishedIsLocal(deps, published, staged, installed) {
|
|
96
|
+
const { next, current } = bundleLocations(deps.engine.workspace, deps.network);
|
|
97
|
+
return !!((staged && bundleAssetDigest(deps.files, next) === published.digest) || (installed && bundleAssetDigest(deps.files, current) === published.digest));
|
|
98
|
+
}
|
|
99
|
+
async function confirmedAssetDigest(deps, before) {
|
|
100
|
+
try {
|
|
101
|
+
const after = await publishedBundle({ files: deps.files, checkout: deps.engine.checkout, network: deps.network,
|
|
102
|
+
fetch: deps.fetch ?? fetch, env: deps.environment.all(), token: deps.environment.get("GITHUB_TOKEN"), signal: "signal" in deps ? deps.signal : undefined, timeoutMs: PUBLISHED_CHECK_MS });
|
|
103
|
+
return after.digest === before.digest ? before.digest : undefined;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
deps.logger.warn(`Could not confirm the downloaded release digest (${shortError(error)}); a later check may download again.`);
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
95
110
|
async function prefetchWhenNewer(forkBite, deps) {
|
|
96
111
|
const { engine, network, logger } = deps;
|
|
97
112
|
let published;
|
|
@@ -110,23 +125,25 @@ async function prefetchWhenNewer(forkBite, deps) {
|
|
|
110
125
|
logger.info(`Could not check for a newer published state of ${network} (${shortError(error)}); a later start checks again.`);
|
|
111
126
|
return undefined;
|
|
112
127
|
}
|
|
113
|
-
if (!
|
|
128
|
+
if (!forkBite)
|
|
114
129
|
return published;
|
|
115
130
|
const staged = stagedBundle(deps.files, engine.workspace, network);
|
|
116
131
|
const installed = currentBundle(deps.files, engine.workspace, network);
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
const ready = [staged, installed].filter((entry) => !!entry && bittenAfter(entry, forkBite))
|
|
133
|
+
.sort((a, b) => Date.parse(b.bittenAt) - Date.parse(a.bittenAt))[0];
|
|
134
|
+
if (ready) {
|
|
119
135
|
const text = newerStateNotice(ready, true, deps.commands);
|
|
120
136
|
logger.info(text);
|
|
121
137
|
deps.notice(text);
|
|
122
|
-
return published;
|
|
123
138
|
}
|
|
139
|
+
if (publishedIsLocal(deps, published, staged, installed))
|
|
140
|
+
return published;
|
|
124
141
|
const { partial } = bundleLocations(engine.workspace, network);
|
|
125
142
|
if (prefetchActive(deps.files, partial)) {
|
|
126
|
-
logger.info(`
|
|
143
|
+
logger.info(`The published state of ${network} is being downloaded by another lab on this machine.`);
|
|
127
144
|
return published;
|
|
128
145
|
}
|
|
129
|
-
const text =
|
|
146
|
+
const text = `Checking the published state of ${network}: downloading it in the background at low priority to read its bite time.`;
|
|
130
147
|
logger.info(`${text} (${Math.round(published.size / 1_000_000)} MB)`);
|
|
131
148
|
deps.notice(text);
|
|
132
149
|
try {
|
|
@@ -147,10 +164,17 @@ async function prefetchWhenNewer(forkBite, deps) {
|
|
|
147
164
|
return;
|
|
148
165
|
}
|
|
149
166
|
try {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
const digest = await confirmedAssetDigest(deps, published);
|
|
168
|
+
const snapshot = await finishPrefetch(deps.files, engine.workspace, network, undefined, digest);
|
|
169
|
+
if (bittenAfter(snapshot, forkBite)) {
|
|
170
|
+
const done = newerStateNotice(snapshot, true, deps.commands);
|
|
171
|
+
logger.info(done);
|
|
172
|
+
deps.notice(done);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
logger.info(`The downloaded state of ${network} (bitten ${snapshot.bittenAt}) is not newer than this fork.`);
|
|
176
|
+
deps.notice(undefined);
|
|
177
|
+
}
|
|
154
178
|
}
|
|
155
179
|
catch (error) {
|
|
156
180
|
logger.warn(`The background download of ${network}'s newer state is unusable (${shortError(error)}); a later start tries again.`);
|
|
@@ -172,7 +196,7 @@ export async function prepareRefresh(directory, deps) {
|
|
|
172
196
|
const saved = fork?.kind === "saved";
|
|
173
197
|
const forkBite = saved && fork.bittenAt ? { bittenAt: fork.bittenAt } : undefined;
|
|
174
198
|
let staged = stagedBundle(deps.files, engine.workspace, network);
|
|
175
|
-
|
|
199
|
+
let installed = currentBundle(deps.files, engine.workspace, network);
|
|
176
200
|
let published;
|
|
177
201
|
try {
|
|
178
202
|
const token = deps.environment.get("GITHUB_TOKEN");
|
|
@@ -184,15 +208,10 @@ export async function prepareRefresh(directory, deps) {
|
|
|
184
208
|
throw new Error(`--refresh could not read the newest published state (${shortError(error)}), and nothing newer than this fork is downloaded. Nothing was changed. Fix: connect to the internet and run it again.`);
|
|
185
209
|
logger.warn(`Could not check GitHub for a newer published state (${shortError(error)}); refreshing from the one downloaded (bitten ${local.bittenAt}).`);
|
|
186
210
|
}
|
|
187
|
-
|
|
188
|
-
if (saved && forkBite && newest && !bittenAfter(newest, forkBite)) {
|
|
189
|
-
logger.info(`--refresh: the fork already runs the newest published state (bitten ${forkBite.bittenAt}); nothing to refresh. Resuming it as it is.`);
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
if (published && (!staged || bittenAfter(published.snapshot, staged)) && (!installed || bittenAfter(published.snapshot, installed))) {
|
|
211
|
+
if (published && !publishedIsLocal(deps, published, staged, installed)) {
|
|
193
212
|
const registry = deps.processes.open(join(existsSync(directory) ? directory : dirname(directory), existsSync(directory) ? "processes" : "refresh-processes"));
|
|
194
213
|
try {
|
|
195
|
-
logger.info(`--refresh: downloading the newest published state (
|
|
214
|
+
logger.info(`--refresh: downloading the newest published state (${Math.round(published.size / 1_000_000)} MB; bite time is read after verification) now to check the rolling release before changing the saved fork.`);
|
|
196
215
|
const child = await startPrefetch({ engine, network, registry, environment: deps.environment, files: deps.files });
|
|
197
216
|
const onAbort = () => {
|
|
198
217
|
registry.stop("bundle-prefetch").catch((error) => logger.warn(`Could not stop the interrupted download: ${shortError(error)}`));
|
|
@@ -204,13 +223,23 @@ export async function prepareRefresh(directory, deps) {
|
|
|
204
223
|
deps.signal?.throwIfAborted();
|
|
205
224
|
if (code !== 0)
|
|
206
225
|
throw new Error(`--refresh: the download failed (exit ${code}; ${registry.logFile("bundle-prefetch")}). Nothing was changed.`);
|
|
207
|
-
await
|
|
226
|
+
const digest = await confirmedAssetDigest(deps, published);
|
|
227
|
+
await finishPrefetch(deps.files, engine.workspace, network, deps.signal, digest);
|
|
208
228
|
staged = stagedBundle(deps.files, engine.workspace, network);
|
|
229
|
+
installed = currentBundle(deps.files, engine.workspace, network);
|
|
209
230
|
}
|
|
210
231
|
finally {
|
|
211
232
|
registry.close();
|
|
212
233
|
}
|
|
213
234
|
}
|
|
235
|
+
const newest = [staged, installed].filter((entry) => !!entry)
|
|
236
|
+
.sort((a, b) => Date.parse(b.bittenAt) - Date.parse(a.bittenAt))[0];
|
|
237
|
+
if (saved && forkBite && newest && !bittenAfter(newest, forkBite)) {
|
|
238
|
+
logger.info(`--refresh: the fork already runs the newest published state (bitten ${forkBite.bittenAt}); nothing to refresh. Resuming it as it is.`);
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
if (!newest)
|
|
242
|
+
throw new Error("--refresh: no bundle is installed after the download; nothing to boot.");
|
|
214
243
|
if (saved) {
|
|
215
244
|
const registry = deps.processes.open(join(directory, "processes"));
|
|
216
245
|
try {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ForkSnapshot } from "@parity/product-lab/domain";
|
|
2
2
|
import type { EnvironmentVariables, FileSystem, OwnedProcess, OwnedProcesses } from "@parity/product-lab/ports/system";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 27 September after 15.5 MB of chain specs (about 4 MB compressed); the snapshots (hundreds of MB) come later.
|
|
4
|
+
* Legacy head-reading budget, retained for callers of the tar helpers. Lookup never reads archive bytes.
|
|
5
|
+
* @deprecated Use publishedBundle for release identity and verifyForkBundle for extracted bundles.
|
|
7
6
|
*/
|
|
8
7
|
export declare const MANIFEST_SEARCH_BYTES: number;
|
|
9
8
|
/** `KEY="${KEY:-default}"` lines of Engine's config/versions.env, resolved against `env` as Engine resolves them. */
|
|
@@ -18,28 +17,31 @@ export interface PublishedBundle {
|
|
|
18
17
|
/** The asset's download URL. */
|
|
19
18
|
url: string;
|
|
20
19
|
size: number;
|
|
21
|
-
/** GitHub's digest of the asset (`sha256:<hex>`)
|
|
22
|
-
digest
|
|
20
|
+
/** GitHub's validated SHA-256 digest of the complete asset (`sha256:<64 lowercase hex digits>`). */
|
|
21
|
+
digest: string;
|
|
23
22
|
/** When the asset was last uploaded. */
|
|
24
23
|
updatedAt: string;
|
|
25
|
-
/**
|
|
26
|
-
snapshot
|
|
24
|
+
/** Lookup does not read the archive. Read its snapshot from the verified installed or staged manifest instead. */
|
|
25
|
+
snapshot?: ForkSnapshot;
|
|
27
26
|
}
|
|
28
27
|
/**
|
|
29
28
|
* Scans a tar stream chunk by chunk for `manifest.json` (ustar headers), keeping only the bytes it still needs: the
|
|
30
29
|
* other entries' contents are skipped as they go by. `push` returns the parsed manifest once it is complete.
|
|
30
|
+
* @deprecated Use publishedBundle for release identity and verifyForkBundle for extracted bundles.
|
|
31
31
|
*/
|
|
32
32
|
export declare class TarManifestScanner {
|
|
33
33
|
#private;
|
|
34
34
|
push(chunk: Uint8Array): unknown;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Find `manifest.json` among a tar stream's first entries (ustar headers).
|
|
38
|
+
* @deprecated Use publishedBundle for release identity and verifyForkBundle for extracted bundles.
|
|
39
|
+
*/
|
|
37
40
|
export declare function manifestFromTarHead(tar: Uint8Array): unknown;
|
|
38
41
|
/**
|
|
39
|
-
* What Engine publishes for `network` now: the asset's record and
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* bundle for the network.
|
|
42
|
+
* What Engine publishes for `network` now: the release asset's record and SHA-256 identity, with no archive request.
|
|
43
|
+
* Rejects when GitHub cannot be reached, the release has no bundle for the network, or its digest is not SHA-256.
|
|
44
|
+
* The archive's entry order is irrelevant; read bite time, blocks and runtimes after downloading and verifying it.
|
|
43
45
|
*/
|
|
44
46
|
export declare function publishedBundle(input: {
|
|
45
47
|
files: Pick<FileSystem, "readText">;
|
|
@@ -49,7 +51,7 @@ export declare function publishedBundle(input: {
|
|
|
49
51
|
env?: Readonly<Record<string, string | undefined>>;
|
|
50
52
|
token?: string;
|
|
51
53
|
signal?: AbortSignal;
|
|
52
|
-
/** Bound of the
|
|
54
|
+
/** Bound of the release-record request (default 180 000 ms); on expiry it rejects. */
|
|
53
55
|
timeoutMs?: number;
|
|
54
56
|
}): Promise<PublishedBundle>;
|
|
55
57
|
/** Where a network's bundle lives in an Engine workspace, the staged newer one, and a download in progress. */
|
|
@@ -63,6 +65,8 @@ type BundleFiles = Pick<FileSystem, "exists" | "readJson" | "listDirectory" | "r
|
|
|
63
65
|
export declare function stagedBundle(files: BundleFiles, workspace: string, network: string): ForkSnapshot | undefined;
|
|
64
66
|
/** The bundle a new fork of this workspace would start from now. */
|
|
65
67
|
export declare function currentBundle(files: BundleFiles, workspace: string, network: string): ForkSnapshot | undefined;
|
|
68
|
+
/** The recorded release cache key; an old or damaged marker has an unknown digest. */
|
|
69
|
+
export declare function bundleAssetDigest(files: Pick<FileSystem, "readJson">, directory: string): string | undefined;
|
|
66
70
|
/** True when `a` was bitten after `b` (either missing: false). */
|
|
67
71
|
export declare function bittenAfter(a: Pick<ForkSnapshot, "bittenAt"> | undefined, b: Pick<ForkSnapshot, "bittenAt"> | undefined): boolean;
|
|
68
72
|
/** A download in progress in `partial`: a file there written within the last minute (another lab's prefetch). */
|
|
@@ -97,9 +101,9 @@ export declare function startPrefetch(input: {
|
|
|
97
101
|
* After the download exited 0: verify it complete and mark it (fork-bundle.ts), then stage it (`completePrefetch`). An
|
|
98
102
|
* incomplete download is removed and rejects, naming what is wrong; the staged and installed bundles are unchanged.
|
|
99
103
|
*/
|
|
100
|
-
export declare function finishPrefetch(files: BundleFiles, workspace: string, network: string, signal?: AbortSignal): Promise<ForkSnapshot>;
|
|
104
|
+
export declare function finishPrefetch(files: BundleFiles & Pick<FileSystem, "writePrivateJson">, workspace: string, network: string, signal?: AbortSignal, assetDigest?: string): Promise<ForkSnapshot>;
|
|
101
105
|
/** Move a finished, verified download into place as the staged bundle (replacing an older staged one). */
|
|
102
|
-
export declare function completePrefetch(files: BundleFiles, workspace: string, network: string): ForkSnapshot;
|
|
106
|
+
export declare function completePrefetch(files: BundleFiles & Pick<FileSystem, "writePrivateJson">, workspace: string, network: string): ForkSnapshot;
|
|
103
107
|
/**
|
|
104
108
|
* Make the staged bundle the one new forks start from. The current bundle is
|
|
105
109
|
* kept beside it (`<current>.previous`) until `discardPrevious`, so a failed
|