diff --git a/docs/atlas/catalog.json b/docs/atlas/catalog.json
index 13c5cdcf..9192144a 100644
--- a/docs/atlas/catalog.json
+++ b/docs/atlas/catalog.json
@@ -1 +1 @@
-{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9","7f62ef13b4b46b4f23cae9536c977c15c85320a253ed2bc609eb7a1f0afdcf56","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","be492023350541da5256d6f653711b5f736d5216cf121a7e02ca43e1887215e4","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"}
+{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","5466c617291eff27f59b569a2d42a18dc0c134d4935d52d802c5a71b8a594281","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9","7f62ef13b4b46b4f23cae9536c977c15c85320a253ed2bc609eb7a1f0afdcf56","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","be492023350541da5256d6f653711b5f736d5216cf121a7e02ca43e1887215e4","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"}
diff --git a/docs/atlas/records/5466c617291eff27f59b569a2d42a18dc0c134d4935d52d802c5a71b8a594281.json b/docs/atlas/records/5466c617291eff27f59b569a2d42a18dc0c134d4935d52d802c5a71b8a594281.json
new file mode 100644
index 00000000..659f2cd9
--- /dev/null
+++ b/docs/atlas/records/5466c617291eff27f59b569a2d42a18dc0c134d4935d52d802c5a71b8a594281.json
@@ -0,0 +1 @@
+{"citations":[{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"d492d37b711d0767db8179d84a062d6a6107c990","sha256":"dd2504b52a261ceba43671ad411323417b4063d3405a9e810badcbdacfb9e636"},{"path":"scripts/wasm-release/bind-installed-release.py","repository":"FS-GG/FS.GG.Game","revision":"d492d37b711d0767db8179d84a062d6a6107c990","sha256":"902739145ec2a456c08420edff822a4dcafb7a375931a19e104d584a97a5dbf8"},{"path":"scripts/wasm-release/installed-evidence.py","repository":"FS-GG/FS.GG.Game","revision":"d492d37b711d0767db8179d84a062d6a6107c990","sha256":"b7b71f15d8a2ea33934d9165b74d40e892e73b55d1f371656b234f61a4e9b53d"},{"path":"tests/release/wasm/test-installed-evidence.py","repository":"FS-GG/FS.GG.Game","revision":"d492d37b711d0767db8179d84a062d6a6107c990","sha256":"fc4e97a0be7da935df29a11e256d2e51358e5b447a0c82d8b5e1c18218fce988"},{"path":"tests/release/wasm/test-installed-org-workflow.py","repository":"FS-GG/FS.GG.Game","revision":"d492d37b711d0767db8179d84a062d6a6107c990","sha256":"c57fd29abcbe9e87e6848c3835751bf35333bae61a2df01c2db027dba7df8ff3"},{"path":"tests/release/wasm/test-release-promotion.py","repository":"FS-GG/FS.GG.Game","revision":"d492d37b711d0767db8179d84a062d6a6107c990","sha256":"e0655c8274b4d9eba1ae53709324e037fa6e2d0985b0039f4692add6cd7cf3a3"},{"path":"scripts/verify-wasm-package-consumer.sh","repository":"FS-GG/FS.GG.Game","revision":"8a5e63ba50d4b3cb7b5a66bb4136990d5589ee9e","sha256":"31d98f934faee58ade6b84ed776bfd344435601b3a5f6741877e45781ce953ac"},{"path":"scripts/wasm-release/qualify-supervisor-installed.sh","repository":"FS-GG/FS.GG.Game","revision":"8a5e63ba50d4b3cb7b5a66bb4136990d5589ee9e","sha256":"a45fb4432a367edf631d8a013ff85022d57a46c84d2fec848fd37053023b8893"},{"path":".github/workflows/wasm-installed-org.yml","repository":"FS-GG/FS.GG.Game","revision":"8a5e63ba50d4b3cb7b5a66bb4136990d5589ee9e","sha256":"5d9a89a860e6af539ce4c5e99add2a2d5cc50e217ae36b720a2b8f90a9434221"},{"path":"sdk/wasm/build-source-archive.sh","repository":"FS-GG/FS.GG.Game","revision":"8a5e63ba50d4b3cb7b5a66bb4136990d5589ee9e","sha256":"24dfc2ef6f8ed0a0c029980e25c2ee578beb0cca7abe22964a2fe2bc3cc9ce09"},{"path":"scripts/wasm-release/promotion.py","repository":"FS-GG/FS.GG.Game","revision":"8a5e63ba50d4b3cb7b5a66bb4136990d5589ee9e","sha256":"d2ea88920b22e30df973765085c92116685dda689616045bb3b01080bd5d848d"},{"path":".github/workflows/release-wasm.yml","repository":"FS-GG/FS.GG.Game","revision":"8a5e63ba50d4b3cb7b5a66bb4136990d5589ee9e","sha256":"8a914479ddb212047706e6ec51440a7425c68df53a8cb5a25c8f4d14cc8fdf17"}],"conflictsWith":[],"id":"game/current-wasm-custody-source-boundary","kind":"boundary","recordedAt":"2026-10-18T02:17:20Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Immutable published0.6 remains P8a5e63ba50d4b3cb7b5a66bb4136990d5589ee9e/tree090906c22f0ef0cd3ca453a28ca20ce8686e0753, protected39084881928/artifact12120591664 and accepted publisher39085989107/a1/release416167085/transactionfa7a48d1b389116b18f00b5ec94dd83e82bfd3fd9e4032fe052eb13eab019119. Manifest e40258fe5cf6471c868afe0f6405d482a5a60ccfdb192f8cdb36b36adf08b8ff and SDK b06bdd4a75e9f9727f58cdbeb272e42f1c88607f2326152c67feb5dc5aeccd6f remain exact P originals. The served SDK contains SDK/examples, not installed collector/binder/qualification helpers; changed Q collection code must never be compared to, inserted into or repacked as P SDK payload.\n\nPR724 delivered Q025cbfd4b3efed0a3baf87c52ebae404124cd814/tree4fc6eb4b9d745d1f0869fa32a411ea00a7f09448 after accepted source original39086999965/a1. First selected served-feed installed39088631914/a1 then failed both feeds. Each actual consumer compiled and passed36 browser cases; authoritative consumer status is runtime0/collection1/directcleanup0/descendantsunproved. Selected-supervisor qualification was unreached. The first recorded evidence refusal is actual Fable library Array.js trying to join nonexistent fs.gg.wasm.browser.0.5.0.nupkg. Fixed5 names also affected package cache, Fable package modules, SDK, merge/assembly and authenticated receipt equality. Existing trap correctly preserved collection failure after runtime success. Native artifacts org12122285091/SHAf79eefd4ec6cdb91f4e14fa641ec29e0777c68f859019dd5d72aa00993feb792 and public12121452710/SHAa5541e625500eba0112c7edeff5db6e6c3462bcc1b20b9762df637f1409a7f1d retain204members each and are ineligible. Independent failed-original review corroborates exact native identities, statuses and representative published Worker bytes; no installed acceptance or first-cause substitution follows. Original failure is consumed, not retried; its temporary source hold ended only after actual terminal/readback disposition.\n\nThe selected finite qualifier correction derives one unambiguous stable VERSION from audited tracked version.props, without environment/CLI selectors, stale-cache inference or old5 fallback. One SDK_ARCHIVE and exact current package/module/cache names are used throughout consumer, selected original-member joins, Fable roles, merge and assembled receipt equality. Fable5.18, Core official TFM roles, exact module stems/contexts, mandatory originals, byte equality, regular/single-link files and512MiB total/256MiB file/32MiB logs/1MiB metadata/1024member bounds remain unchanged. All producer/runtime/version.props/SDK/workflow/promotion/pin inputs remain P-identical; collector/binder qualification inputs belong to selected Q, not P.\n\nAuthority is the existing immutable P/tree, ancestry, complete raw no-rename path/mode/blob/hash audit and actual clean current Q=GITHUB_SHA=GITHUB_WORKFLOW_SHA=ACCEPTED_EXECUTOR/QUALIFIER_SOURCE on genuine main/manual-event/attempt1/installed job/feed. The finite P-to-successor-Q closure retains the prior seven exact leaves, adds only installed-evidence.py/test-installed-evidence.py and this exact new capture leaf. No prefix/glob, arbitrary descendant, historical executor overlay, publisher/repack or runtime permission is admitted. Source audit establishes correspondence; independent exact-source/root review and actual native admission still establish the selected invocation.\n\nActual current CLI consumer→sidecar/tar→merge→selected→assemble controls use unchanged maintained script bytes and declared synthetic archives/runtime-shaped files, never executed fixtures. They exercise original Array.js/current archive provenance, hostile5 environment, old-only cache/Fable/SDK refusals and unequal authenticated SDK bytes. The actual EXIT trap invokes the real collector, deletes only private fixture work and preserves runtime/collection/cleanup/merged eligibility; independent controlled failure ports retain runtime-first, then collection, then cleanup and command/log priorities. Real source_audit controls cover exact collector/test positive, missing allowlist entry, excluded version/runtime/SDK/workflow/promotion/arbitrary leaves, mode/delete/rename/link and wrong P/tree. Affected source-mapping/workflow and18inert collector tests pass. Early broad local preflight on a dirty tree and extra256MiB address-space ceiling refused; clean unchanged37promotion controls subsequently passed in1.58s/~42MiB peak RSS with a compatible local data-read ceiling. No native collector/payload cap was raised.\n\nWhole connected source review passes for this prepared correction; exact final capture/leaf audit and root authority review precede ordinary source admission. Fresh current source compilation/coherentCI, qualified source delivery and separately selected both-feed installed operation remain required; no unchanged rerun is selected. Automatic protectedQ39088390588 remains independent evidence. Last accepted served installed producer remains0.5 Qc938/original39076218708; current0.6 publication/source/consumer runtime passes do not establish installed0.6, product/native/image/wholeS1, descendant settlement, old timeout cause or future timing fit. Original logs/archives stay in restricted custody rather than Atlas.","supersedes":["be492023350541da5256d6f653711b5f736d5216cf121a7e02ca43e1887215e4"],"tags":["wasm","qualification","current-only","custody","collector","publication","first-cause"],"title":"Keep published 0.6 originals immutable while repairing finite current qualifier evidence"}
diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md
index e9372428..a5ed3d80 100644
--- a/docs/roadmaps/wasm-shared-01.md
+++ b/docs/roadmaps/wasm-shared-01.md
@@ -57,15 +57,47 @@ and original promotion-binding SHA-256 is
`fa7a48d1b389116b18f00b5ec94dd83e82bfd3fd9e4032fe052eb13eab019119`.
The source hold ended after accepted original terminal/readback evidence.
-The finite read-only installed binder now selects only this published 0.6 P/tree.
-The selected qualifier Q must be the actual clean current main/dispatch executor,
-with the complete closed P-to-Q path audit and genuine original publication
-receipts. Runtime, build, workflow and profile bytes remain unchanged. Historical
-0.5/0.4 installed successes stay immutable evidence, not alternative current
-execution routes. Qualifier source CI/delivery and a separately selected fresh
-org/public installed operation remain required. Last accepted installed remains
-0.5 at Q `c938e9158542e3a0572b62dd09c01ad01cbb344f`; publication alone accepts
-no installed 0.6, SC2 adoption, image/native workload or product outcome.
+The finite read-only installed binder selects only this published 0.6 P/tree.
+[PR724](https://github.com/FS-GG/FS.GG.Game/pull/724) delivered qualifier
+`025cbfd4b3efed0a3baf87c52ebae404124cd814`, with exact tested/delivered tree
+`4fc6eb4b9d745d1f0869fa32a411ea00a7f09448`. Its original source qualification
+39086999965/attempt1 passed50 Lifecycle controls,36 browser cases and the final
+default53/selected16 compiled cohorts. That source result did not exercise the
+feed-only evidence projection.
+
+The first selected served-feed operation,
+[39088631914/attempt1](https://github.com/FS-GG/FS.GG.Game/actions/runs/39088631914),
+failed on both feeds. Each actual consumer compiled and passed36 browser cases;
+the consumer's recorded runtime was0, collection1 and direct cleanup0, with
+descendant settlement unproved. The first recorded projection refusal concerns
+the actual Fable library `Array.js`: the collector requested an obsolete
+`fs.gg.wasm.browser.0.5.0.nupkg` original. The same duplicated version assumption
+also affected package-cache, Fable package-module, SDK and assembled receipt
+paths. The existing shell trap preserved the runtime/collection distinction and
+exited1 on collection failure; the selected-supervisor stage was not reached.
+Both ineligible original artifacts and native failures remain retained. Neither
+the successful consumer runtime nor the later failed collector is installed
+acceptance, and no unchanged retry or package replacement is selected.
+
+The prepared collector correction derives one current namespace from the audited
+tracked `version.props`, with stable-version validation and no environment/CLI
+version override or historical fallback. Consumer, selected dependency originals,
+Fable roles, SDK, merge and authenticated receipt joins use that same input.
+Existing mandatory roles, byte joins, link/census/size refusals and first-cause
+ordering remain. Inert controls run the maintained consumer and assemble CLI
+actions against a declared synthetic file topology, including current dependency
+bytes, stale-cache refusal and a mismatched authenticated SDK receipt. These are
+projection controls; they execute no fixture runtime or installed consumer.
+
+Any successor qualifier must be the actual clean current main/dispatch executor,
+with a reviewed finite P-to-Q file/blob audit and genuine original publication
+receipts. Collector/test changes require an explicit bounded qualifier closure;
+the earlier seven-file binder does not authorize them. Runtime, workflow, package
+pins and profile remain unchanged. Historical0.5/0.4 successes remain immutable
+evidence, not execution alternatives. Fresh source qualification and a separately
+selected installed operation remain required. Last accepted installed remains0.5
+at Q `c938e9158542e3a0572b62dd09c01ad01cbb344f`; installed0.6, image/native and
+whole-product acceptance remain open.
### First 0.6 source qualification and exact follower-expiry proof
diff --git a/scripts/wasm-release/bind-installed-release.py b/scripts/wasm-release/bind-installed-release.py
index 9ad0dff9..ae5beb06 100644
--- a/scripts/wasm-release/bind-installed-release.py
+++ b/scripts/wasm-release/bind-installed-release.py
@@ -19,12 +19,15 @@
# for arbitrary descendants or semantic changes inside a broad scripts prefix.
INSTALLED_PATHS = frozenset({
'scripts/wasm-release/bind-installed-release.py',
+ 'scripts/wasm-release/installed-evidence.py',
+ 'tests/release/wasm/test-installed-evidence.py',
'tests/release/wasm/test-installed-org-workflow.py',
'tests/release/wasm/test-release-promotion.py',
'docs/roadmaps/wasm-shared-01.md',
'docs/atlas/catalog.json',
'docs/atlas/records/7f62ef13b4b46b4f23cae9536c977c15c85320a253ed2bc609eb7a1f0afdcf56.json',
'docs/atlas/records/be492023350541da5256d6f653711b5f736d5216cf121a7e02ca43e1887215e4.json',
+ 'docs/atlas/records/5466c617291eff27f59b569a2d42a18dc0c134d4935d52d802c5a71b8a594281.json',
})
@@ -90,7 +93,7 @@ def fingerprint(revision, blob):
assert rows, 'installed correction must be an explicit reviewed source change'
return {'schema':'fsgg.wasm.installed-inputs/v1','publishedSource':source,'publishedTree':PUBLISHED_TREE,
'qualifierSource':qualifier,'qualifierTree':git('rev-parse',qualifier+'^{tree}'),'changes':rows,
- 'consumerInputs':'all paths outside exact reviewed repair closure byte-identical'}
+ 'consumerInputs':'all paths outside exact reviewed qualifier correction byte-identical to P; published packages/SDK remain P inputs, evidence collector/binder are selected Q inputs'}
def bind_executor(source, executor, version):
diff --git a/scripts/wasm-release/installed-evidence.py b/scripts/wasm-release/installed-evidence.py
index 8b0dad4d..264cd82f 100644
--- a/scripts/wasm-release/installed-evidence.py
+++ b/scripts/wasm-release/installed-evidence.py
@@ -6,11 +6,22 @@
import json
import os
import pathlib
+import re
import stat
import tarfile
+import xml.etree.ElementTree as ET
import zipfile
ROOT = pathlib.Path(__file__).resolve().parents[2]
+# The qualifier's audited source fixes this input; callers cannot select an old
+# cache namespace through an environment variable or a second version literal.
+_versions = ET.parse(ROOT/'eng/wasm-shared/version.props').findall('.//WasmSharedVersion')
+if len(_versions) != 1:
+ raise ValueError('ambiguous tracked current WASM version')
+VERSION = _versions[0].text
+if not isinstance(VERSION, str) or not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', VERSION):
+ raise ValueError('invalid tracked current WASM version')
+SDK_ARCHIVE = f'fsgg-wasm-sdk-{VERSION}.tar.gz'
MIB = 1024 * 1024
LIMITS = dict(total=512*MIB, file=256*MIB, logs=32*MIB, metadata=MIB, members=1024)
MANIFEST_RESERVE = 256 * 1024
@@ -50,10 +61,10 @@ def fable_dependency(name):
return ('library-runtime', 'fs.gg.wasm.browser',
'contentFiles/any/any/_content/FS.GG.Wasm.Browser/policy/fable_modules/'+relative)
modules = {
- 'FS.GG.Wasm.Browser.0.5.0': ('fs.gg.wasm.browser',
+ f'FS.GG.Wasm.Browser.{VERSION}': ('fs.gg.wasm.browser',
('Admission', 'Host', 'Invocation', 'Lifecycle', 'RuntimeProtocol', 'WorkerEntry'),
'FS.GG.Wasm.Browser'),
- 'FS.GG.Wasm.Contracts.0.5.0': ('fs.gg.wasm.contracts', ('Contracts',), 'FS.GG.Wasm.Contracts')}
+ f'FS.GG.Wasm.Contracts.{VERSION}': ('fs.gg.wasm.contracts', ('Contracts',), 'FS.GG.Wasm.Contracts')}
if module not in modules: raise ValueError('unreviewed Fable dependency module')
package, stems, project = modules[module]
if leaf in {stem+suffix for stem in stems for suffix in ('.fs', '.fsi')}:
@@ -70,10 +81,10 @@ def fable_dependency(name):
def consumer_member(name):
safe_name(name)
fixed={'consumer/release-manifest.json','consumer/NuGet.Config','consumer/consumer/packages.lock.json',
- 'consumer/fsgg-wasm-sdk-0.5.0.tar.gz','consumer/installed-member-receipt.log','consumer/browser-result.json','consumer/http.log','consumer/public/sub/app/Consumer'}
+ 'consumer/'+SDK_ARCHIVE,'consumer/installed-member-receipt.log','consumer/browser-result.json','consumer/http.log','consumer/public/sub/app/Consumer'}
if name in fixed:return True
for ident in PACKAGES:
- if name in {'consumer/packages/'+ident+'/'+leaf for leaf in (ident+'.0.5.0.nupkg',ident+'.0.5.0.nupkg.sha512','.nupkg.metadata')}:return True
+ if name in {'consumer/packages/'+ident+'/'+leaf for leaf in (f'{ident}.{VERSION}.nupkg',f'{ident}.{VERSION}.nupkg.sha512','.nupkg.metadata')}:return True
if name in {'consumer/core/'+leaf for leaf in ('fsharp.core.10.1.302.nupkg','fsharp.core.10.1.302.nupkg.sha512','.nupkg.metadata','reference-netstandard2.0.dll','reference-netstandard2.1.dll')}:return True
if '/fable_modules/' in name:
context = name.split('/fable_modules/', 1)[0]
@@ -130,7 +141,7 @@ def dependency(self, name):
raise ValueError('pinned Fable library metadata drift')
return
context = 'consumer' if name.startswith('consumer/') else 'selected'
- original = self.files[context+'/packages/'+package+'/'+package+'.0.5.0.nupkg']
+ original = self.files[context+'/packages/'+package+'/'+f'{package}.{VERSION}.nupkg']
with zipfile.ZipFile(io.BytesIO(original)) as archive:
rows = [row for row in archive.infolist() if row.filename == member]
if len(rows) != 1 or rows[0].is_dir() or rows[0].file_size > LIMITS['file']:
@@ -174,8 +185,8 @@ def tree(self, path, prefix):
self.errors.append({'member':prefix,'status':'refused','reason':str(error)})
def packages(self, cache, prefix):
for ident in PACKAGES:
- base=pathlib.Path(cache)/ident/'0.5.0'
- for leaf in (ident+'.0.5.0.nupkg',ident+'.0.5.0.nupkg.sha512','.nupkg.metadata'):
+ base=pathlib.Path(cache)/ident/VERSION
+ for leaf in (f'{ident}.{VERSION}.nupkg',f'{ident}.{VERSION}.nupkg.sha512','.nupkg.metadata'):
self.add(base/leaf,prefix+'/'+ident+'/'+leaf)
def core(self, cache, prefix):
base=pathlib.Path(cache)/'fsharp.core/10.1.302'
@@ -228,7 +239,7 @@ def consumer(projection, work):
work=pathlib.Path(work)
for name in ('release-manifest.json','NuGet.Config','consumer/packages.lock.json'):
projection.add(work/name,'consumer/'+name)
- projection.add(work/'sdk-artifacts/fsgg-wasm-sdk-0.5.0.tar.gz','consumer/fsgg-wasm-sdk-0.5.0.tar.gz')
+ projection.add(work/'sdk-artifacts'/SDK_ARCHIVE,'consumer/'+SDK_ARCHIVE)
projection.add(work/'installed-member-receipt.log','consumer/installed-member-receipt.log')
projection.add(work/'consumer-http.log','consumer/http.log')
projection.packages(work/'consumer-packages','consumer/packages')
@@ -324,11 +335,11 @@ def main():
if args.action=='consumer': consumer(p,args.work)
else:
merge(p,args.consumer); selected(p,args.work)
- for name in ('release-manifest.json','promotion-binding.json','installed-input-audit.json','published-native.json','fsgg-wasm-sdk-0.5.0.tar.gz'):
+ for name in ('release-manifest.json','promotion-binding.json','installed-input-audit.json','published-native.json',SDK_ARCHIVE):
p.add(pathlib.Path(args.receipts)/name,'receipts/'+name)
for name in ('consumer.log','selected.log','runtime-status.json','consumer-status.json'):
p.add(pathlib.Path(args.logs)/name,'execution/'+name)
- for consumer_name,receipt_name in [('consumer/release-manifest.json','receipts/release-manifest.json'),('consumer/fsgg-wasm-sdk-0.5.0.tar.gz','receipts/fsgg-wasm-sdk-0.5.0.tar.gz')]:
+ for consumer_name,receipt_name in [('consumer/release-manifest.json','receipts/release-manifest.json'),('consumer/'+SDK_ARCHIVE,'receipts/'+SDK_ARCHIVE)]:
if consumer_name not in p.files or receipt_name not in p.files or p.files[consumer_name]!=p.files[receipt_name]:
p.errors.append({'member':consumer_name,'status':'absent/refused','reason':'consumer original differs from authenticated publication receipt'})
try:
diff --git a/tests/release/wasm/test-installed-evidence.py b/tests/release/wasm/test-installed-evidence.py
index 257f4d67..c4532261 100644
--- a/tests/release/wasm/test-installed-evidence.py
+++ b/tests/release/wasm/test-installed-evidence.py
@@ -11,9 +11,11 @@
import tarfile
import tempfile
import unittest
+import xml.etree.ElementTree as ET
from unittest.mock import patch
sys.dont_write_bytecode=True
ROOT=pathlib.Path(__file__).resolve().parents[3]
+CURRENT=ET.parse(ROOT/'eng/wasm-shared/version.props').findtext('.//WasmSharedVersion')
spec=importlib.util.spec_from_file_location('evidence',ROOT/'scripts/wasm-release/installed-evidence.py')
e=importlib.util.module_from_spec(spec);spec.loader.exec_module(e)
spec=importlib.util.spec_from_file_location('binder',ROOT/'scripts/wasm-release/bind-installed-release.py')
@@ -24,18 +26,160 @@ def put(path, body=b'actual control bytes'):
path.parent.mkdir(parents=True,exist_ok=True);path.write_bytes(body);return path
+def current_cli_topology(root):
+ """Inert files for the actual two CLI actions; no file is executed as code.
+
+ The maintained collector bytes and tracked version are copied unchanged to
+ an isolated source root so selected() reads its real fixed runtime paths.
+ This proves projection/caller composition, never installed/runtime proof.
+ """
+ source=root/'source';script=source/'scripts/wasm-release/installed-evidence.py'
+ put(script,(ROOT/'scripts/wasm-release/installed-evidence.py').read_bytes())
+ put(source/'eng/wasm-shared/version.props',(ROOT/'eng/wasm-shared/version.props').read_bytes())
+ work=root/'consumer-work';selected=root/'selected';receipts=root/'receipts';logs=root/'logs'
+ manifest=json.dumps({'version':CURRENT}).encode();sdk=b'inert current SDK fixture'
+ for name,body in (('release-manifest.json',manifest),('NuGet.Config',b''),
+ ('consumer/packages.lock.json',b'{}'),('installed-member-receipt.log',b'fixture'),
+ ('consumer-http.log',b'fixture'),('consumer/browser/test-results/.last-run.json',b'{"status":"passed"}')):
+ put(work/name,body)
+ put(work/'sdk-artifacts'/f'fsgg-wasm-sdk-{CURRENT}.tar.gz',sdk)
+ for name in ('release-manifest.json','promotion-binding.json','installed-input-audit.json','published-native.json'):
+ put(receipts/name,manifest if name=='release-manifest.json' else b'{}')
+ put(receipts/f'fsgg-wasm-sdk-{CURRENT}.tar.gz',sdk)
+ put(root/'wasm-installed-receipts/release-manifest.json',manifest)
+ put(root/'wasm-installed-receipts/promotion-binding.json',b'{}')
+ archives={}
+ for ident in e.PACKAGES:
+ out=io.BytesIO()
+ with zipfile.ZipFile(out,'w') as z:
+ if ident=='fs.gg.wasm.browser':
+ z.writestr('fable/Host.fs',b'current Host source fixture')
+ z.writestr('contentFiles/any/any/_content/FS.GG.Wasm.Browser/policy/fable_modules/fable-library-js.5.18.0/Array.js',b'current library fixture')
+ else:z.writestr('fable/Contracts.fs',b'current Contracts source fixture')
+ archives[ident]=out.getvalue()
+ for cache in (work/'consumer-packages',selected/'packages'):
+ for ident,body in archives.items():
+ for name,data in ((f'{ident}.{CURRENT}.nupkg',body),(f'{ident}.{CURRENT}.nupkg.sha512',b'fixture'),('.nupkg.metadata',b'{}')):
+ put(cache/ident/CURRENT/name,data)
+ for name in ('fsharp.core.10.1.302.nupkg','fsharp.core.10.1.302.nupkg.sha512','.nupkg.metadata',
+ 'lib/netstandard2.0/FSharp.Core.dll','lib/netstandard2.1/FSharp.Core.dll'):
+ put(cache/'fsharp.core/10.1.302'/name)
+ public=work/'consumer/public/sub/app'
+ for name in ('FSharp.Core.dll','Consumer.dll','Consumer','fable/Program.js'):
+ put(public/name)
+ for context in (public,root/'selected-browser'):
+ for name in ('module-worker.mjs','worker-client.mjs','worker-build.json','policy/WorkerEntry.js'):
+ put(context/'_content/FS.GG.Wasm.Browser'/name)
+ put(context/'_content/FS.GG.Wasm.Browser/policy/fable_modules/fable-library-js.5.18.0/Array.js',b'current library fixture')
+ for context in (public/'fable',selected/'installed-fable',root/'selected-browser/fable'):
+ put(context/'Program.js');put(context/'BrowserBridge.js')
+ put(context/f'fable_modules/FS.GG.Wasm.Browser.{CURRENT}/Host.fs',b'current Host source fixture')
+ put(context/f'fable_modules/FS.GG.Wasm.Contracts.{CURRENT}/Contracts.fs',b'current Contracts source fixture')
+ for name in ('limits-guest.wasm','http.log'):put(root/'selected-browser'/name)
+ for name in (*e.LOGS,'installed.packages.lock.json','installed.bad-hash.lock.json','official-core-joins.json'):
+ put(selected/name,b'{}' if name.endswith('.json') else b'fixture')
+ put(root/'selected-NuGet.Config',b'')
+ baseline=root/'selected-baseline'
+ for name in ('receipt.json','fsharp.core.10.1.302.nupkg','fsharp.core.10.1.401.nupkg','fsharp.core.10.1.203.nupkg',
+ 'FSharp.Core.10.1.302.netstandard2.0.dll','FSharp.Core.10.1.302.netstandard2.1.dll',
+ 'FSharp.Core.10.1.401.netstandard2.0.dll','FSharp.Core.10.1.401.netstandard2.1.dll','FSharp.Core.10.1.203.netstandard2.1.dll'):
+ put(baseline/'official-core'/name,b'{}' if name.endswith('.json') else b'fixture')
+ put(baseline/'compiler/FSharp.Core.dll')
+ put(baseline/'tools/.store/fable/5.18.0/fable/5.18.0/tools/net10.0/any/FSharp.Core.dll')
+ put(source/'tests/Wasm.Supervisor.Compatibility/browser/test-results/.last-run.json',b'{"status":"passed"}')
+ for name in ('InstalledCompatibility.dll','InstalledCompatibility.deps.json','InstalledCompatibility.runtimeconfig.json','FSharp.Core.dll'):
+ put(source/'tests/Wasm.Supervisor.Compatibility/Installed/bin/Debug/net10.0'/name)
+ for name,body in (('consumer.log',b'fixture'),('selected.log',b'fixture'),('runtime-status.json',b'{"exitCode":0}'),
+ ('consumer-status.json',b'{"runtime":0,"collection":0,"cleanup":0,"descendants":"unproved"}')):
+ put(logs/name,body)
+ return script,work,selected,receipts,logs
+
+
class Evidence(unittest.TestCase):
+ def test_actual_current_cli_consumer_merge_and_assemble(self):
+ self.assertEqual(e.VERSION,CURRENT)
+ for defect in (None,'old-cache-only','different-authenticated-sdk'):
+ with self.subTest(defect=defect),tempfile.TemporaryDirectory() as raw:
+ root=pathlib.Path(raw);script,work,selected,receipts,logs=current_cli_topology(root)
+ if defect=='old-cache-only':
+ for ident in e.PACKAGES:
+ folder=work/'consumer-packages'/ident/CURRENT
+ folder.rename(folder.with_name('0.5.0'))
+ for leaf in folder.with_name('0.5.0').iterdir():
+ if CURRENT in leaf.name:leaf.rename(leaf.with_name(leaf.name.replace(CURRENT,'0.5.0')))
+ environment=dict(os.environ,RUNNER_TEMP=raw,RELEASE_VERSION='0.5.0',WASM_CANDIDATE_VERSION='0.5.0')
+ consumer=subprocess.run([sys.executable,str(script),'consumer','--work',str(work),'--output',str(root/'consumer-output'),'--runtime','0'],env=environment,capture_output=True,text=True)
+ manifest=json.loads((root/'consumer-output/evidence-manifest.json').read_text())
+ if defect=='old-cache-only':
+ self.assertEqual(consumer.returncode,1);self.assertFalse(manifest['eligible'])
+ self.assertTrue(any(f'.{CURRENT}.nupkg' in row['member'] for row in manifest['errors']))
+ continue
+ self.assertEqual(consumer.returncode,0,consumer.stderr);self.assertTrue(manifest['eligible'])
+ if defect:put(receipts/f'fsgg-wasm-sdk-{CURRENT}.tar.gz',b'wrong authenticated receipt fixture')
+ assembled=subprocess.run([sys.executable,str(script),'assemble','--work',str(selected),'--consumer',str(root/'consumer-output'),
+ '--receipts',str(receipts),'--logs',str(logs),'--output',str(root/'assembled'),'--runtime','0'],env=environment,capture_output=True,text=True)
+ final=json.loads((root/'assembled/evidence-manifest.json').read_text())
+ self.assertEqual(assembled.returncode,int(defect is not None),assembled.stderr)
+ self.assertEqual(final['eligible'],defect is None)
+ if defect:self.assertTrue(any(row['reason']=='consumer original differs from authenticated publication receipt' for row in final['errors']))
+ else:
+ self.assertFalse(final['errors'])
+ names={row['path'] for row in final['members']}
+ self.assertIn(f'receipts/fsgg-wasm-sdk-{CURRENT}.tar.gz',names)
+ self.assertIn(f'selected/packages/fs.gg.wasm.browser/fs.gg.wasm.browser.{CURRENT}.nupkg',names)
+ self.assertTrue(all('0.5.0' not in name for name in names))
+
+ def test_old_fable_and_package_names_are_not_current_fallbacks(self):
+ self.assertNotEqual(CURRENT,'0.5.0')
+ for project in ('FS.GG.Wasm.Browser','FS.GG.Wasm.Contracts'):
+ with self.assertRaisesRegex(ValueError,'unreviewed Fable dependency module'):
+ e.fable_dependency(f'consumer/public/sub/app/fable/fable_modules/{project}.0.5.0/Host.fs')
+ self.assertFalse(e.consumer_member('consumer/packages/fs.gg.wasm.browser/fs.gg.wasm.browser.0.5.0.nupkg'))
+ self.assertFalse(e.consumer_member('consumer/fsgg-wasm-sdk-0.5.0.tar.gz'))
+
+ def test_ambiguous_or_invalid_tracked_version_refuses_before_collection(self):
+ for xml in ('','0.6.00.5.0',
+ 'current'):
+ with self.subTest(xml=xml),tempfile.TemporaryDirectory() as raw:
+ root=pathlib.Path(raw);script,work,*_=current_cli_topology(root)
+ put(root/'source/eng/wasm-shared/version.props',xml.encode())
+ result=subprocess.run([sys.executable,str(script),'consumer','--work',str(work),'--output',str(root/'out'),'--runtime','0'],capture_output=True,text=True)
+ self.assertNotEqual(result.returncode,0)
+ self.assertFalse((root/'out').exists())
+
+ def test_actual_owner_trap_runs_current_collector_and_preserves_cleanup(self):
+ import shlex
+ source=(ROOT/'scripts/verify-wasm-package-consumer.sh').read_text()
+ trap=source.split('finish_consumer() {',1)[1].split('\ntrap finish_consumer EXIT',1)[0]
+ for missing in (False,True):
+ with self.subTest(missing=missing),tempfile.TemporaryDirectory() as raw:
+ root=pathlib.Path(raw);script,work,*_=current_cli_topology(root)
+ if missing:
+ (work/'consumer-packages/fs.gg.wasm.browser'/CURRENT/f'fs.gg.wasm.browser.{CURRENT}.nupkg').unlink()
+ status=root/'status.json';output=root/'consumer-output'
+ values={'mode':'--feed-only','repo':str(root/'source'),'work':str(work),
+ 'WASM_CONSUMER_EVIDENCE':str(output),'WASM_CONSUMER_STATUS':str(status)}
+ command='set -euo pipefail\n'+'\n'.join(key+'='+shlex.quote(value) for key,value in values.items())
+ command+='\nfinish_consumer() {'+trap+'\ntrap finish_consumer EXIT\nexit 0\n'
+ result=subprocess.run(['bash','-c',command],capture_output=True,text=True)
+ self.assertEqual(result.returncode,int(missing),result.stderr)
+ actual=json.loads(status.read_text())
+ self.assertEqual(actual,{'runtime':0,'collection':int(missing),'cleanup':0,'descendants':'unproved'})
+ self.assertFalse(work.exists())
+ merged=e.Projection();e.merge(merged,output)
+ self.assertEqual(bool(merged.errors),missing,merged.errors)
+
def test_hidden_metadata_and_colon_cache_exclusion(self):
with tempfile.TemporaryDirectory() as raw:
root=pathlib.Path(raw);work=root/'work'
for name in ('release-manifest.json','NuGet.Config','consumer/packages.lock.json'):
put(work/name,b'{}')
- put(work/'sdk-artifacts/fsgg-wasm-sdk-0.5.0.tar.gz')
+ put(work/'sdk-artifacts'/f'fsgg-wasm-sdk-{CURRENT}.tar.gz')
put(work/'installed-member-receipt.log')
put(work/'consumer-http.log')
for ident in e.PACKAGES:
- for leaf in (ident+'.0.5.0.nupkg',ident+'.0.5.0.nupkg.sha512','.nupkg.metadata'):
- put(work/'consumer-packages'/ident/'0.5.0'/leaf)
+ for leaf in (f'{ident}.{CURRENT}.nupkg',f'{ident}.{CURRENT}.nupkg.sha512','.nupkg.metadata'):
+ put(work/'consumer-packages'/ident/CURRENT/leaf)
for name in ('fsharp.core.10.1.302.nupkg','fsharp.core.10.1.302.nupkg.sha512','.nupkg.metadata','lib/netstandard2.0/FSharp.Core.dll','lib/netstandard2.1/FSharp.Core.dll'):
put(work/'consumer-packages/fsharp.core/10.1.302'/name)
put(work/'consumer/browser/test-results/.last-run.json',b'{"status":"passed"}')
@@ -80,7 +224,7 @@ def test_complete_fable_dependency_roles_and_original_archive_joins(self):
with self.subTest(context=context),tempfile.TemporaryDirectory() as raw:
root=pathlib.Path(raw);p=e.Projection();owner=context.split('/')[0]
for package,body in archives.items():
- p.add(put(root/package,body),owner+'/packages/'+package+'/'+package+'.0.5.0.nupkg')
+ p.add(put(root/package,body),owner+'/packages/'+package+'/'+f'{package}.{CURRENT}.nupkg')
dependency=root/'compiled/fable_modules'
put(dependency/'.gitignore',b'*')
library=dependency/'fable-library-js.5.18.0'
@@ -88,7 +232,7 @@ def test_complete_fable_dependency_roles_and_original_archive_joins(self):
('package.json',b'{"name":"@fable-org/fable-library-js","version":"2.8.0","type":"module"}'),
('Array.js',b'actual runtime'),('lib/big.js',b'actual nested runtime')):put(library/leaf,body)
for package,module in (('fs.gg.wasm.browser','FS.GG.Wasm.Browser'),('fs.gg.wasm.contracts','FS.GG.Wasm.Contracts')):
- folder=dependency/(module+'.0.5.0')
+ folder=dependency/(module+'.'+CURRENT)
for member,body in members[package].items():
if not member.startswith('fable/'):continue
leaf=member[len('fable/'):]
@@ -97,13 +241,13 @@ def test_complete_fable_dependency_roles_and_original_archive_joins(self):
if leaf.endswith('.fs'):put(folder/(leaf+'.js'),b'generated current source')
p.tree(root/'compiled',context)
self.assertFalse(p.errors,p.errors)
- self.assertIn(context+'/fable_modules/FS.GG.Wasm.Browser.0.5.0/Host.fs',p.files)
- self.assertIn(context+'/fable_modules/FS.GG.Wasm.Browser.0.5.0/Host.fs.js',p.files)
+ self.assertIn(context+f'/fable_modules/FS.GG.Wasm.Browser.{CURRENT}/Host.fs',p.files)
+ self.assertIn(context+f'/fable_modules/FS.GG.Wasm.Browser.{CURRENT}/Host.fs.js',p.files)
if owner=='consumer':
self.assertEqual(p.finish(root/'out',0),0)
joined=e.Projection();e.merge(joined,root/'out')
self.assertFalse(joined.errors,joined.errors);self.assertEqual(joined.files,p.files)
- copied=context+'/fable_modules/FS.GG.Wasm.Browser.0.5.0/Host.fs'
+ copied=context+f'/fable_modules/FS.GG.Wasm.Browser.{CURRENT}/Host.fs'
original=p.files[copied];p.files[copied]=b'changed copied source'
p.finish(root/'forged',0)
refused=e.Projection();e.merge(refused,root/'forged')
@@ -118,8 +262,8 @@ def test_complete_fable_dependency_roles_and_original_archive_joins(self):
bad.dependency(context+'/fable_modules/fable-library-js.5.18.0/Array.js')
self.assertTrue(any(x['reason']=='Fable dependency differs from original package member' for x in bad.errors),bad.errors)
for leaf in ('fable-library-js.5.18.0/unexpected.exe','fable-library-js.5.18.0/foreign.js',
- 'Other.0.5.0/Host.fs','FS.GG.Wasm.Browser.0.5.0/Foreign.fs',
- 'FS.GG.Wasm.Browser.0.5.0/Foreign.fs.js'):
+ f'Other.{CURRENT}/Host.fs',f'FS.GG.Wasm.Browser.{CURRENT}/Foreign.fs',
+ f'FS.GG.Wasm.Browser.{CURRENT}/Foreign.fs.js'):
name=context+'/fable_modules/'+leaf
if leaf.endswith('foreign.js'):
bad.files[name]=b'foreign runtime';bad.dependency(name)
@@ -184,13 +328,14 @@ def test_owner_trap_retains_original_status_after_collection_failure(self):
trap=source.split('finish_consumer() {',1)[1].split('\ntrap finish_consumer EXIT',1)[0]
# Run only the maintained shell trap body with inert collection/cleanup
# functions. No consumer command or downloaded source is executed.
- for runtime,collection,expected in ((37,19,37),(0,19,19),(0,0,0)):
+ for runtime,collection,cleanup,expected in ((37,19,23,37),(0,19,23,19),(0,0,23,23),(0,0,0,0)):
with tempfile.TemporaryDirectory() as raw:
- script='set -euo pipefail\nmode=--feed-only\nrepo=/inert\nwork=/inert\nWASM_CONSUMER_EVIDENCE=/inert\nWASM_CONSUMER_STATUS='+raw+'/status.json\npython3(){ return '+str(collection)+'; }\nrm(){ return 0; }\nfinish_consumer() {'+trap+'\ntrap finish_consumer EXIT\nexit '+str(runtime)
+ script='set -euo pipefail\nmode=--feed-only\nrepo=/inert\nwork=/inert\nWASM_CONSUMER_EVIDENCE=/inert\nWASM_CONSUMER_STATUS='+raw+'/status.json\npython3(){ return '+str(collection)+'; }\nrm(){ return '+str(cleanup)+'; }\nfinish_consumer() {'+trap+'\ntrap finish_consumer EXIT\nexit '+str(runtime)
result=subprocess.run(['bash','-c',script],capture_output=True,text=True)
self.assertEqual(result.returncode,expected,result.stderr)
status=json.loads((pathlib.Path(raw)/'status.json').read_text())
self.assertEqual(status['runtime'],runtime);self.assertEqual(status['collection'],collection)
+ self.assertEqual(status['cleanup'],cleanup)
def test_workflow_preserves_command_first_cause_and_native_log_failure(self):
text=(ROOT/'.github/workflows/wasm-installed-org.yml').read_text()
@@ -274,16 +419,31 @@ def test_complete_diff_binds_exact_blobs_and_refuses_semantic_change(self):
root=pathlib.Path(raw)
def git(*args):return subprocess.check_output(['git','-C',raw,*args],text=True,stderr=subprocess.DEVNULL).strip()
git('init','-q');git('config','user.name','inert control');git('config','user.email','control@example.invalid')
- allowed='scripts/wasm-release/bind-installed-release.py'
- put(root/allowed,b'old');git('add','.');git('commit','-qm','P');p=git('rev-parse','HEAD');tree=git('rev-parse','HEAD^{tree}')
- put(root/allowed,b'new');git('add','.');git('commit','-qm','Q');q=git('rev-parse','HEAD')
+ allowed=('scripts/wasm-release/installed-evidence.py','tests/release/wasm/test-installed-evidence.py')
+ excluded=('eng/wasm-shared/version.props','src/Wasm.Browser/Host.fs','sdk/wasm/verify-sdk.sh',
+ '.github/workflows/wasm-installed-org.yml','scripts/wasm-release/promotion.py','unlisted.txt')
+ for name in (*allowed,*excluded):put(root/name,b'old')
+ git('add','.');git('commit','-qm','P');p=git('rev-parse','HEAD');tree=git('rev-parse','HEAD^{tree}')
+ for name in allowed:put(root/name,b'new')
+ git('add','.');git('commit','-qm','Q');q=git('rev-parse','HEAD')
with patch.object(b,'ROOT',root),patch.object(b,'PUBLISHED',p),patch.object(b,'PUBLISHED_TREE',tree):
- audit=b.source_audit(p,q);self.assertEqual(len(audit['changes']),1)
+ audit=b.source_audit(p,q);self.assertEqual({row['path'] for row in audit['changes']},set(allowed))
row=audit['changes'][0];self.assertEqual(row['new']['sha256'],e.hashlib.sha256(b'new').hexdigest())
- self.assertEqual(row['old']['blob'],git('rev-parse',p+':'+allowed))
- put(root/'src/Wasm.Browser/Host.fs',b'semantic mutation');git('add','.');git('commit','-qm','bad')
- with self.assertRaises(AssertionError):b.source_audit(p,git('rev-parse','HEAD'))
+ self.assertEqual(row['old']['blob'],git('rev-parse',p+':'+row['path']))
+ with patch.object(b,'INSTALLED_PATHS',b.INSTALLED_PATHS-{allowed[0]}),self.assertRaises(AssertionError):b.source_audit(p,q)
+ for name in excluded:
+ git('reset','--hard',q);put(root/name,b'forbidden change');git('add','.');git('commit','-qm','excluded leaf')
+ with self.subTest(excluded=name),self.assertRaises(AssertionError):b.source_audit(p,git('rev-parse','HEAD'))
+ for defect in ('mode','delete','rename','symlink'):
+ git('reset','--hard',q);path=root/allowed[0]
+ if defect=='mode':path.chmod(0o755)
+ elif defect=='delete':path.unlink()
+ elif defect=='rename':path.rename(path.with_name('renamed.py'))
+ else:path.unlink();path.symlink_to(root/allowed[1])
+ git('add','.');git('commit','-qm',defect)
+ with self.subTest(defect=defect),self.assertRaises(AssertionError):b.source_audit(p,git('rev-parse','HEAD'))
with self.assertRaises(AssertionError):b.source_audit('a'*40,q)
+ with patch.object(b,'PUBLISHED_TREE','a'*40),self.assertRaises(AssertionError):b.source_audit(p,q)
if __name__=='__main__':unittest.main()