@ceralive/modem-control 0.2.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -0
- package/dist/backend/at-lease.d.ts +57 -0
- package/dist/backend/at-lease.js +109 -0
- package/dist/backend/cell-info.d.ts +46 -0
- package/dist/backend/cell-info.js +124 -0
- package/dist/backend/constants.d.ts +24 -0
- package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
- package/dist/backend/device-classifier.d.ts +46 -0
- package/dist/backend/device-classifier.js +258 -0
- package/dist/backend/enrichment.d.ts +28 -0
- package/dist/backend/enrichment.js +59 -0
- package/dist/backend/features.d.ts +61 -0
- package/dist/backend/features.js +109 -0
- package/dist/backend/identity-ladder.d.ts +57 -0
- package/dist/backend/identity-ladder.js +158 -0
- package/dist/backend/identity-registry.d.ts +51 -0
- package/dist/backend/identity-registry.js +101 -0
- package/dist/backend/index.d.ts +30 -0
- package/dist/backend/index.js +35 -0
- package/dist/backend/lifecycle-interlock.d.ts +25 -0
- package/dist/backend/lifecycle-interlock.js +18 -0
- package/dist/backend/managed-objects.d.ts +39 -0
- package/dist/backend/managed-objects.js +82 -0
- package/dist/backend/mapping.d.ts +11 -0
- package/dist/backend/mapping.js +148 -0
- package/dist/backend/mm-backend.d.ts +44 -0
- package/dist/backend/mm-backend.js +154 -0
- package/dist/backend/mm-location.d.ts +21 -0
- package/dist/backend/mm-location.js +237 -0
- package/dist/backend/mm-mutations.d.ts +42 -0
- package/dist/backend/mm-mutations.js +259 -0
- package/dist/backend/modem-actor.d.ts +41 -0
- package/dist/backend/modem-actor.js +78 -0
- package/dist/backend/nm-auto-apn.d.ts +54 -0
- package/dist/backend/nm-auto-apn.js +124 -0
- package/dist/backend/nm-gsm-fields.d.ts +23 -0
- package/dist/backend/nm-gsm-fields.js +107 -0
- package/dist/backend/nmcli-nm-port.d.ts +28 -0
- package/dist/backend/nmcli-nm-port.js +173 -0
- package/dist/backend/nmcli-runner.d.ts +24 -0
- package/dist/backend/nmcli-runner.js +35 -0
- package/dist/backend/observer.d.ts +37 -0
- package/dist/backend/observer.js +219 -0
- package/dist/backend/power-contract.d.ts +49 -0
- package/dist/backend/power-contract.js +34 -0
- package/dist/backend/recovery-attribution.d.ts +32 -0
- package/dist/backend/recovery-attribution.js +57 -0
- package/dist/backend/recovery-budget.d.ts +45 -0
- package/dist/backend/recovery-budget.js +44 -0
- package/dist/backend/recovery-ladder.d.ts +94 -0
- package/dist/backend/recovery-ladder.js +116 -0
- package/dist/backend/router-ethernet.d.ts +19 -0
- package/dist/backend/router-ethernet.js +66 -0
- package/dist/backend/row-store.d.ts +13 -0
- package/dist/backend/row-store.js +82 -0
- package/dist/backend/signal-setup.d.ts +30 -0
- package/dist/backend/signal-setup.js +92 -0
- package/dist/backend/sim-unlock.d.ts +13 -0
- package/dist/backend/sim-unlock.js +153 -0
- package/dist/backend/transition-preconditions.d.ts +76 -0
- package/dist/backend/transition-preconditions.js +64 -0
- package/dist/backend/usage/accounting.d.ts +39 -0
- package/dist/backend/usage/accounting.js +73 -0
- package/dist/backend/usage/billing-cycle.d.ts +11 -0
- package/dist/backend/usage/billing-cycle.js +39 -0
- package/dist/backend/usage/boot-id.d.ts +6 -0
- package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
- package/dist/backend/usage/index.d.ts +8 -0
- package/dist/backend/usage/index.js +11 -0
- package/dist/backend/usage/policy-store.d.ts +50 -0
- package/dist/backend/usage/policy-store.js +161 -0
- package/dist/backend/usage/policy-write.d.ts +65 -0
- package/dist/backend/usage/policy-write.js +112 -0
- package/dist/backend/usage/proc-net-dev.d.ts +18 -0
- package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
- package/dist/backend/usage/sampler.d.ts +75 -0
- package/dist/backend/usage/sampler.js +211 -0
- package/dist/backend/usage/store.d.ts +38 -0
- package/dist/backend/usage/store.js +126 -0
- package/dist/backend/usb-device-snapshot.d.ts +31 -0
- package/dist/backend/usb-device-snapshot.js +1 -0
- package/dist/backend/usb-enumerator.d.ts +21 -0
- package/dist/backend/usb-enumerator.js +153 -0
- package/dist/backend/usb-mode-transition.d.ts +29 -0
- package/dist/backend/usb-mode-transition.js +196 -0
- package/dist/band/band-names.d.ts +42 -0
- package/dist/band/band-names.js +150 -0
- package/dist/band/certification.d.ts +84 -0
- package/dist/band/certification.js +127 -0
- package/dist/band/certified-bands.json +4 -0
- package/dist/band/index.d.ts +2 -0
- package/dist/band/index.js +8 -0
- package/dist/capability/detect.d.ts +52 -0
- package/dist/capability/detect.js +86 -0
- package/dist/capability/five-g-preference.d.ts +104 -0
- package/dist/capability/five-g-preference.js +171 -0
- package/dist/capability/index.d.ts +3 -0
- package/dist/capability/index.js +10 -0
- package/dist/capability/support-claim.d.ts +39 -0
- package/dist/capability/support-claim.js +81 -0
- package/dist/domain/brand.d.ts +10 -0
- package/dist/domain/brand.js +21 -0
- package/dist/domain/errors.d.ts +32 -0
- package/dist/domain/errors.js +48 -0
- package/dist/domain/generation.d.ts +8 -0
- package/dist/domain/generation.js +12 -0
- package/dist/domain/guards.d.ts +6 -0
- package/dist/domain/guards.js +127 -0
- package/dist/domain/identity.d.ts +109 -0
- package/dist/domain/identity.js +86 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.js +18 -0
- package/dist/domain/mm-enums.d.ts +12 -0
- package/dist/domain/mm-enums.js +139 -0
- package/dist/domain/modem-presentation.d.ts +10 -0
- package/dist/domain/modem-presentation.js +39 -0
- package/dist/domain/observation.d.ts +39 -0
- package/dist/domain/observation.js +4 -0
- package/dist/domain/operation.d.ts +118 -0
- package/dist/domain/operation.js +85 -0
- package/dist/domain/physical-identity.d.ts +43 -0
- package/dist/domain/physical-identity.js +113 -0
- package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
- package/dist/domain/policy.js +38 -0
- package/dist/domain/shadow-divergence.d.ts +27 -0
- package/dist/domain/shadow-divergence.js +70 -0
- package/dist/domain/snapshot.d.ts +53 -0
- package/dist/domain/snapshot.js +75 -0
- package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
- package/dist/domain/state.js +38 -0
- package/dist/fcc/coverage.d.ts +63 -0
- package/dist/fcc/coverage.js +102 -0
- package/dist/fcc/index.d.ts +3 -0
- package/dist/fcc/index.js +12 -0
- package/dist/fcc/policy-store.d.ts +40 -0
- package/dist/fcc/policy-store.js +146 -0
- package/dist/fcc/policy-write.d.ts +32 -0
- package/dist/fcc/policy-write.js +37 -0
- package/dist/hardware/hilink-protocol.d.ts +39 -0
- package/dist/hardware/hilink-protocol.js +58 -0
- package/dist/hardware/index.d.ts +3 -0
- package/dist/hardware/index.js +15 -0
- package/dist/hardware/router-parsers.d.ts +89 -0
- package/dist/hardware/router-parsers.js +234 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +26 -0
- package/dist/journal/codec.d.ts +44 -0
- package/dist/journal/codec.js +198 -0
- package/dist/journal/engine.d.ts +28 -0
- package/dist/journal/engine.js +68 -0
- package/dist/journal/entry.d.ts +74 -0
- package/dist/journal/entry.js +56 -0
- package/dist/journal/index.d.ts +6 -0
- package/dist/journal/index.js +6 -0
- package/dist/journal/legacy-ceraui.d.ts +73 -0
- package/dist/journal/legacy-ceraui.js +227 -0
- package/dist/journal/recovery.d.ts +58 -0
- package/dist/journal/recovery.js +117 -0
- package/dist/journal/store.d.ts +55 -0
- package/dist/journal/store.js +150 -0
- package/dist/location/fix-state.d.ts +53 -0
- package/dist/location/fix-state.js +75 -0
- package/dist/location/index.d.ts +2 -0
- package/dist/location/index.js +8 -0
- package/dist/location/nmea.d.ts +10 -0
- package/dist/location/nmea.js +89 -0
- package/dist/observations/envelope.d.ts +63 -0
- package/dist/observations/envelope.js +77 -0
- package/dist/observations/freshness.d.ts +28 -0
- package/dist/observations/freshness.js +76 -0
- package/dist/observations/index.d.ts +13 -0
- package/dist/observations/index.js +24 -0
- package/dist/observations/metric.d.ts +47 -0
- package/dist/observations/metric.js +73 -0
- package/dist/observations/model.d.ts +80 -0
- package/dist/observations/model.js +11 -0
- package/dist/observations/provenance.d.ts +94 -0
- package/dist/observations/provenance.js +67 -0
- package/dist/observations/raw.d.ts +42 -0
- package/dist/observations/raw.js +146 -0
- package/dist/observations/reading.d.ts +51 -0
- package/dist/observations/reading.js +65 -0
- package/dist/observations/sources/hilink.d.ts +10 -0
- package/dist/observations/sources/hilink.js +84 -0
- package/dist/observations/sources/modemmanager.d.ts +18 -0
- package/dist/observations/sources/modemmanager.js +239 -0
- package/dist/observations/sources/router-shared.d.ts +29 -0
- package/dist/observations/sources/router-shared.js +68 -0
- package/dist/observations/sources/ufi.d.ts +10 -0
- package/dist/observations/sources/ufi.js +111 -0
- package/dist/observations/sources/zte.d.ts +7 -0
- package/dist/observations/sources/zte.js +71 -0
- package/dist/observations/state-separation.d.ts +64 -0
- package/dist/observations/state-separation.js +52 -0
- package/dist/operations/contracts.d.ts +72 -0
- package/dist/operations/contracts.js +1 -0
- package/dist/operations/index.d.ts +1 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/operation-engine.d.ts +16 -0
- package/dist/operations/operation-engine.js +194 -0
- package/dist/ports/index.d.ts +12 -0
- package/{src/ports/index.ts → dist/ports/index.js} +12 -8
- package/dist/ports/location.d.ts +87 -0
- package/dist/ports/location.js +36 -0
- package/dist/ports/modem-manager.d.ts +89 -0
- package/dist/ports/modem-manager.js +9 -0
- package/dist/ports/mutation-admission.d.ts +27 -0
- package/dist/ports/mutation-admission.js +9 -0
- package/dist/ports/network-manager.d.ts +68 -0
- package/dist/ports/network-manager.js +13 -0
- package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
- package/dist/ports/observation.js +7 -0
- package/dist/ports/ops.d.ts +44 -0
- package/dist/ports/ops.js +16 -0
- package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
- package/dist/ports/receipts.js +9 -0
- package/dist/ports/reconcile.d.ts +33 -0
- package/dist/ports/reconcile.js +200 -0
- package/dist/ports/resource-ownership.d.ts +29 -0
- package/dist/ports/resource-ownership.js +1 -0
- package/dist/ports/router.d.ts +19 -0
- package/dist/ports/router.js +7 -0
- package/dist/ports/sms.d.ts +64 -0
- package/dist/ports/sms.js +24 -0
- package/dist/ports/uhubctl.d.ts +6 -0
- package/dist/ports/uhubctl.js +1 -0
- package/dist/providers/contracts.d.ts +124 -0
- package/dist/providers/contracts.js +10 -0
- package/dist/providers/huawei-hilink/index.d.ts +2 -0
- package/dist/providers/huawei-hilink/index.js +2 -0
- package/dist/providers/huawei-hilink/operations.d.ts +20 -0
- package/dist/providers/huawei-hilink/operations.js +56 -0
- package/dist/providers/huawei-hilink/provider.d.ts +52 -0
- package/dist/providers/huawei-hilink/provider.js +76 -0
- package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
- package/dist/providers/huawei-hilink/runtime.js +171 -0
- package/dist/providers/huawei-hilink/session.d.ts +28 -0
- package/dist/providers/huawei-hilink/session.js +120 -0
- package/dist/providers/huawei-hilink/transport.d.ts +19 -0
- package/dist/providers/huawei-hilink/transport.js +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/matcher.d.ts +3 -0
- package/dist/providers/matcher.js +205 -0
- package/dist/providers/modem-manager/errors.d.ts +7 -0
- package/dist/providers/modem-manager/errors.js +37 -0
- package/dist/providers/modem-manager/generic-operations.d.ts +8 -0
- package/dist/providers/modem-manager/generic-operations.js +207 -0
- package/dist/providers/modem-manager/index.d.ts +3 -0
- package/dist/providers/modem-manager/index.js +3 -0
- package/dist/providers/modem-manager/module-operations.d.ts +21 -0
- package/dist/providers/modem-manager/module-operations.js +118 -0
- package/dist/providers/modem-manager/provider.d.ts +39 -0
- package/dist/providers/modem-manager/provider.js +152 -0
- package/dist/providers/modem-manager/snapshot.d.ts +5 -0
- package/dist/providers/modem-manager/snapshot.js +204 -0
- package/dist/providers/modem-manager/types.d.ts +135 -0
- package/dist/providers/modem-manager/types.js +1 -0
- package/dist/providers/network-manager/adapter.d.ts +71 -0
- package/dist/providers/network-manager/adapter.js +348 -0
- package/dist/providers/network-manager/index.d.ts +2 -0
- package/dist/providers/network-manager/index.js +2 -0
- package/dist/providers/network-manager/types.d.ts +163 -0
- package/dist/providers/network-manager/types.js +77 -0
- package/dist/providers/registry.d.ts +13 -0
- package/dist/providers/registry.js +33 -0
- package/dist/providers/ufi-himi/index.d.ts +6 -0
- package/dist/providers/ufi-himi/index.js +6 -0
- package/dist/providers/ufi-himi/operations.d.ts +41 -0
- package/dist/providers/ufi-himi/operations.js +66 -0
- package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
- package/dist/providers/ufi-himi/prohibitions.js +88 -0
- package/dist/providers/ufi-himi/provider.d.ts +41 -0
- package/dist/providers/ufi-himi/provider.js +204 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
- package/dist/providers/ufi-himi/session.d.ts +46 -0
- package/dist/providers/ufi-himi/session.js +92 -0
- package/dist/providers/ufi-himi/transport.d.ts +29 -0
- package/dist/providers/ufi-himi/transport.js +25 -0
- package/dist/providers/zte-goform/index.d.ts +2 -0
- package/dist/providers/zte-goform/index.js +2 -0
- package/dist/providers/zte-goform/provider.d.ts +51 -0
- package/dist/providers/zte-goform/provider.js +101 -0
- package/dist/providers/zte-goform/session.d.ts +17 -0
- package/dist/providers/zte-goform/session.js +133 -0
- package/dist/providers/zte-goform/transport.d.ts +16 -0
- package/dist/providers/zte-goform/transport.js +1 -0
- package/dist/radio/band-truth.d.ts +50 -0
- package/dist/radio/band-truth.js +92 -0
- package/dist/radio/index.d.ts +3 -0
- package/dist/radio/index.js +10 -0
- package/dist/radio/mode-combinations.d.ts +78 -0
- package/dist/radio/mode-combinations.js +198 -0
- package/dist/radio/mode-truth.d.ts +67 -0
- package/dist/radio/mode-truth.js +112 -0
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +189 -0
- package/dist/safety/composition-root.d.ts +28 -0
- package/dist/safety/composition-root.js +65 -0
- package/dist/safety/flock-resource-ownership.d.ts +11 -0
- package/dist/safety/flock-resource-ownership.js +151 -0
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/sms/dbus-messaging.d.ts +17 -0
- package/dist/sms/dbus-messaging.js +185 -0
- package/dist/sms/inbox-store.d.ts +10 -0
- package/dist/sms/inbox-store.js +82 -0
- package/dist/sms/index.d.ts +4 -0
- package/dist/sms/index.js +10 -0
- package/dist/sms/mmcli-parse.d.ts +54 -0
- package/dist/sms/mmcli-parse.js +224 -0
- package/dist/sms/normalize.d.ts +42 -0
- package/dist/sms/normalize.js +95 -0
- package/dist/testing/domain-fakes.d.ts +58 -0
- package/dist/testing/domain-fakes.js +98 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/provider-fakes.d.ts +45 -0
- package/dist/testing/provider-fakes.js +64 -0
- package/dist/transport/calls.d.ts +9 -0
- package/dist/transport/calls.js +88 -0
- package/dist/transport/codec.d.ts +3 -0
- package/dist/transport/codec.js +207 -0
- package/dist/transport/dbus-native.d.ts +57 -0
- package/dist/transport/dbus-native.js +17 -0
- package/dist/transport/errors.d.ts +21 -0
- package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.js +9 -0
- package/dist/transport/signals.d.ts +15 -0
- package/dist/transport/signals.js +123 -0
- package/dist/transport/signature.d.ts +7 -0
- package/dist/transport/signature.js +94 -0
- package/dist/transport/transport.d.ts +2 -0
- package/dist/transport/transport.js +202 -0
- package/dist/transport/types.d.ts +61 -0
- package/dist/transport/types.js +19 -0
- package/dist/usb-mode/catalog-schema.d.ts +139 -0
- package/dist/usb-mode/catalog-schema.js +97 -0
- package/dist/usb-mode/catalog.d.ts +21 -0
- package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
- package/dist/usb-mode/certified-catalog.json +67 -0
- package/dist/usb-mode/index.d.ts +5 -0
- package/dist/usb-mode/index.js +15 -0
- package/dist/usb-mode/ingestion.d.ts +111 -0
- package/dist/usb-mode/ingestion.js +187 -0
- package/dist/usb-mode/promotion-review.d.ts +21 -0
- package/dist/usb-mode/promotion-review.js +87 -0
- package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
- package/dist/usb-mode/usb-devices-parse.js +157 -0
- package/dist/ussd/calls.d.ts +32 -0
- package/dist/ussd/calls.js +96 -0
- package/dist/ussd/index.d.ts +5 -0
- package/dist/ussd/index.js +12 -0
- package/dist/ussd/mm-ussd.d.ts +37 -0
- package/dist/ussd/mm-ussd.js +205 -0
- package/dist/ussd/refusal.d.ts +53 -0
- package/dist/ussd/refusal.js +154 -0
- package/dist/ussd/registration.d.ts +20 -0
- package/dist/ussd/registration.js +101 -0
- package/dist/ussd/session.d.ts +84 -0
- package/dist/ussd/session.js +163 -0
- package/package.json +38 -4
- package/src/backend/at-lease.test.ts +0 -106
- package/src/backend/at-lease.ts +0 -158
- package/src/backend/cell-info.test.ts +0 -154
- package/src/backend/cell-info.ts +0 -160
- package/src/backend/device-classifier.test.ts +0 -168
- package/src/backend/device-classifier.ts +0 -248
- package/src/backend/enrichment.ts +0 -96
- package/src/backend/features.test.ts +0 -162
- package/src/backend/features.ts +0 -179
- package/src/backend/identity-ladder.test.ts +0 -117
- package/src/backend/identity-ladder.ts +0 -221
- package/src/backend/identity-registry.test.ts +0 -89
- package/src/backend/identity-registry.ts +0 -151
- package/src/backend/index.ts +0 -221
- package/src/backend/lifecycle-interlock.ts +0 -38
- package/src/backend/managed-objects.ts +0 -108
- package/src/backend/mapping.ts +0 -160
- package/src/backend/mm-backend.ts +0 -191
- package/src/backend/mm-mutations.ts +0 -228
- package/src/backend/modem-actor.test.ts +0 -95
- package/src/backend/modem-actor.ts +0 -112
- package/src/backend/nm-auto-apn.ts +0 -161
- package/src/backend/nm-gsm-fields.ts +0 -122
- package/src/backend/nmcli-nm-port.ts +0 -228
- package/src/backend/nmcli-runner.ts +0 -52
- package/src/backend/observer.ts +0 -297
- package/src/backend/power-contract.test.ts +0 -40
- package/src/backend/power-contract.ts +0 -83
- package/src/backend/recovery-attribution.test.ts +0 -102
- package/src/backend/recovery-attribution.ts +0 -86
- package/src/backend/recovery-budget.test.ts +0 -64
- package/src/backend/recovery-budget.ts +0 -84
- package/src/backend/recovery-ladder.test.ts +0 -257
- package/src/backend/recovery-ladder.ts +0 -249
- package/src/backend/router-ethernet.test.ts +0 -71
- package/src/backend/router-ethernet.ts +0 -90
- package/src/backend/row-store.ts +0 -105
- package/src/backend/signal-setup.ts +0 -112
- package/src/backend/sim-unlock.ts +0 -193
- package/src/backend/transition-preconditions.ts +0 -149
- package/src/backend/usage/accounting.test.ts +0 -147
- package/src/backend/usage/accounting.ts +0 -123
- package/src/backend/usage/billing-cycle.test.ts +0 -62
- package/src/backend/usage/billing-cycle.ts +0 -45
- package/src/backend/usage/index.ts +0 -37
- package/src/backend/usage/proc-net-dev.test.ts +0 -56
- package/src/backend/usage/sampler.test.ts +0 -219
- package/src/backend/usage/sampler.ts +0 -228
- package/src/backend/usage/store.test.ts +0 -148
- package/src/backend/usage/store.ts +0 -177
- package/src/backend/usb-enumerator.test.ts +0 -87
- package/src/backend/usb-enumerator.ts +0 -181
- package/src/backend/usb-mode-transition.test.ts +0 -323
- package/src/backend/usb-mode-transition.ts +0 -253
- package/src/domain/brand.ts +0 -29
- package/src/domain/errors.ts +0 -77
- package/src/domain/guards.test.ts +0 -218
- package/src/domain/guards.ts +0 -144
- package/src/domain/identity.test.ts +0 -83
- package/src/domain/identity.ts +0 -165
- package/src/domain/index.ts +0 -12
- package/src/domain/snapshot.test.ts +0 -266
- package/src/domain/snapshot.ts +0 -120
- package/src/index.test.ts +0 -6
- package/src/index.ts +0 -15
- package/src/ports/README.md +0 -61
- package/src/ports/forbidden-surface.test.ts +0 -241
- package/src/ports/modem-manager.ts +0 -72
- package/src/ports/network-manager.ts +0 -87
- package/src/ports/ops.ts +0 -60
- package/src/ports/ops.type-test.ts +0 -39
- package/src/ports/receipts.test.ts +0 -153
- package/src/ports/reconcile.test.ts +0 -152
- package/src/ports/reconcile.ts +0 -338
- package/src/ports/router.ts +0 -29
- package/src/redact.test.ts +0 -82
- package/src/redact.ts +0 -73
- package/src/transport/README.md +0 -65
- package/src/transport/calls.ts +0 -113
- package/src/transport/characterization.test.ts +0 -260
- package/src/transport/codec.test.ts +0 -118
- package/src/transport/codec.ts +0 -240
- package/src/transport/conformance-python.test.ts +0 -152
- package/src/transport/conformance-same-lib.test.ts +0 -115
- package/src/transport/dbus-native-lib.d.ts +0 -19
- package/src/transport/dbus-native.ts +0 -85
- package/src/transport/index.ts +0 -30
- package/src/transport/no-library-leak.test.ts +0 -61
- package/src/transport/reliability.test.ts +0 -173
- package/src/transport/signals.ts +0 -150
- package/src/transport/signature.ts +0 -110
- package/src/transport/test-support/fake-service.ts +0 -168
- package/src/transport/test-support/independent-producer.py +0 -110
- package/src/transport/test-support/private-bus.ts +0 -66
- package/src/transport/transport.ts +0 -250
- package/src/transport/types.ts +0 -118
- package/src/usb-mode/catalog-schema.test.ts +0 -181
- package/src/usb-mode/catalog-schema.ts +0 -113
- package/src/usb-mode/certified-catalog.json +0 -67
- package/src/usb-mode/index.ts +0 -27
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// Recovery budget — the bounded-attempts / cooldown / loop-stop circuit breaker.
|
|
2
|
-
//
|
|
3
|
-
// A permanently-broken modem must not be recovered forever. The budget caps how many
|
|
4
|
-
// attempts may fire within a cooldown window; once the cap is spent it latches the
|
|
5
|
-
// modem `degraded` (the loop-stop), and no further attempt is allowed until an
|
|
6
|
-
// explicit reset. This module is a PURE reducer: it owns no clock and mutates
|
|
7
|
-
// nothing — the caller supplies `now` and stores the returned next state.
|
|
8
|
-
|
|
9
|
-
import type { EpochMillis } from '../domain';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Recovery budget. `maxAttempts` recovery attempts are allowed before the loop-stop
|
|
13
|
-
* engages; two attempts sooner than `cooldownMs` apart are refused (cooldown).
|
|
14
|
-
*/
|
|
15
|
-
export interface RecoveryBudget {
|
|
16
|
-
readonly maxAttempts: number;
|
|
17
|
-
readonly cooldownMs: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** The Phase-A default: two attempts, 30s cooldown between them. */
|
|
21
|
-
export const DEFAULT_RECOVERY_BUDGET: RecoveryBudget = {
|
|
22
|
-
maxAttempts: 2,
|
|
23
|
-
cooldownMs: 30_000,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Per-modem budget bookkeeping. `degraded` is a LATCH: once the loop-stop fires it
|
|
28
|
-
* stays set until an explicit `markRecovered`, so a flapping modem is left degraded
|
|
29
|
-
* rather than retried forever.
|
|
30
|
-
*/
|
|
31
|
-
export interface RecoveryBudgetState {
|
|
32
|
-
readonly attempts: number;
|
|
33
|
-
readonly degraded: boolean;
|
|
34
|
-
readonly lastAttemptAt?: EpochMillis;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** A fresh, un-attempted budget state. */
|
|
38
|
-
export const INITIAL_BUDGET_STATE: RecoveryBudgetState = {
|
|
39
|
-
attempts: 0,
|
|
40
|
-
degraded: false,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** The verdict on whether a recovery attempt may proceed, plus the next state. */
|
|
44
|
-
export type BudgetDecision =
|
|
45
|
-
| { readonly kind: 'proceed'; readonly state: RecoveryBudgetState }
|
|
46
|
-
| {
|
|
47
|
-
readonly kind: 'cooldown';
|
|
48
|
-
readonly state: RecoveryBudgetState;
|
|
49
|
-
readonly retryAfter: EpochMillis;
|
|
50
|
-
}
|
|
51
|
-
| { readonly kind: 'loop-stop'; readonly state: RecoveryBudgetState };
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Decide whether a recovery attempt may proceed at `now`, returning the NEXT state:
|
|
55
|
-
* - already `degraded` → loop-stop (latched; zero further attempts)
|
|
56
|
-
* - within `cooldownMs` of last → cooldown (refused; attempts NOT incremented)
|
|
57
|
-
* - budget already spent → loop-stop (latch `degraded`)
|
|
58
|
-
* - otherwise → proceed (attempts + 1; stamp `lastAttemptAt`)
|
|
59
|
-
*/
|
|
60
|
-
export function beginAttempt(
|
|
61
|
-
state: RecoveryBudgetState,
|
|
62
|
-
budget: RecoveryBudget,
|
|
63
|
-
now: EpochMillis,
|
|
64
|
-
): BudgetDecision {
|
|
65
|
-
if (state.degraded) {
|
|
66
|
-
return { kind: 'loop-stop', state };
|
|
67
|
-
}
|
|
68
|
-
if (state.lastAttemptAt !== undefined && now - state.lastAttemptAt < budget.cooldownMs) {
|
|
69
|
-
const retryAfter = (state.lastAttemptAt + budget.cooldownMs) as EpochMillis;
|
|
70
|
-
return { kind: 'cooldown', state, retryAfter };
|
|
71
|
-
}
|
|
72
|
-
if (state.attempts >= budget.maxAttempts) {
|
|
73
|
-
return { kind: 'loop-stop', state: { ...state, degraded: true } };
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
kind: 'proceed',
|
|
77
|
-
state: { attempts: state.attempts + 1, degraded: false, lastAttemptAt: now },
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** A successful recovery clears the counter and the degraded latch. */
|
|
82
|
-
export function markRecovered(): RecoveryBudgetState {
|
|
83
|
-
return INITIAL_BUDGET_STATE;
|
|
84
|
-
}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
// The recovery ladder — the safety gates are the whole point:
|
|
2
|
-
// - disabled by default ⇒ literally ZERO side effects (throwing spies prove it)
|
|
3
|
-
// - only a confident modem-fault may ever be disruptive
|
|
4
|
-
// - budget → loop-stop marks a flapping modem degraded, never retried forever
|
|
5
|
-
// - a successful step restores health and resets the budget
|
|
6
|
-
// - rung 4 (power) is always unsupported in Phase A
|
|
7
|
-
// - disruptive rungs route through the shared per-modem actor (serialisation)
|
|
8
|
-
|
|
9
|
-
import { describe, expect, test } from 'bun:test';
|
|
10
|
-
import { type DesiredRecovery, epochMillis, runtimePath } from '../domain';
|
|
11
|
-
import { ALLOW_ALL_INTERLOCK, type LifecycleInterlock } from './lifecycle-interlock';
|
|
12
|
-
import { ModemActor } from './modem-actor';
|
|
13
|
-
import { NONE_POWER_CAPABILITY, type PowerHook } from './power-contract';
|
|
14
|
-
import { DEFAULT_RECOVERY_BUDGET } from './recovery-budget';
|
|
15
|
-
import {
|
|
16
|
-
RecoveryLadder,
|
|
17
|
-
type RecoveryLadderConfig,
|
|
18
|
-
type RecoveryRequest,
|
|
19
|
-
type RecoverySteps,
|
|
20
|
-
type StepOutcome,
|
|
21
|
-
} from './recovery-ladder';
|
|
22
|
-
|
|
23
|
-
const ENABLED: DesiredRecovery = { enabled: true };
|
|
24
|
-
const DISABLED: DesiredRecovery = { enabled: false };
|
|
25
|
-
|
|
26
|
-
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
-
|
|
28
|
-
interface StepCalls {
|
|
29
|
-
nmCycle: number;
|
|
30
|
-
mmCycle: number;
|
|
31
|
-
reset: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Steps that count calls and return a fixed outcome. */
|
|
35
|
-
function countingSteps(outcome: StepOutcome, calls: StepCalls): RecoverySteps {
|
|
36
|
-
return {
|
|
37
|
-
nmCycle: () => {
|
|
38
|
-
calls.nmCycle += 1;
|
|
39
|
-
return Promise.resolve(outcome);
|
|
40
|
-
},
|
|
41
|
-
mmCycle: () => {
|
|
42
|
-
calls.mmCycle += 1;
|
|
43
|
-
return Promise.resolve(outcome);
|
|
44
|
-
},
|
|
45
|
-
reset: () => {
|
|
46
|
-
calls.reset += 1;
|
|
47
|
-
return Promise.resolve(outcome);
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Steps that throw if ever invoked — the strongest "zero side effects" proof. */
|
|
53
|
-
const throwingSteps: RecoverySteps = {
|
|
54
|
-
nmCycle: () => Promise.reject(new Error('nmCycle must not fire')),
|
|
55
|
-
mmCycle: () => Promise.reject(new Error('mmCycle must not fire')),
|
|
56
|
-
reset: () => Promise.reject(new Error('reset must not fire')),
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const throwingPowerHook: PowerHook = {
|
|
60
|
-
capability: NONE_POWER_CAPABILITY,
|
|
61
|
-
cycle: () => Promise.reject(new Error('power hook must not fire')),
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const throwingInterlock: LifecycleInterlock = {
|
|
65
|
-
canDisrupt: () => Promise.reject(new Error('interlock must not be consulted')),
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
function makeRequest(overrides: Partial<RecoveryRequest> = {}): RecoveryRequest {
|
|
69
|
-
return {
|
|
70
|
-
stableKey: 'slot:a',
|
|
71
|
-
modem: runtimePath('/org/freedesktop/ModemManager1/Modem/0'),
|
|
72
|
-
attribution: 'modem-fault',
|
|
73
|
-
now: epochMillis(0),
|
|
74
|
-
probeHealthy: () => Promise.resolve(false),
|
|
75
|
-
...overrides,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
describe('RecoveryLadder — disabled by default fires ZERO steps', () => {
|
|
80
|
-
test('recovery.enabled=false makes zero side-effecting calls, even for a clear modem-fault', async () => {
|
|
81
|
-
const ladder = new RecoveryLadder({
|
|
82
|
-
actor: new ModemActor(),
|
|
83
|
-
steps: throwingSteps,
|
|
84
|
-
powerHook: throwingPowerHook,
|
|
85
|
-
interlock: throwingInterlock,
|
|
86
|
-
});
|
|
87
|
-
// If ANY of the throwing spies leaked through, run() would reject here.
|
|
88
|
-
const outcome = await ladder.run(DISABLED, makeRequest({ attribution: 'modem-fault' }));
|
|
89
|
-
expect(outcome.kind).toBe('disabled');
|
|
90
|
-
expect(outcome.steps).toEqual([]);
|
|
91
|
-
expect(outcome.degraded).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('RecoveryLadder — attribution gating (never disrupt on non-modem-fault)', () => {
|
|
96
|
-
for (const attribution of ['indeterminate', 'network-fault'] as const) {
|
|
97
|
-
test(`enabled + '${attribution}' fires zero disruptive steps`, async () => {
|
|
98
|
-
const ladder = new RecoveryLadder({
|
|
99
|
-
actor: new ModemActor(),
|
|
100
|
-
steps: throwingSteps,
|
|
101
|
-
powerHook: throwingPowerHook,
|
|
102
|
-
interlock: throwingInterlock,
|
|
103
|
-
});
|
|
104
|
-
const outcome = await ladder.run(ENABLED, makeRequest({ attribution }));
|
|
105
|
-
expect(outcome.kind).toBe('not-attributed');
|
|
106
|
-
expect(outcome.steps).toEqual([]);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('RecoveryLadder — budget, cooldown, loop-stop', () => {
|
|
112
|
-
test('a flapping modem gets EXACTLY 2 attempts, then loop-stop marks it degraded', async () => {
|
|
113
|
-
const calls: StepCalls = { nmCycle: 0, mmCycle: 0, reset: 0 };
|
|
114
|
-
const ladder = new RecoveryLadder({
|
|
115
|
-
actor: new ModemActor(),
|
|
116
|
-
steps: countingSteps({ status: 'failed', reason: 'still broken' }, calls),
|
|
117
|
-
config: { ...allowAll(), budget: { maxAttempts: 2, cooldownMs: 1000 } },
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const a1 = await ladder.run(ENABLED, makeRequest({ now: epochMillis(0) }));
|
|
121
|
-
const a2 = await ladder.run(ENABLED, makeRequest({ now: epochMillis(1000) }));
|
|
122
|
-
const a3 = await ladder.run(ENABLED, makeRequest({ now: epochMillis(2000) }));
|
|
123
|
-
|
|
124
|
-
expect(a1.kind).toBe('exhausted');
|
|
125
|
-
expect(a2.kind).toBe('exhausted');
|
|
126
|
-
expect(a3.kind).toBe('loop-stop');
|
|
127
|
-
// Rung 1 fired exactly twice — the third invocation short-circuited.
|
|
128
|
-
expect(calls.nmCycle).toBe(2);
|
|
129
|
-
expect(a3.degraded).toBe(true);
|
|
130
|
-
expect(ladder.budgetStateFor('slot:a').degraded).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('a second attempt within the cooldown window is refused with zero steps', async () => {
|
|
134
|
-
const calls: StepCalls = { nmCycle: 0, mmCycle: 0, reset: 0 };
|
|
135
|
-
const ladder = new RecoveryLadder({
|
|
136
|
-
actor: new ModemActor(),
|
|
137
|
-
steps: countingSteps({ status: 'failed', reason: 'still broken' }, calls),
|
|
138
|
-
config: { ...allowAll(), budget: { maxAttempts: 3, cooldownMs: 1000 } },
|
|
139
|
-
});
|
|
140
|
-
await ladder.run(ENABLED, makeRequest({ now: epochMillis(0) }));
|
|
141
|
-
const soon = await ladder.run(ENABLED, makeRequest({ now: epochMillis(500) }));
|
|
142
|
-
expect(soon.kind).toBe('cooldown');
|
|
143
|
-
expect(calls.nmCycle).toBe(1);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('RecoveryLadder — a successful step restores health', () => {
|
|
148
|
-
test('rung 1 succeeding + a healthy probe → recovered, later rungs never run, budget reset', async () => {
|
|
149
|
-
const calls: StepCalls = { nmCycle: 0, mmCycle: 0, reset: 0 };
|
|
150
|
-
const ladder = new RecoveryLadder({
|
|
151
|
-
actor: new ModemActor(),
|
|
152
|
-
steps: countingSteps({ status: 'applied', reason: 'nm reactivated' }, calls),
|
|
153
|
-
});
|
|
154
|
-
const outcome = await ladder.run(
|
|
155
|
-
ENABLED,
|
|
156
|
-
makeRequest({ probeHealthy: () => Promise.resolve(true) }),
|
|
157
|
-
);
|
|
158
|
-
expect(outcome.kind).toBe('recovered');
|
|
159
|
-
expect(calls.nmCycle).toBe(1);
|
|
160
|
-
expect(calls.mmCycle).toBe(0);
|
|
161
|
-
expect(calls.reset).toBe(0);
|
|
162
|
-
expect(ladder.budgetStateFor('slot:a').attempts).toBe(0);
|
|
163
|
-
expect(outcome.degraded).toBe(false);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('RecoveryLadder — rung 4 (power) is always unsupported', () => {
|
|
168
|
-
test('with rungs 1-3 gated off, the power rung runs and reports unsupported', async () => {
|
|
169
|
-
const ladder = new RecoveryLadder({
|
|
170
|
-
actor: new ModemActor(),
|
|
171
|
-
// Throwing steps prove rungs 1-3 are skipped (allowDisruptive:false), not run.
|
|
172
|
-
steps: throwingSteps,
|
|
173
|
-
config: {
|
|
174
|
-
nmCycle: { allowDisruptive: false },
|
|
175
|
-
mmCycle: { allowDisruptive: false },
|
|
176
|
-
reset: { allowDisruptive: false },
|
|
177
|
-
powerCycle: { allowDisruptive: true },
|
|
178
|
-
budget: DEFAULT_RECOVERY_BUDGET,
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
const outcome = await ladder.run(ENABLED, makeRequest());
|
|
182
|
-
expect(outcome.kind).toBe('exhausted');
|
|
183
|
-
const power = outcome.steps.find((s) => s.rung === 'powerCycle');
|
|
184
|
-
expect(power?.status).toBe('unsupported');
|
|
185
|
-
expect(power?.reason).toContain('none');
|
|
186
|
-
// The gated rungs are reported skipped, never executed.
|
|
187
|
-
expect(outcome.steps.filter((s) => s.status === 'skipped').map((s) => s.rung)).toEqual([
|
|
188
|
-
'nmCycle',
|
|
189
|
-
'mmCycle',
|
|
190
|
-
'reset',
|
|
191
|
-
]);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe('RecoveryLadder — interlock blocks disruption', () => {
|
|
196
|
-
test('a denying interlock stops the ladder before any step fires', async () => {
|
|
197
|
-
const calls: StepCalls = { nmCycle: 0, mmCycle: 0, reset: 0 };
|
|
198
|
-
const denying: LifecycleInterlock = {
|
|
199
|
-
canDisrupt: () => Promise.resolve({ allow: false, reason: 'modem is streaming' }),
|
|
200
|
-
};
|
|
201
|
-
const ladder = new RecoveryLadder({
|
|
202
|
-
actor: new ModemActor(),
|
|
203
|
-
steps: countingSteps({ status: 'applied', reason: 'x' }, calls),
|
|
204
|
-
interlock: denying,
|
|
205
|
-
});
|
|
206
|
-
const outcome = await ladder.run(ENABLED, makeRequest());
|
|
207
|
-
expect(outcome.kind).toBe('interlock-blocked');
|
|
208
|
-
expect(calls.nmCycle).toBe(0);
|
|
209
|
-
expect(outcome.steps[0]?.status).toBe('blocked');
|
|
210
|
-
expect(outcome.reason).toContain('streaming');
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe('RecoveryLadder — disruptive steps route through the shared actor', () => {
|
|
215
|
-
test('two same-key recoveries serialise their steps (no interleave)', async () => {
|
|
216
|
-
const order: string[] = [];
|
|
217
|
-
const slowSteps: RecoverySteps = {
|
|
218
|
-
nmCycle: async () => {
|
|
219
|
-
order.push('start');
|
|
220
|
-
await sleep(20);
|
|
221
|
-
order.push('end');
|
|
222
|
-
return { status: 'failed', reason: 'still broken' };
|
|
223
|
-
},
|
|
224
|
-
mmCycle: () => Promise.resolve({ status: 'failed', reason: 'x' }),
|
|
225
|
-
reset: () => Promise.resolve({ status: 'failed', reason: 'x' }),
|
|
226
|
-
};
|
|
227
|
-
const ladder = new RecoveryLadder({
|
|
228
|
-
actor: new ModemActor(),
|
|
229
|
-
// Only rung 1 is enabled so the serialisation of nmCycle is unambiguous.
|
|
230
|
-
steps: slowSteps,
|
|
231
|
-
interlock: ALLOW_ALL_INTERLOCK,
|
|
232
|
-
config: {
|
|
233
|
-
nmCycle: { allowDisruptive: true },
|
|
234
|
-
mmCycle: { allowDisruptive: false },
|
|
235
|
-
reset: { allowDisruptive: false },
|
|
236
|
-
powerCycle: { allowDisruptive: false },
|
|
237
|
-
budget: { maxAttempts: 5, cooldownMs: 0 },
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
await Promise.all([
|
|
241
|
-
ladder.run(ENABLED, makeRequest({ now: epochMillis(0) })),
|
|
242
|
-
ladder.run(ENABLED, makeRequest({ now: epochMillis(1) })),
|
|
243
|
-
]);
|
|
244
|
-
// Serialised through the actor ⇒ the first nmCycle fully completes before the second.
|
|
245
|
-
expect(order).toEqual(['start', 'end', 'start', 'end']);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
/** Every rung allowed — the shared base for budget-focused configs. */
|
|
250
|
-
function allowAll(): Omit<RecoveryLadderConfig, 'budget'> {
|
|
251
|
-
return {
|
|
252
|
-
nmCycle: { allowDisruptive: true },
|
|
253
|
-
mmCycle: { allowDisruptive: true },
|
|
254
|
-
reset: { allowDisruptive: true },
|
|
255
|
-
powerCycle: { allowDisruptive: true },
|
|
256
|
-
};
|
|
257
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
// The evidence-gated recovery ladder — disabled by default.
|
|
2
|
-
//
|
|
3
|
-
// A bounded four-rung ladder [1 nm-cycle: NM deactivate→reactivate exact pair; 2
|
|
4
|
-
// mm-cycle: MM disable→enable; 3 reset: MM Reset(); 4 power-cycle: power hook (only
|
|
5
|
-
// `none` → always unsupported)]. Every disruptive rung routes through A3.3's shared
|
|
6
|
-
// per-modem `ModemActor` (serialised behind all other disruptive ops) and consults
|
|
7
|
-
// the `LifecycleInterlock` first so it never disrupts a streaming modem. GATES, in
|
|
8
|
-
// order: recovery.enabled=false → zero steps · attribution≠modem-fault → zero steps ·
|
|
9
|
-
// budget spent / cooldown → zero steps · per-rung allowDisruptive · interlock.
|
|
10
|
-
// Disabled or un-attributed, NOT ONE side-effecting call is made.
|
|
11
|
-
|
|
12
|
-
import type { DesiredRecovery, EpochMillis } from '../domain';
|
|
13
|
-
import type { ModemRef } from '../ports';
|
|
14
|
-
import { ALLOW_ALL_INTERLOCK, type LifecycleInterlock } from './lifecycle-interlock';
|
|
15
|
-
import type { ModemActor } from './modem-actor';
|
|
16
|
-
import { NONE_POWER_HOOK, type PowerHook } from './power-contract';
|
|
17
|
-
import type { FaultAttribution } from './recovery-attribution';
|
|
18
|
-
import {
|
|
19
|
-
beginAttempt,
|
|
20
|
-
DEFAULT_RECOVERY_BUDGET,
|
|
21
|
-
INITIAL_BUDGET_STATE,
|
|
22
|
-
markRecovered,
|
|
23
|
-
type RecoveryBudget,
|
|
24
|
-
type RecoveryBudgetState,
|
|
25
|
-
} from './recovery-budget';
|
|
26
|
-
|
|
27
|
-
export const LADDER_ORDER = ['nmCycle', 'mmCycle', 'reset', 'powerCycle'] as const;
|
|
28
|
-
export type RecoveryRung = (typeof LADDER_ORDER)[number];
|
|
29
|
-
|
|
30
|
-
/** Context handed to each disruptive rung. */
|
|
31
|
-
export interface RecoveryStepContext {
|
|
32
|
-
readonly stableKey: string;
|
|
33
|
-
readonly modem: ModemRef;
|
|
34
|
-
readonly at: EpochMillis;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Outcome of a disruptive rung (1–3). */
|
|
38
|
-
export interface StepOutcome {
|
|
39
|
-
readonly status: 'applied' | 'failed';
|
|
40
|
-
readonly reason: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The disruptive recovery actions for rungs 1–3. Each is the raw effect only — the
|
|
45
|
-
* ladder wraps every call in `actor.run(stableKey, …)`, so serialisation is the
|
|
46
|
-
* ladder's job, not the step's. Phase A injects a fake in tests; the real D-Bus / NM
|
|
47
|
-
* implementation (hardware-gated) is wired at the composition root.
|
|
48
|
-
*/
|
|
49
|
-
export interface RecoverySteps {
|
|
50
|
-
/** Rung 1: NM deactivate then reactivate — an EXACT pair, never a bare deactivate. */
|
|
51
|
-
nmCycle(context: RecoveryStepContext): Promise<StepOutcome>;
|
|
52
|
-
/** Rung 2: MM disable then enable. */
|
|
53
|
-
mmCycle(context: RecoveryStepContext): Promise<StepOutcome>;
|
|
54
|
-
/** Rung 3: MM `Reset()`. */
|
|
55
|
-
reset(context: RecoveryStepContext): Promise<StepOutcome>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface RecoveryStepGate {
|
|
59
|
-
readonly allowDisruptive: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** The ladder's operational config: per-rung gates + the attempt budget. */
|
|
63
|
-
export interface RecoveryLadderConfig {
|
|
64
|
-
readonly nmCycle: RecoveryStepGate;
|
|
65
|
-
readonly mmCycle: RecoveryStepGate;
|
|
66
|
-
readonly reset: RecoveryStepGate;
|
|
67
|
-
readonly powerCycle: RecoveryStepGate;
|
|
68
|
-
readonly budget: RecoveryBudget;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const ALLOW: RecoveryStepGate = { allowDisruptive: true };
|
|
72
|
-
|
|
73
|
-
/** Default config: every rung permitted, default budget. */
|
|
74
|
-
export const DEFAULT_LADDER_CONFIG: RecoveryLadderConfig = {
|
|
75
|
-
nmCycle: ALLOW,
|
|
76
|
-
mmCycle: ALLOW,
|
|
77
|
-
reset: ALLOW,
|
|
78
|
-
powerCycle: ALLOW,
|
|
79
|
-
budget: DEFAULT_RECOVERY_BUDGET,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export interface RecoveryStepReport {
|
|
83
|
-
readonly rung: RecoveryRung;
|
|
84
|
-
readonly status: 'applied' | 'unsupported' | 'failed' | 'skipped' | 'blocked';
|
|
85
|
-
readonly reason: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** How a whole ladder invocation ended. */
|
|
89
|
-
export type RecoveryOutcomeKind =
|
|
90
|
-
| 'disabled'
|
|
91
|
-
| 'not-attributed'
|
|
92
|
-
| 'cooldown'
|
|
93
|
-
| 'loop-stop'
|
|
94
|
-
| 'interlock-blocked'
|
|
95
|
-
| 'recovered'
|
|
96
|
-
| 'exhausted';
|
|
97
|
-
|
|
98
|
-
/** The result of one ladder invocation. */
|
|
99
|
-
export interface RecoveryOutcome {
|
|
100
|
-
readonly kind: RecoveryOutcomeKind;
|
|
101
|
-
readonly attribution: FaultAttribution;
|
|
102
|
-
readonly steps: readonly RecoveryStepReport[];
|
|
103
|
-
readonly degraded: boolean;
|
|
104
|
-
readonly reason: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** One recovery request for one modem at one instant. */
|
|
108
|
-
export interface RecoveryRequest {
|
|
109
|
-
readonly stableKey: string;
|
|
110
|
-
readonly modem: ModemRef;
|
|
111
|
-
/** The pre-computed attribution (see `attributeFault`). */
|
|
112
|
-
readonly attribution: FaultAttribution;
|
|
113
|
-
readonly now: EpochMillis;
|
|
114
|
-
/** Re-checked AFTER an applied rung; `true` ⇒ the modem recovered (ladder stops). */
|
|
115
|
-
readonly probeHealthy: () => Promise<boolean>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Dependencies the ladder is constructed with. */
|
|
119
|
-
export interface RecoveryLadderDeps {
|
|
120
|
-
readonly actor: ModemActor;
|
|
121
|
-
readonly steps: RecoverySteps;
|
|
122
|
-
readonly powerHook?: PowerHook;
|
|
123
|
-
readonly interlock?: LifecycleInterlock;
|
|
124
|
-
readonly config?: RecoveryLadderConfig;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* The recovery ladder. Holds per-modem budget state keyed by stable key so the
|
|
129
|
-
* loop-stop can latch a flapping modem `degraded` across invocations.
|
|
130
|
-
*/
|
|
131
|
-
export class RecoveryLadder {
|
|
132
|
-
readonly #actor: ModemActor;
|
|
133
|
-
readonly #steps: RecoverySteps;
|
|
134
|
-
readonly #powerHook: PowerHook;
|
|
135
|
-
readonly #interlock: LifecycleInterlock;
|
|
136
|
-
readonly #config: RecoveryLadderConfig;
|
|
137
|
-
readonly #states = new Map<string, RecoveryBudgetState>();
|
|
138
|
-
|
|
139
|
-
constructor(deps: RecoveryLadderDeps) {
|
|
140
|
-
this.#actor = deps.actor;
|
|
141
|
-
this.#steps = deps.steps;
|
|
142
|
-
this.#powerHook = deps.powerHook ?? NONE_POWER_HOOK;
|
|
143
|
-
this.#interlock = deps.interlock ?? ALLOW_ALL_INTERLOCK;
|
|
144
|
-
this.#config = deps.config ?? DEFAULT_LADDER_CONFIG;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** The current budget state for a modem (for observability / assertions). */
|
|
148
|
-
budgetStateFor(stableKey: string): RecoveryBudgetState {
|
|
149
|
-
return this.#states.get(stableKey) ?? INITIAL_BUDGET_STATE;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Clear a modem's budget / degraded latch (operator un-degrade). */
|
|
153
|
-
clear(stableKey: string): void {
|
|
154
|
-
this.#states.delete(stableKey);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Run one recovery attempt. Enforces every gate before any side effect. */
|
|
158
|
-
async run(recovery: DesiredRecovery, request: RecoveryRequest): Promise<RecoveryOutcome> {
|
|
159
|
-
const { stableKey, attribution } = request;
|
|
160
|
-
|
|
161
|
-
// GATE 1 — master switch. Disabled ⇒ literally zero steps, zero side effects.
|
|
162
|
-
if (!recovery.enabled) {
|
|
163
|
-
return this.#end('disabled', request, [], 'recovery disabled by policy');
|
|
164
|
-
}
|
|
165
|
-
// GATE 2 — attribution. Only a confident modem-fault may ever be disruptive.
|
|
166
|
-
if (attribution !== 'modem-fault') {
|
|
167
|
-
return this.#end(
|
|
168
|
-
'not-attributed',
|
|
169
|
-
request,
|
|
170
|
-
[],
|
|
171
|
-
`attribution '${attribution}' is never disruptive`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
// GATE 3 — budget / cooldown / loop-stop.
|
|
175
|
-
const decision = beginAttempt(this.budgetStateFor(stableKey), this.#config.budget, request.now);
|
|
176
|
-
if (decision.kind === 'loop-stop') {
|
|
177
|
-
this.#states.set(stableKey, decision.state);
|
|
178
|
-
return this.#end(
|
|
179
|
-
'loop-stop',
|
|
180
|
-
request,
|
|
181
|
-
[],
|
|
182
|
-
'recovery budget exhausted — modem marked degraded',
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
if (decision.kind === 'cooldown') {
|
|
186
|
-
return this.#end('cooldown', request, [], 'within cooldown window — attempt refused');
|
|
187
|
-
}
|
|
188
|
-
this.#states.set(stableKey, decision.state);
|
|
189
|
-
|
|
190
|
-
return this.#runRungs(request);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async #runRungs(request: RecoveryRequest): Promise<RecoveryOutcome> {
|
|
194
|
-
const { stableKey } = request;
|
|
195
|
-
const context: RecoveryStepContext = { stableKey, modem: request.modem, at: request.now };
|
|
196
|
-
const steps: RecoveryStepReport[] = [];
|
|
197
|
-
|
|
198
|
-
for (const rung of LADDER_ORDER) {
|
|
199
|
-
if (!this.#config[rung].allowDisruptive) {
|
|
200
|
-
steps.push({ rung, status: 'skipped', reason: 'allowDisruptive is false' });
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
// Interlock BEFORE any disruptive step — never disrupt a streaming modem.
|
|
204
|
-
const verdict = await this.#interlock.canDisrupt({ stableKey });
|
|
205
|
-
if (!verdict.allow) {
|
|
206
|
-
steps.push({ rung, status: 'blocked', reason: verdict.reason });
|
|
207
|
-
return this.#end(
|
|
208
|
-
'interlock-blocked',
|
|
209
|
-
request,
|
|
210
|
-
steps,
|
|
211
|
-
`interlock blocked '${rung}': ${verdict.reason}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
const report = await this.#runRung(rung, context);
|
|
215
|
-
steps.push(report);
|
|
216
|
-
if (report.status === 'applied' && (await request.probeHealthy())) {
|
|
217
|
-
this.#states.set(stableKey, markRecovered());
|
|
218
|
-
return this.#end('recovered', request, steps, `recovered at rung '${rung}'`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return this.#end('exhausted', request, steps, 'ladder exhausted without restoring health');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async #runRung(rung: RecoveryRung, context: RecoveryStepContext): Promise<RecoveryStepReport> {
|
|
225
|
-
if (rung === 'powerCycle') {
|
|
226
|
-
const result = await this.#powerHook.cycle({ stableKey: context.stableKey, at: context.at });
|
|
227
|
-
return { rung, status: result.status, reason: result.reason };
|
|
228
|
-
}
|
|
229
|
-
// Rungs 1–3 route through the shared per-modem actor for serialisation.
|
|
230
|
-
const step = this.#steps[rung];
|
|
231
|
-
const outcome = await this.#actor.run(context.stableKey, () => step(context));
|
|
232
|
-
return { rung, status: outcome.status, reason: outcome.reason };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
#end(
|
|
236
|
-
kind: RecoveryOutcomeKind,
|
|
237
|
-
request: RecoveryRequest,
|
|
238
|
-
steps: readonly RecoveryStepReport[],
|
|
239
|
-
reason: string,
|
|
240
|
-
): RecoveryOutcome {
|
|
241
|
-
return {
|
|
242
|
-
kind,
|
|
243
|
-
attribution: request.attribution,
|
|
244
|
-
steps,
|
|
245
|
-
degraded: this.budgetStateFor(request.stableKey).degraded,
|
|
246
|
-
reason,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// The router-ethernet probe is ADVISORY: presence, gateway reachability, and egress
|
|
2
|
-
// health are reported, never gated. `checkHealth` must never throw — a throwing probe
|
|
3
|
-
// degrades to `false`, it does not blow up the caller.
|
|
4
|
-
|
|
5
|
-
import { describe, expect, test } from 'bun:test';
|
|
6
|
-
import { deviceIfname } from '../ports';
|
|
7
|
-
import { createRouterEthernetProbe, type RouterEthernetProbeDeps } from './router-ethernet';
|
|
8
|
-
|
|
9
|
-
const IFNAME = deviceIfname('eth1');
|
|
10
|
-
|
|
11
|
-
function probeWith(overrides: RouterEthernetProbeDeps) {
|
|
12
|
-
return createRouterEthernetProbe(overrides);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('createRouterEthernetProbe — presence', () => {
|
|
16
|
-
test('a link that is up is present; a link that is down is absent', async () => {
|
|
17
|
-
const up = probeWith({ checkLinkUp: () => Promise.resolve(true) });
|
|
18
|
-
const down = probeWith({ checkLinkUp: () => Promise.resolve(false) });
|
|
19
|
-
expect(await up.probePresence(IFNAME)).toBe('present');
|
|
20
|
-
expect(await down.probePresence(IFNAME)).toBe('absent');
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('createRouterEthernetProbe — advisory health', () => {
|
|
25
|
-
test('present + gateway + egress all healthy', async () => {
|
|
26
|
-
const probe = probeWith({
|
|
27
|
-
checkLinkUp: () => Promise.resolve(true),
|
|
28
|
-
resolveGateway: () => Promise.resolve('192.168.8.1'),
|
|
29
|
-
ping: () => Promise.resolve(true),
|
|
30
|
-
});
|
|
31
|
-
const health = await probe.checkHealth(IFNAME);
|
|
32
|
-
expect(health.presence).toBe('present');
|
|
33
|
-
expect(health.gatewayReachable).toBe(true);
|
|
34
|
-
expect(health.egressHealthy).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('present but no gateway → degraded health, but still returned (not thrown)', async () => {
|
|
38
|
-
const probe = probeWith({
|
|
39
|
-
checkLinkUp: () => Promise.resolve(true),
|
|
40
|
-
resolveGateway: () => Promise.resolve(undefined),
|
|
41
|
-
ping: () => Promise.resolve(true),
|
|
42
|
-
});
|
|
43
|
-
const health = await probe.checkHealth(IFNAME);
|
|
44
|
-
expect(health.presence).toBe('present');
|
|
45
|
-
expect(health.gatewayReachable).toBe(false);
|
|
46
|
-
expect(health.egressHealthy).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('gateway reachable but egress down → egressHealthy false only', async () => {
|
|
50
|
-
const probe = probeWith({
|
|
51
|
-
checkLinkUp: () => Promise.resolve(true),
|
|
52
|
-
resolveGateway: () => Promise.resolve('192.168.8.1'),
|
|
53
|
-
ping: (host) => Promise.resolve(host === '192.168.8.1'),
|
|
54
|
-
});
|
|
55
|
-
const health = await probe.checkHealth(IFNAME);
|
|
56
|
-
expect(health.gatewayReachable).toBe(true);
|
|
57
|
-
expect(health.egressHealthy).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('checkHealth NEVER throws, even when a probe throws', async () => {
|
|
61
|
-
const probe = probeWith({
|
|
62
|
-
checkLinkUp: () => Promise.reject(new Error('ip crashed')),
|
|
63
|
-
resolveGateway: () => Promise.reject(new Error('route crashed')),
|
|
64
|
-
ping: () => Promise.reject(new Error('ping crashed')),
|
|
65
|
-
});
|
|
66
|
-
const health = await probe.checkHealth(IFNAME);
|
|
67
|
-
expect(health.presence).toBe('absent');
|
|
68
|
-
expect(health.gatewayReachable).toBe(false);
|
|
69
|
-
expect(health.egressHealthy).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
});
|