diff --git a/docs/atlas/catalog.json b/docs/atlas/catalog.json index e223299c..b83c2bfe 100644 --- a/docs/atlas/catalog.json +++ b/docs/atlas/catalog.json @@ -1 +1 @@ -{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","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","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"} diff --git a/docs/atlas/records/0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json b/docs/atlas/records/0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json new file mode 100644 index 00000000..2d690e15 --- /dev/null +++ b/docs/atlas/records/0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json @@ -0,0 +1 @@ +{"citations":[{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"ed35ba5affc25efcb38213ad6e2f568d542ccd49","sha256":"1d14f1661b2857c3cc52df5e3004c3c73add5ba15362a079e08556acbb72bf09"},{"path":"tests/Wasm.PackageConsumer/Program.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"3c53d61db88ebe7eddbdce897b3976cfe7f75f714eeac4e28bc037c5e9fb244c"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.mjs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"d2994c129805a73a23505237dc8582be386e7a63ca14771ebb2f393a2024e966"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs","repository":"FS-GG/FS.GG.Game","revision":"ed35ba5affc25efcb38213ad6e2f568d542ccd49","sha256":"8971b6aa2eeab82d5be20bc2e9bad6dc9f82c76ce86cee903aeff3a00401fbc4"},{"path":"tests/Wasm.Lifecycle.Tests/LifecycleTests.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"757cedc77d5c9c4e55c4bb9ee489e31a92f6ab5df11ccecd3344d7dadd9d74f5"},{"path":"src/Wasm.Browser/Lifecycle.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"3eeb9466880a9cc8db0284a3422d3e93d8d0634c128ceb32fc8aa456c98c0437"},{"path":"src/Wasm.Browser/Lifecycle.fsi","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"d263e8dd7535335751fd5480e47babce440f8ca0975c0d3cd8159bf29bc7f477"},{"path":"src/Wasm.Browser/Host.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"c502f8f9c55ec78dada7908241ae2f65242f442b91d14821bb83aef2c2319957"},{"path":"src/Wasm.Browser/Admission.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"0bb3799d2ee00ddbbbfa413db6b616a692e3228943d1fb43cf3c370f1e0475dc"},{"path":"eng/wasm-shared/version.props","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"6b50423f8f30da7d52305ee527baaddd91f77cc422f7585cf61c063bcad209e1"}],"conflictsWith":[],"id":"game/closed-worker-build-boundary","kind":"lesson","recordedAt":"2026-10-18T00:36:55Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Original0.6 source PR722/run39079271365/attempt1 passed at5d39; native merged producerbc78722f7a62a1ce5f0351d7969a26c307ec4fe7 has exact tested/delivered tree9df5fea131983d4d9ec98176106df62f143ad529. Source/candidate-installed CLR/Fable/numeric and all36 package-browser cases passed, including the public instruction-dense history. This is exact PR acceptance, not protection against a different subsequent result or proof of earlier timeout CPU cause.\nFirst protected original39080719438/attempt1 failed SDK browser qualification with35PASS/1FAIL8.3s: inherited terminal Process mode11 expected followerHostDisposed, but actual no-outcome/discarded follower retainedDeadlineExpired. The new66524-byte dense module passed allfour operations and94 complete observations across its three fresh Workers. The failed original retained model artifact12119445244/SHA549a7767f21cb8e01b66b2a3207c791d544006fc8fb93518e83464ba37f1466e/27974bytes; no eligible package custody/publication acceptance resulted. Neither the passed dense control nor failed follower assertion settles prior timing causes.\nDefaultCompatibility expireAt partitions and settles queued due= and held semantics remain distinct and unchanged.\nThe source delta is test/owning documentation only; parser/lifecycle/clock/caps/ABI/version/workflow code remains unchanged, with unpublished0.6 and the same36 real-browser cases/35-minute profile. Existing clean-source inert preflight and syntax checks passed; actual compilation/Fable, repaired metadata observation, boundary cohort/real browser behavior/cleanup and fit remain pending. Original failures and all prior source/publication/installed evidence stay preserved. No unchanged rerun,0.6publication/installed/consumer/image/native operation or whole-product acceptance is selected by this correction.\nThe first corrected original PR723 qualification39082520529/attempt1 at5d77 compiled, passed all50 Lifecycle controls (including the nine new reducer/connected-host boundaries), and then failed29PASS/7FAIL in the36-case browser cohort. All seven first failures were the new shared helper comparing actual Fable Uint8Array empty output to plain Array[]; matching first-cause/no-outcome/discarded/undispatched fields agreed. Seven original scalar reports were complete; mode11 due458/decision457/callback459 demonstrates the intended distinction. Assertions after the shared byte comparison were blocked, not passed; first-family retirement assertions after it were also unqualified. The narrow repair uses Array.from(actualOutput).toEqual([]), matching existing real ABI assertions without defaulting missing bytes or changing the receipt. Full terminal caller/clock/effect and byte-projection siblings were swept; no production defect was identified. Exact native synthetic d578b7f636c09d1b6573427cf4ed55b6519b8e69 has ordered bc/5d parents and equal candidate treeb36d03d7e1975e9f228e0d37cbfbde3c1a2e4025. Original model artifact12119227914 retained SHA6ec92956d407ad526ca8e1046907b961a98e42b5007192e248b662aa60c2fcaf/27975bytes; no eligible package custody resulted. Node syntax and diff checks pass for this repair; fresh exact-head full hosted qualification remains mandatory, with all original failures/budgets and separate unpublished0.6 gates unchanged.","supersedes":["7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9"],"tags":["wasm","expiry","deadline","terminal","first-cause","qualification","current-only"],"title":"Bind Default terminal follower cause to original due and actual producer clock"} diff --git a/docs/atlas/records/7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json b/docs/atlas/records/7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json new file mode 100644 index 00000000..d6cf1113 --- /dev/null +++ b/docs/atlas/records/7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json @@ -0,0 +1 @@ +{"citations":[{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"1b18d63e3dd7633818f39357336378230101cf52aa745b3d8f0bcf59fda4f5f5"},{"path":"tests/Wasm.PackageConsumer/Program.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"3c53d61db88ebe7eddbdce897b3976cfe7f75f714eeac4e28bc037c5e9fb244c"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.mjs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"d2994c129805a73a23505237dc8582be386e7a63ca14771ebb2f393a2024e966"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"8c35dcb9ab07d91aa631c85bf7ff60539ac0ad98d67b929627fdd1d8d3387c2b"},{"path":"tests/Wasm.Lifecycle.Tests/LifecycleTests.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"757cedc77d5c9c4e55c4bb9ee489e31a92f6ab5df11ccecd3344d7dadd9d74f5"},{"path":"src/Wasm.Browser/Lifecycle.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"3eeb9466880a9cc8db0284a3422d3e93d8d0634c128ceb32fc8aa456c98c0437"},{"path":"src/Wasm.Browser/Lifecycle.fsi","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"d263e8dd7535335751fd5480e47babce440f8ca0975c0d3cd8159bf29bc7f477"},{"path":"src/Wasm.Browser/Host.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"c502f8f9c55ec78dada7908241ae2f65242f442b91d14821bb83aef2c2319957"},{"path":"src/Wasm.Browser/Admission.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"0bb3799d2ee00ddbbbfa413db6b616a692e3228943d1fb43cf3c370f1e0475dc"},{"path":"eng/wasm-shared/version.props","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"6b50423f8f30da7d52305ee527baaddd91f77cc422f7585cf61c063bcad209e1"}],"conflictsWith":[],"id":"game/closed-worker-build-boundary","kind":"lesson","recordedAt":"2026-10-18T00:18:37Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Original0.6 source PR722/run39079271365/attempt1 passed at5d39; native merged producerbc78722f7a62a1ce5f0351d7969a26c307ec4fe7 has exact tested/delivered tree9df5fea131983d4d9ec98176106df62f143ad529. Source/candidate-installed CLR/Fable/numeric and all36 package-browser cases passed, including the public instruction-dense history. This is exact PR acceptance, not protection against a different subsequent result or proof of earlier timeout CPU cause.\nFirst protected original39080719438/attempt1 failed SDK browser qualification with35PASS/1FAIL8.3s: inherited terminal Process mode11 expected followerHostDisposed, but actual no-outcome/discarded follower retainedDeadlineExpired. The new66524-byte dense module passed allfour operations and94 complete observations across its three fresh Workers. The failed original retained model artifact12119445244/SHA549a7767f21cb8e01b66b2a3207c791d544006fc8fb93518e83464ba37f1466e/27974bytes; no eligible package custody/publication acceptance resulted. Neither the passed dense control nor failed follower assertion settles prior timing causes.\nDefaultCompatibility expireAt partitions and settles queued due= and held semantics remain distinct and unchanged.\nThe source delta is test/owning documentation only; parser/lifecycle/clock/caps/ABI/version/workflow code remains unchanged, with unpublished0.6 and the same36 real-browser cases/35-minute profile. Existing clean-source inert preflight and syntax checks passed; actual compilation/Fable, repaired metadata observation, boundary cohort/real browser behavior/cleanup and fit remain pending. Original failures and all prior source/publication/installed evidence stay preserved. No unchanged rerun,0.6publication/installed/consumer/image/native operation or whole-product acceptance is selected by this correction.","supersedes":["9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa"],"tags":["wasm","expiry","deadline","terminal","first-cause","qualification","current-only"],"title":"Bind Default terminal follower cause to original due and actual producer clock"} diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md index a5ed4478..5fbd7ae4 100644 --- a/docs/roadmaps/wasm-shared-01.md +++ b/docs/roadmaps/wasm-shared-01.md @@ -38,6 +38,64 @@ installed binder still selects that exact publication and refuses the unpublishe This source assignment selects no publication, installed operation, consumer activation, image/native workload or product outcome. +### First 0.6 source qualification and exact follower-expiry proof + +[PR722](https://github.com/FS-GG/FS.GG.Game/pull/722) delivered the opcode change +at `bc78722f7a62a1ce5f0351d7969a26c307ec4fe7`, exact tested/delivered tree +`9df5fea131983d4d9ec98176106df62f143ad529`. Its original PR qualification +39079271365/attempt1 passed the parser controls, 36 package-browser cases and +source/candidate-installed cohorts. The first protected qualification +[39080719438/attempt1](https://github.com/FS-GG/FS.GG.Game/actions/runs/39080719438) +failed with35 package-browser cases passed and one inherited terminal-mode11 +assertion failed: the undispatched follower retained `DeadlineExpired`, while the +fixture required `HostDisposed`. The new66,524-byte dense fixture passed all four +operations and its94 complete stage observations. This failed original retained +model evidence only; it produced no eligible package custody and selects no +publication or unchanged rerun. The successful PR result is not protected-current +acceptance or proof of an earlier timeout's cause. + +The producer's unchanged `DefaultCompatibility` update expires queued requests +at strict `original due < producer Clock` before active invalidation/whole-session +retirement. A real late timer can therefore expire both nearby250ms requests; +first cause must not be relabeled as disposal. The connected test correction reads +the follower's actual admitted queue due and actual producer clock at the first +retirement effect, before native termination and connected Host reentry, then +requires exactly the reason selected by that strict comparison. Both default +terminal families retain no ABI outcome, discarded authority, no follower Post, +distinct identity, once-only settlement, follower-before-terminal order and actual +whole-worker retirement before the terminal callback. The later follower-callback +clock is retained separately: connected termination +reenters `Observe(Now)` and can advance it beyond the original decision. Queued +expiry must settle before the first termination, disposal after it. No additional +clock sample sets the expected result. Missing actual queue admission or retirement +batch remains a setup failure. + +Six direct Lifecycle controls independently exercise before/exact/after follower +due for shutdown and terminal-Process followers, including actual phase setup, +original due, duplicate-timer refusal and complete retirement effect order. Three +additional actual connected-Host controls reproduce mechanical termination +reentry with declared inert ports: before/equal/after decision clocks, captured +actual timer identity/delay, and a later callback clock beyond the queued due. +They are logical interpreter controls, not browser timing or ABI execution proof. +The separate compatible SC2 queue-head `>=` boundary and held-snapshot semantics remain +unchanged; Default expiry is not generalized to that profile. Producer code, +parser, deadlines/caps/version and all36 real-browser cases remain unchanged. +The repaired current source still requires independent review, exact native +qualification, complete custody and fit under the existing35-minute profile. + +The first corrected source attempt, [39082520529/attempt1](https://github.com/FS-GG/FS.GG.Game/actions/runs/39082520529), +compiled and passed all50 Lifecycle controls, including the six new reducer and +three connected-host boundaries. Its browser cohort failed29PASS/7FAIL: the new +shared assertion compared an actual Fable `Uint8Array` receipt to plain `[]`. +All seven first failures were this representation mismatch, with matched reason +and no-outcome/discarded/undispatched fields. The reports retain the actual clocks; +mode11 recorded due458, decision457 and callback459. Assertions after that shared +comparison remained blocked and are not accepted. The repair compares actual byte +content with `Array.from`, as existing ABI assertions already do, while preserving +all first-cause, identity, ordering, no-post and retirement checks. Runtime code +and all36 browser cases remain unchanged. Fresh exact-head full qualification +is required; this failed original yielded no eligible package custody. + ## Accepted 0.5 closed Worker and installed qualifier The accepted source successor selected coherent `0.5.0` for the pair and SDK. diff --git a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs index 41775366..0b1e234e 100644 --- a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs +++ b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs @@ -547,6 +547,106 @@ let terminalProcess = let _, resumed = update 3L (ResumeRequested "unused") state Expect.isEmpty resumed "resume cannot revive retired held work" + for shutdownFollower in [ false; true ] do + for observed in [ 4L; 5L; 6L ] do + testCase (sprintf "Default follower strict due at %d shutdown=%b" observed shutdownFollower) <| fun _ -> + let reset = identity "worker-1" 1UL 1UL + let follower = identity "worker-1" 2UL 1UL + let candidate = identity "worker-2" 3UL 2UL + let state = loadedSelected DefaultCompatibility + let state = next 0L (LoadRequested(candidate, PrepareCandidate("due-candidate", Some 1UL), [| 0uy |])) state + let state = limited 0L (LimitedInvokeAndRetire(reset, [| 1uy |])) { terminalLimits with MaximumDeadlineMilliseconds = 3 } state + let following = if shutdownFollower then LimitedShutdown follower else LimitedInvokeAndRetire(follower, [| 2uy |]) + let state = limited 0L following { terminalLimits with MaximumDeadlineMilliseconds = 5 } state + let state = next 1L (WorkerObserved("worker-1", PhaseObserved(reset, tokenFor reset, AllocateInput))) state + let state = next 2L (WorkerObserved("worker-1", PhaseObserved(reset, tokenFor reset, Process))) state + let before = Lifecycle.project state + Expect.equal before.Current.DeadlineMilliseconds 3L "phase callbacks keep original active due" + Expect.equal before.Ordinary.Head.Id follower.Request "follower is actually queued" + Expect.equal before.Ordinary.Head.DeadlineMilliseconds 5L "original admitted follower due" + let state, effects = update observed (TimerObserved { Identity = reset; Correlation = tokenFor reset; Phase = Process }) state + expectRetired state effects reset [ "worker-1"; "worker-2" ] + Expect.equal (Lifecycle.project state).Clock observed "settlement uses exact producer clock" + let results = terminalResults effects + let destroyed = results |> List.find (fun result -> result.Identity = follower) + Expect.equal destroyed.Outcome None "queued follower never invokes an ABI" + Expect.equal destroyed.Disposition Discarded "no follower delivery authority" + Expect.equal destroyed.Reason (Some(if 5L < observed then DeadlineExpired else HostDisposed)) "strict queued expiry precedes terminal retirement; exact due is unexpired" + Expect.equal (results |> List.filter (fun result -> result.Identity = follower)).Length 1 "follower settles once" + let actual = results |> List.last + Expect.equal actual.Identity reset "actual terminal follows follower settlement" + Expect.isTrue (actual.Outcome |> Option.exists (fun outcome -> outcome.State = TimedOut && outcome.Dispatch = Dispatched)) "active Process timeout keeps actual ABI dispatch" + let _, duplicate = update (observed + 1L) (TimerObserved { Identity = reset; Correlation = tokenFor reset; Phase = Process }) state + Expect.isEmpty duplicate "duplicate timer cannot settle or dispatch again" + + // Actual connected interpreter with declared inert mechanics. Termination + // advances only this injected port clock, reproducing Observe(Now) reentry. + // This is not a browser timing or ABI-execution claim. + for observed in [ 4L; 5L; 6L ] do + testCase (sprintf "connected retirement decision clock survives reentry at %d" observed) <| fun _ -> + let mutable portClock = 0L + let mutable connected: Host option = None + let timers = System.Collections.Generic.Dictionary unit)>() + let events = ResizeArray() + let results = ResizeArray() + let posts = ResizeArray() + let transport: HostTransport = + { Now = fun () -> portClock + CreateWorker = fun _ _ -> () + PostCommand = fun _ command -> posts.Add command + TerminateWorker = fun worker -> + events.Add(("terminate:" + worker, connected.Value.Projection.Clock)) + portClock <- 6L + ArmTimer = fun key delay callback -> timers[key] <- (delay, callback) + CancelTimer = fun key -> timers.Remove key |> ignore } + let receive (result: BrowserResult) = + let clock = connected.Value.Projection.Clock + events.Add(("settle:" + string result.Identity.Request, clock)) + results.Add((result, clock)) + let host = Host.CreateConnected(settings Sc2ImportedStrict, transport, receive) |> Result.defaultWith (fun issues -> failtestf "%A" issues) + connected <- Some host + let load = identity "worker-1" 100UL 1UL + let initialize = identity "worker-1" 101UL 1UL + let reset = identity "worker-1" 1UL 1UL + let follower = identity "worker-1" 2UL 1UL + host.Load(0L, load, [| 0uy |]) + host.Observe(0L, "worker-1", AbiVersionObserved(load, tokenFor load, 0x10000u)) + host.Initialize(0L, initialize, [| 0uy |]) + host.Observe(0L, "worker-1", InvocationObserved(success Initialize initialize, tokenFor initialize)) + host.SubmitLimited(0L, LimitedInvokeAndRetire(reset, [| 1uy |]), { terminalLimits with MaximumDeadlineMilliseconds = 3 }) |> fun result -> Expect.equal result (Ok ()) "terminal admitted" + host.SubmitLimited(0L, LimitedShutdown follower, { terminalLimits with MaximumDeadlineMilliseconds = 5 }) |> fun result -> Expect.equal result (Ok ()) "follower admitted" + for at, phase in [ 1L, AllocateInput; 2L, Process ] do + portClock <- at + host.Observe(at, "worker-1", PhaseObserved(reset, tokenFor reset, phase)) + Expect.equal host.Projection.Current.Id reset.Request "actual current terminal identity" + Expect.equal host.Projection.Current.Worker reset.WorkerInstance "actual current worker" + Expect.equal host.Projection.Current.DeadlineMilliseconds 3L "active due remains original" + Expect.equal host.Projection.Ordinary.Head.Id follower.Request "actual queued follower identity" + Expect.equal host.Projection.Ordinary.Head.DeadlineMilliseconds 5L "actual queued due" + Expect.equal timers.Count 1 "only actual active phase timer remains" + let timer = timers |> Seq.exactlyOne + Expect.equal timer.Key $"{reset.WorkerInstance}:{reset.Generation}:{reset.Request}:{OperationToken.value (tokenFor reset)}:Process" "exact current request/correlation/phase timer selected" + let delay, fire = timer.Value + Expect.equal delay 2 "actual strict timer uses due-now+1" + events.Clear(); results.Clear(); posts.Clear() + portClock <- observed + fire () + let firstTermination = events |> Seq.find (fun (kind, _) -> kind.StartsWith("terminate:")) + Expect.equal (snd firstTermination) observed "capture outer producer clock before termination reentry" + let destroyed, callbackClock = results |> Seq.find (fun (result, _) -> result.Identity = follower) + Expect.equal destroyed.Reason (Some(if 5L < observed then DeadlineExpired else HostDisposed)) "reason binds decision, not later callback clock" + Expect.equal destroyed.Outcome None "no follower ABI" + Expect.equal destroyed.Disposition Discarded "no follower authority" + Expect.equal callbackClock 6L "connected termination advances callback clock even when queue was unexpired" + let kinds = events |> Seq.map fst |> Seq.toList + Expect.equal ((kinds |> List.findIndex ((=) "settle:2")) < (kinds |> List.findIndex ((=) "terminate:worker-1"))) (5L < observed) "queued expiry precedes termination; disposal follows it" + Expect.equal kinds.[kinds.Length - 1] "settle:1" "terminal result follows retirement/follower" + Expect.isTrue host.Projection.Disposed "whole session retired" + Expect.isEmpty posts "no following post" + let count = results.Count + fire () + Expect.equal results.Count count "stale real timer callback cannot settle twice" + for observed, expired in [ 4L, false; 5L, true; 6L, true ] do testCase (sprintf "queued terminal head boundary %d" observed) <| fun _ -> let a = identity "worker-1" 1UL 1UL diff --git a/tests/Wasm.PackageConsumer/Program.fs b/tests/Wasm.PackageConsumer/Program.fs index 06dabdf4..76ecb82e 100644 --- a/tests/Wasm.PackageConsumer/Program.fs +++ b/tests/Wasm.PackageConsumer/Program.fs @@ -105,6 +105,14 @@ let createHost path digest outputCap deadline (transport: HostTransport) receive Host.CreateConnected(settings, transport, settle) |> unwrap +// Test observation of actual producer state; never samples/advances its clock. +let producerClock (host: Host) = string host.Projection.Clock +let queuedDeadline (host: Host) request = + host.Projection.Ordinary + |> List.tryFind (fun pending -> string pending.Id = request) + |> Option.map (fun pending -> string pending.DeadlineMilliseconds) + |> Option.defaultValue "" + let load (host: Host) now wire artifact = host.Load(now, identity wire, artifact) // The cold-start controls use the same real, pre-create enclosing deadline as diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs index 0ac890b6..e9627cba 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs @@ -4,9 +4,14 @@ const bytes = async name => new Uint8Array(await (await fetch(`./modules/${name} const digest = async value => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", value)), byte => byte.toString(16).padStart(2, "0")).join(""); async function session(artifact, prefix, outputCap = 65536, deadline = 250, inject = false, observer, holdLoaded = false) { const transport = createHostTransport(new URL("./_content/FS.GG.Wasm.Browser/", import.meta.url),observer?{observe:observer}:undefined); - const waiting = new Map(), results = [], posts=[], terminations=[], deliveryStates=[]; + const waiting = new Map(), results = [], posts=[], terminations=[], deliveryStates=[], admissions=[], terminationStates=[]; const terminate=transport.TerminateWorker; - transport.TerminateWorker=name=>{terminations.push(name);terminate(name);}; + transport.TerminateWorker=name=>{ + // Observe the producer decision BEFORE mechanical termination and the + // connected Host's subsequent WorkerTerminated/Now reentry. + terminationStates.push({worker:name,clock:bridge.producerClock(host),disposed:bridge.project(host).Disposed,settledRequests:results.map(x=>x.Request)}); + terminations.push(name);terminate(name); + }; let ignored=0, host;const held=[]; const create=transport.CreateWorker; transport.CreateWorker=(name,receive)=>create(name,wire=>{ @@ -27,15 +32,19 @@ async function session(artifact, prefix, outputCap = 65536, deadline = 250, inje transport.PostCommand=(name,command)=>{posts.push({name,Kind:command.Kind,request:command.Identity.Request});post(name,command);}; const artifactDigest=await digest(artifact); host = bridge.createHost(prefix === "barc" ? "bar-protected" : "sc2-imported-strict", artifactDigest, outputCap, deadline, transport, result => { - deliveryStates.push({request:result.Request,projection:bridge.project(host),terminations:[...terminations]}); + deliveryStates.push({request:result.Request,clock:bridge.producerClock(host),projection:bridge.project(host),terminations:[...terminations],terminationStates:[...terminationStates]}); results.push(result); const pending = waiting.get(result.Request); waiting.delete(result.Request); pending?.(result); }); let request = 0; const send = (method, value, owner = "installed-worker", generation = "1") => { const id = String(++request), identity = {WorkerInstance: owner, Request: id, Generation: generation}; - return new Promise(resolve => { waiting.set(id, resolve); bridge[method](host, transport.Now(), identity, value); }); + return new Promise(resolve => { + waiting.set(id, resolve); + bridge[method](host, transport.Now(), identity, value); + admissions.push({request:id,queuedDeadline:bridge.queuedDeadline(host,id)}); + }); }; - return { host, send, results, posts, terminations, deliveryStates, transport, + return { host, send, results, posts, terminations, deliveryStates, admissions, terminationStates, transport, heldCount:()=>held.length, releaseHeld:()=>{for(const row of held.splice(0))row.receive(row.wire);}, ignored:()=>ignored, @@ -222,23 +231,27 @@ window.runTerminalProcess = async (mode=18, follower="invokeAndRetire") => { await s.send("load",artifact);await s.send("initialize",new Uint8Array([mode])); const candidate=await s.prepare(artifact,"terminal-candidate"); if(candidate.State!=="succeeded")throw new Error("candidate preparation failed"); + const priorTerminations=s.terminationStates.length; const active=s.send("invoke",new Uint8Array([1])); const reset=s.send("invokeAndRetire",new Uint8Array([2])); const following=s.send(follower,new Uint8Array([3])); const [a,r,f]=await Promise.all([active,reset,following]); const retirement=s.deliveryStates.find(item=>item.request===r.Request); - return {active:a,reset:r,follower:f,posts:s.posts.filter(item=>item.Kind==="process"||item.Kind==="shutdown").map(item=>({request:item.request,kind:item.Kind})),retirement,deliveries:s.results.map(item=>item.Request),projection:s.projection()}; + return {active:a,reset:r,follower:f,retirementBatch:retirement.terminationStates.slice(priorTerminations),followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,posts:s.posts.filter(item=>item.Kind==="process"||item.Kind==="shutdown").map(item=>({request:item.request,kind:item.Kind})),retirement,deliveries:s.results.map(item=>item.Request),projection:s.projection()}; }finally{s.dispose();} }; window.runTerminalRaw = async mode => { const artifact=await bytes("control-ordinary.wasm"),s=await session(artifact,"sc2c"); try { - await s.send("load",artifact);await s.send("initialize",new Uint8Array([mode])); - await s.prepare(artifact,"terminal-candidate"); + const loaded=await s.send("load",artifact),initialized=await s.send("initialize",new Uint8Array([mode])); + const candidate=await s.prepare(artifact,"terminal-candidate"); + if([loaded,initialized,candidate].some(x=>x.State!=="succeeded"))throw new Error("terminal raw setup did not succeed"); + const priorTerminations=s.terminationStates.length; const reset=s.send("invokeAndRetire",new Uint8Array([1])); const follower=s.send("shutdown"); const [r,f]=await Promise.all([reset,follower]); - return {reset:r,follower:f,posts:s.posts.filter(x=>x.Kind==="process"||x.Kind==="shutdown").map(x=>x.Kind),retirement:s.deliveryStates.find(x=>x.request===r.Request)}; + const retirement=s.deliveryStates.find(x=>x.request===r.Request); + return {reset:r,follower:f,retirementBatch:retirement.terminationStates.slice(priorTerminations),followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,deliveries:s.results.map(x=>x.Request),posts:s.posts.filter(x=>x.Kind==="process"||x.Kind==="shutdown").map(x=>x.Kind),retirement}; }finally{s.dispose();} }; diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs index e51f70c5..dd3a46c5 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs @@ -190,12 +190,49 @@ test("unsafe output after guest rejection preserves status and removes dead Work expect(value.active).toBe(""); }); +// DefaultCompatibility expires queued work before active retirement, using the +// strict original due < producer decision Clock boundary. Read actual queue/effect +// state instead of assuming that a late timer fires between two nearby deadlines. +function reportTerminal(value) { + const decision=value.retirementBatch[0]; + const complete=/^[0-9]+$/.test(value.followerDue??"")&&/^[0-9]+$/.test(decision?.clock??""); + console.log(JSON.stringify({stage:"terminal follower original deadline",resetRequest:value.reset.Request,resetState:value.reset.State,resetPhase:value.reset.Phase,resetDispatched:value.reset.Dispatched,followerRequest:value.follower.Request,due:value.followerDue,decisionClock:decision?.clock,followerCallbackClock:value.followerClock,reason:value.follower.Reason,complete,expired:complete?BigInt(value.followerDue)=BigInt(decision.clock)).toBe(true); + } + expect(value.follower.Request).not.toBe(value.reset.Request); + expect(value.deliveries.indexOf(value.follower.Request)).toBeGreaterThanOrEqual(0); + expect(value.deliveries.indexOf(value.follower.Request)).toBeLessThan(value.deliveries.indexOf(value.reset.Request)); + expect(new Set(value.deliveries).size).toBe(value.deliveries.length); +} + for(const follower of ["invokeAndRetire","shutdown"])test(`terminal Process retires actual package Workers before result and ${follower}`,async({page})=>{ const value=await page.evaluate(follower=>window.runTerminalProcess(18,follower),follower); + reportTerminal(value); expect(value.reset).toMatchObject({State:"succeeded",Phase:"Process",Dispatched:true}); expect(Array.from(value.reset.Output)).toEqual([83,67,50,51]); expect(value.posts.map(item=>item.kind)).toEqual(["process","process"]); - expect(value.follower).toMatchObject({HasOutcome:false,Disposition:"Discarded",Reason:"HostDisposed"}); + expectTerminalFollower(value); expect(value.retirement.projection).toMatchObject({Disposed:true,ActiveWorker:"",CandidateWorker:""}); expect(new Set(value.retirement.terminations)).toEqual(new Set(["installed-worker","candidate-worker"])); expect(value.deliveries.indexOf(value.follower.Request)).toBeLessThan(value.deliveries.indexOf(value.reset.Request)); @@ -205,10 +242,11 @@ for(const follower of ["invokeAndRetire","shutdown"])test(`terminal Process reti for(const [mode,state,status] of [[18,"succeeded",0],[19,"rejected",92],[16,"faulted",0],[17,"faulted",0],[11,"timeout",0]]) test(`terminal Process preserves actual installed ABI and cleanup evidence mode ${mode}`,async({page})=>{ const r=await page.evaluate(mode=>window.runTerminalRaw(mode),mode); + reportTerminal(r); expect(r.reset).toMatchObject({State:state,GuestStatus:status,Dispatched:true}); if(mode===16||mode===17)expect(r.reset.CleanupDiagnostic).toContain("unreachable"); expect(r.posts).toEqual(["process"]); expect(r.retirement.projection).toMatchObject({Disposed:true,ActiveWorker:"",CandidateWorker:""}); expect(new Set(r.retirement.terminations)).toEqual(new Set(["installed-worker","candidate-worker"])); - expect(r.follower).toMatchObject({HasOutcome:false,Disposition:"Discarded",Reason:"HostDisposed"}); + expectTerminalFollower(r); });