Repository navigation
Commit 9f9510f
fix(cloud-connection): reseed and purge refuse a protocol-incompatible install-local entry before any side effect (#21862)
Fixes #21834
Clause-②: no
## What changed
`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`
and `POST …/:manifestId/purge-sample-data` now run ADR-0087 D1's
handshake (`checkProtocolCompat`) on the ledger entry right after
reading it. An entry whose declared range excludes this runtime gets the
install route's answer for the same manifest: `422
OS_PROTOCOL_INCOMPATIBLE`, the error's own message, and the diagnostic's
five fields in `error.details`. The answer is shaped by the producer's
`protocolIncompatibleAnswer`, so there is no second copy of the shaping.
Nothing happens before that answer: no translation load, no seed-dataset
merge, no seed-row read or delete, and no ledger write.
- `packages/cloud-connection/src/marketplace-install-local-plugin.ts`:
one private helper, `refuseProtocolIncompatibleEntry`, called by both
doors right after `ledger.read`. Admission, the 404 and the
unreadable-ledger answer still come first, because there is no manifest
to judge before them. Only a positive incompatibility is refused. An
absent or unreadable range is admitted as before, with no new warning.
- No change to DELETE, the install route, the rehydrate or the listing.
No `packages/spec` path. No new error code.
- Changeset: `@objectstack/cloud-connection` `patch`, carrying the same
`Clause-②: no` line.
- `scripts/engine-double-contract.pinned.json`: one generated row
recording the new suite's `delete` double, which routes through
`assertEngineDeleteDispatch`. The gate prescribes this (`--write`). It
is not part of the claimed surface.
## Mechanism assumptions, measured on `origin/main` `5b2d189e`
- **H1, confirmed.** Neither `handleReseed` nor `handlePurge` called a
handshake. The reseed reached `applySideEffects`, whose step 1 loads
translations and step 2 merges seed datasets (and registers the
replayer) before the seed run.
- **H2, confirmed.** The install route refuses through
`assertProtocolCompat` + `protocolIncompatibleAnswer`.
`protocolIncompatibleAnswer` takes a `ProtocolIncompatibleError`, and
`checkProtocolCompat` returns the diagnostic. The helper builds the
producer's own error from that diagnostic (`new
ProtocolIncompatibleError(compat.diagnostic)`, the same construction
`assertProtocolCompat` throws) and answers through the shared helper.
Per the ruling, the check is `checkProtocolCompat`, not
`assertProtocolCompat`, so the doors add no grandfathering warning on
loadable entries.
- **H3, measured: the doors and the rehydrate's record can disagree.**
Suppose a protocol-incompatible entry is written to the ledger after
this boot's rehydrate, for example by another runtime sharing the
ledger. The GET listing then serves it as loaded: `withSampleData:
false`, no `notLoaded` marker, and its per-request seed-row `warn`. Both
doors answer it `422`, because they judge the entry themselves, as the
ruling directs. This was measured with a throwaway probe in the unit
harness, which was not committed. The doors' half is pinned (case 5
below). The listing's half is unchanged and noted under Acceptance
notes.
- **H4, measured.** In the reseed, both the organization wall's refusal
(`mode: 'refused'`, 403) and the `skipped` answer (`400 RESEED_SKIPPED`)
are decided inside `applySideEffects`, after steps 1 and 2. So the
handshake sits before that call. In the purge, the wall check precedes
the engine deletes and the ledger write, and the handshake sits before
both. Pinned: on a walled boot with no active organization, a refused
entry gets `422`, not `403`, and nothing moves (case 4).
## Tests (`92261ad5`)
- New unit suite
`packages/cloud-connection/src/marketplace-install-local-sample-data-not-loaded.test.ts`:
10 passed. It uses the real plugin `start()` + `kernel:ready` over a
pre-written ledger and the real `SeedLoaderService`, over an in-memory
engine that answers only for registered objects. Its probes are the
`i18n` service's `loadTranslations` call count, the length of the shared
`seed-datasets` list, the engine's writes, and the ledger directory's
bytes. The cases:
1. Precondition: the rehydrate refused the old entry and loaded the
current one.
2. Reseed on the refused entry: `422`, byte-identical to the install
route's answer for the same manifest. Every probe unchanged.
3. Purge on the refused entry: the same `422`, no delete,
`withSampleData` still `true`.
4. Walled, with no active organization: `422` ahead of the wall's `403`,
nothing moves. Control: the loadable entry meets the `403`.
5. An entry written after this boot's rehydrate is refused by both
doors.
6. DELETE still works after both refusals.
7. A compatible version installed over the refused entry: reseed `200`
(`skipped: 1`), purge `200` (`deleted: 1`).
8–10. Loadable entries answer as before. The reseed moves the probes (+2
translation loads, +1 dataset, ledger rewritten), which is the control
for every "unchanged" above. The purge deletes its row and rewrites the
ledger. A no-range entry is admitted with no `[protocol]` warning.
- New real-boot pin
`packages/qa/dogfood/test/install-local-sample-data-not-loaded.dogfood.test.ts`:
7 passed. It runs two showcase boots over one database file and one
ledger. Boot 1 installs CRM (28 rows), and a reseed there moves the i18n
probe. Between the boots, the CRM ledger entry is made to declare the
previous major. On boot 2 the rehydrate refuses it. Reseed and purge
both answer `422`, byte-identical to the install route's answer for the
manifest the ledger holds. The i18n service (same object, 0 loads), the
`seed-datasets` length and the ledger bytes are unchanged. A compatible
re-install over the entry answers `200`, after which reseed answers
`200` (`skipped: 28`, loads equal to boot 1's, +5 datasets) and purge
answers `200` (`deleted: 28`). DELETE then removes it. DELETE on a
still-refused entry at real boot is pinned by
`install-local-listing-not-loaded.dogfood.test.ts`.
- `@objectstack/cloud-connection` full suite: 41 files, 505 tests passed
(at `9f60b045`; the only later commit is the ledger JSON, which neither
package reads).
- `pnpm --filter @objectstack/cloud-connection typecheck` and `pnpm
--filter @objectstack/dogfood typecheck`: both exit 0. `--listFiles`
counts each new test file once in its program (cloud-connection's two
programs and dogfood's).
## Ablation (fix committed first; mutation through
`scripts/ablation-replace.mjs`, anchor hit 1, blob changed, restore
proven blob == HEAD and `git diff HEAD` empty)
The mutation makes the helper never refuse. The plugin is read from
source in both suites: a relative import in the unit suite, and the
dogfood config's `@objectstack/cloud-connection` source alias. So no
rebuild was needed for the mutation to reach the subject. The results
went red in the expected direction:
- Unit: 6 failed, 4 passed. The refusal cases failed and the
precondition and loadable cases held. The ablated reseed answered `400
RESEED_SKIPPED "Reseed did not run: seed-error: Object 'qa_old_account'
not found"`.
- Real boot: 3 failed, 4 passed. This reproduces the card's measurement.
Reseed answered `400 RESEED_SKIPPED "…Object 'crm_account' not found"`.
Purge answered `200 {deleted: 0, skipped: 0, errors: 28, withSampleData:
false}`. The probes moved: `translationLoads` 0 to 2, `seed-datasets` 19
to 24, and the ledger's `withSampleData` true to false and
`sampleDataPurged` false to true.
## Gates (`92261ad5`)
- `node scripts/pm/dispatch-gates.mjs --commands` re-derived on the
final head gave 75 commands. The union with the dispatch order's list
(including the full `pnpm lint`) is 76 commands, and all 76 exited 0.
`--ran`: "75 derived famil(ies) accounted for — 75 run, 0 NOT-MEASURED
(a DERIVED zero …)".
- `pnpm lint` (`eslint . --no-inline-config`, whole repo, not narrowed):
exit 0.
- A first pass at `9f60b045` had three non-zero exits, each resolved
before the pass above:
- `check:engine-double-contract` needed the generated ledger row
(committed in `92261ad5`).
- `check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: eight
unrelated packages had no `dist/`). It was built from the turbo cache
and then passed.
- `check-comment-mask-corpus` read a throwaway probe file I deleted
mid-run, so that reading was void. It is green on the clean tree.
## Acceptance notes
- **Listing vs doors on an entry written after boot (H3).** The listing
marks only what this boot's rehydrate refused, as its contract (#21822)
states, so an incompatible entry another runtime writes later is listed
as loaded. The doors refuse it. The listing is out of this card's
surface and is left unchanged. Not filed: it is not a breach of the
listing's stated contract, and its reach is a second runtime of another
protocol major sharing one ledger directory.
- **`seed-datasets` merge is append-only.** Every loadable reseed
appends the package's datasets again (measured +5 on the real boot over
a list that already held them). This is unchanged by this PR. It is an
observation without a measured wrong answer, so it is not filed.
- **Envelope wrap.** The install route keeps its inline `{ success:
false, error: { code, message, details } }` wrap, per the claim's "no
change to the install route". The two sample-data doors share one wrap
in the new helper. Folding the install route onto the helper is a
possible follow-up, and it would not change any answer.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a43d90a commit 9f9510f
5 files changed
Lines changed: 746 additions & 2 deletions
File tree
- .changeset
- packages
- cloud-connection/src
- qa/dogfood/test
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
Lines changed: 62 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
73 | | - | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
74 | 77 | | |
75 | 78 | | |
76 | 79 | | |
| |||
101 | 104 | | |
102 | 105 | | |
103 | 106 | | |
104 | | - | |
| 107 | + | |
| 108 | + | |
105 | 109 | | |
106 | 110 | | |
107 | 111 | | |
108 | 112 | | |
109 | 113 | | |
| 114 | + | |
110 | 115 | | |
111 | 116 | | |
112 | 117 | | |
| |||
1937 | 1942 | | |
1938 | 1943 | | |
1939 | 1944 | | |
| 1945 | + | |
| 1946 | + | |
| 1947 | + | |
| 1948 | + | |
| 1949 | + | |
1940 | 1950 | | |
1941 | 1951 | | |
1942 | 1952 | | |
| |||
1955 | 1965 | | |
1956 | 1966 | | |
1957 | 1967 | | |
| 1968 | + | |
| 1969 | + | |
| 1970 | + | |
| 1971 | + | |
1958 | 1972 | | |
1959 | 1973 | | |
1960 | 1974 | | |
| |||
2056 | 2070 | | |
2057 | 2071 | | |
2058 | 2072 | | |
| 2073 | + | |
| 2074 | + | |
| 2075 | + | |
| 2076 | + | |
| 2077 | + | |
2059 | 2078 | | |
2060 | 2079 | | |
2061 | 2080 | | |
| |||
2074 | 2093 | | |
2075 | 2094 | | |
2076 | 2095 | | |
| 2096 | + | |
| 2097 | + | |
| 2098 | + | |
| 2099 | + | |
2077 | 2100 | | |
2078 | 2101 | | |
2079 | 2102 | | |
| |||
2140 | 2163 | | |
2141 | 2164 | | |
2142 | 2165 | | |
| 2166 | + | |
| 2167 | + | |
| 2168 | + | |
| 2169 | + | |
| 2170 | + | |
| 2171 | + | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
| 2176 | + | |
| 2177 | + | |
| 2178 | + | |
| 2179 | + | |
| 2180 | + | |
| 2181 | + | |
| 2182 | + | |
| 2183 | + | |
| 2184 | + | |
| 2185 | + | |
| 2186 | + | |
| 2187 | + | |
| 2188 | + | |
| 2189 | + | |
| 2190 | + | |
| 2191 | + | |
| 2192 | + | |
| 2193 | + | |
| 2194 | + | |
| 2195 | + | |
| 2196 | + | |
| 2197 | + | |
| 2198 | + | |
| 2199 | + | |
| 2200 | + | |
| 2201 | + | |
| 2202 | + | |
2143 | 2203 | | |
2144 | 2204 | | |
2145 | 2205 | | |
| |||
0 commit comments