@danceiny/gotry 0.0.1-rc.8 → 0.2.0-rc.27
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 +172 -217
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1744 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -49
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +135 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +546 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +968 -0
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/session/action-cache.js +126 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +141 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +54 -0
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +50 -0
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +107 -0
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +109 -0
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +686 -0
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +72 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-tests.js +116 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +68 -10
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1479 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/companion-tests.js +67 -0
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +564 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +41 -0
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +569 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +480 -0
- package/dist/scripts/ledger-workflow-crash.js +32 -0
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +49 -0
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +29 -31
- package/dist/scripts/memory-value-report.js +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +161 -0
- package/dist/scripts/nightly-evidence.js +286 -0
- package/dist/scripts/nudge-digest.js +43 -42
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +31 -0
- package/dist/scripts/session-attach-poc.js +62 -0
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +60 -0
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +29 -0
- package/dist/scripts/session-tests.js +862 -0
- package/dist/scripts/session-zones-tests.js +358 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +703 -219
- package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +261 -0
- package/dist/scripts/state-cli.js +585 -0
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +59 -0
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +41 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +86 -0
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2305 -731
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +358 -162
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +47 -0
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zones.js +507 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +767 -0
- package/dist/src/state-repair.js +299 -0
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +59 -0
- package/dist/src/turn-deadline.js +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +538 -360
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +255 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +296 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +568 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +2107 -224
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +278 -40
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +912 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +360 -89
- package/ts/src/wish-pool.ts +79 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ensureLedger } from '../src/state-ledger.js';
|
|
6
|
+
import { bookingDigest } from '../src/booking-surface/runtime.js';
|
|
7
|
+
import { BookingCopilotProofRuntime as BookingCopilotTaskRuntime } from './booking-copilot-proof-runtime.js';
|
|
8
|
+
const offerVerifiedIntent = { schemaVersion: 'booking.intent.v1', target: 'offer.verified' };
|
|
9
|
+
const workspace = (revision) => ({
|
|
10
|
+
schemaVersion: 'booking.surface', contextRef: 'ctx-ledger-binding', surface: 'tenant', revision,
|
|
11
|
+
locale: 'en-US', currency: 'AED', searchDraft: {}, results: { status: 'idle' },
|
|
12
|
+
visibleHotels: [{ hotelRef: 'h1', name: 'H1', factRefs: [] }, { hotelRef: 'h2', name: 'H2', factRefs: [] }],
|
|
13
|
+
loadedOffers: [{ offerRef: 'o1', offerVersionRef: 'o1:v1', hotelRef: 'h1', evidenceLevel: 'rate_loaded', factRefs: [] }, { offerRef: 'o2', offerVersionRef: 'o2:v1', hotelRef: 'h2', evidenceLevel: 'rate_loaded', factRefs: [] }],
|
|
14
|
+
shortlistedOfferRefs: ['o1', 'o2'], capabilities: { surface: 'tenant', allowedActions: ['offers.query', 'offer.check'] },
|
|
15
|
+
});
|
|
16
|
+
const offersReceipt = (actionId, revision) => ({
|
|
17
|
+
schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-ledger-binding', status: 'applied', revision,
|
|
18
|
+
observation: { kind: 'offers.state', hotelRefs: ['h1'], offerRefs: ['o1'], loadedHotelCount: 1 },
|
|
19
|
+
resultContract: { outcome: 'complete', hardCriteriaMet: true, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] },
|
|
20
|
+
});
|
|
21
|
+
const checkReceipt = (actionId, revision) => ({
|
|
22
|
+
schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-ledger-binding', status: 'unavailable', revision,
|
|
23
|
+
observation: { kind: 'offer.availability', offerRef: 'o1', checkedOfferVersionRef: 'o1:v1', available: false, changedFactRefs: [], gapCodes: [] },
|
|
24
|
+
resultContract: { outcome: 'empty', hardCriteriaMet: false, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] },
|
|
25
|
+
});
|
|
26
|
+
function fixture(stage) {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), 'gotry-availability-ledger-binding-'));
|
|
28
|
+
const ledger = ensureLedger(root);
|
|
29
|
+
const runtime = new BookingCopilotTaskRuntime(ledger, { contextRefFactory: () => 'ctx-ledger-binding' });
|
|
30
|
+
const task = 'ledger-binding-task';
|
|
31
|
+
runtime.startTask({ schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task, turnId: 'turn-1', workspace: workspace(0), request: { text: 'find rates' } });
|
|
32
|
+
const query = { schemaVersion: 'booking.surface', kind: 'offers.query', actionId: 'query-1', contextRef: 'ctx-ledger-binding', expectedRevision: 0, reason: 'load offers', factRefs: [], input: { hotelRefs: ['h1'], criteria: {} } };
|
|
33
|
+
runtime.issueOperation(task, query, offerVerifiedIntent);
|
|
34
|
+
runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: task, workspace: workspace(1), receipt: offersReceipt('query-1', 1) });
|
|
35
|
+
const check = { schemaVersion: 'booking.surface', kind: 'offer.check', actionId: 'check-1', contextRef: 'ctx-ledger-binding', expectedRevision: 1, reason: 'check', factRefs: [], input: { offerRef: 'o1', offerVersionRef: 'o1:v1' } };
|
|
36
|
+
runtime.issueOperation(task, check, offerVerifiedIntent);
|
|
37
|
+
if (stage === 'receipt')
|
|
38
|
+
runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: task, workspace: { ...workspace(2), shortlistedOfferRefs: ['o2'], selectedOfferRef: undefined, verifiedOffer: undefined }, receipt: checkReceipt('check-1', 2) });
|
|
39
|
+
ledger.close();
|
|
40
|
+
return { root, ledger, task };
|
|
41
|
+
}
|
|
42
|
+
function rewrite(root, kind, mutate) {
|
|
43
|
+
const ledger = ensureLedger(root);
|
|
44
|
+
const row = ledger.db.prepare('SELECT seq, payload FROM events WHERE run_id = ? AND kind = ? ORDER BY seq DESC LIMIT 1').get('ledger-binding-task', kind);
|
|
45
|
+
assert.ok(row);
|
|
46
|
+
const payload = JSON.parse(row.payload);
|
|
47
|
+
mutate(payload);
|
|
48
|
+
ledger.db.prepare('UPDATE events SET payload = ? WHERE seq = ?').run(JSON.stringify(payload), row.seq);
|
|
49
|
+
ledger.close();
|
|
50
|
+
}
|
|
51
|
+
function assertCorrupt(root, label) {
|
|
52
|
+
const ledger = ensureLedger(root);
|
|
53
|
+
const runtime = new BookingCopilotTaskRuntime(ledger, { contextRefFactory: () => 'ctx-ledger-binding' });
|
|
54
|
+
assert.throws(() => runtime.resumeTask('ledger-binding-task'), /ledger_corrupt:ledger-binding-task/, label);
|
|
55
|
+
ledger.close();
|
|
56
|
+
}
|
|
57
|
+
// The untouched durable sequence is replayable after reopening the ledger.
|
|
58
|
+
{
|
|
59
|
+
const fixtureState = fixture('action');
|
|
60
|
+
const ledger = ensureLedger(fixtureState.root);
|
|
61
|
+
assert.doesNotThrow(() => new BookingCopilotTaskRuntime(ledger).resumeTask(fixtureState.task));
|
|
62
|
+
ledger.close();
|
|
63
|
+
rmSync(fixtureState.root, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
for (const [label, mutate] of [
|
|
66
|
+
['candidate digest', (payload) => { payload.availability.candidateSetDigest = 'f'.repeat(64); payload.availabilityDigest = bookingDigest(payload.availability); }],
|
|
67
|
+
['offer set digest', (payload) => { payload.availability.hotels.h1.currentGeneration.offerSetDigest = 'e'.repeat(64); payload.availabilityDigest = bookingDigest(payload.availability); }],
|
|
68
|
+
['source receipt digest', (payload) => { payload.availability.hotels.h1.currentGeneration.source.receiptDigest = 'd'.repeat(64); payload.availabilityDigest = bookingDigest(payload.availability); }],
|
|
69
|
+
['active ordinal', (payload) => { payload.availability.activeHotelOrdinal = 1; payload.availabilityDigest = bookingDigest(payload.availability); }],
|
|
70
|
+
['fake terminal', (payload) => { payload.availability.terminal = { code: 'availability_confirmed', hotelRefs: ['h1', 'h2'], reason: 'confirmed', evidence: 'conclusive' }; payload.availability.availabilityPhase = 'terminal'; payload.availabilityDigest = bookingDigest(payload.availability); }],
|
|
71
|
+
]) {
|
|
72
|
+
const fixtureState = fixture('action');
|
|
73
|
+
rewrite(fixtureState.root, 'booking.copilot.action.issued', mutate);
|
|
74
|
+
assertCorrupt(fixtureState.root, label);
|
|
75
|
+
rmSync(fixtureState.root, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
// A digest mismatch must be rejected before any legacy/normalized availability
|
|
78
|
+
// migration can make the payload appear coherent.
|
|
79
|
+
{
|
|
80
|
+
const fixtureState = fixture('action');
|
|
81
|
+
rewrite(fixtureState.root, 'booking.copilot.action.issued', (payload) => { payload.availability.activeHotelOrdinal = 1; });
|
|
82
|
+
assertCorrupt(fixtureState.root, 'raw ACTION availability digest mismatch');
|
|
83
|
+
rmSync(fixtureState.root, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
{
|
|
86
|
+
const fixtureState = fixture('receipt');
|
|
87
|
+
rewrite(fixtureState.root, 'booking.copilot.receipt.observed', (payload) => { payload.availability.activeHotelOrdinal = 1; });
|
|
88
|
+
assertCorrupt(fixtureState.root, 'raw RECEIPT availability digest mismatch');
|
|
89
|
+
rmSync(fixtureState.root, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
// A forged action can be structurally valid and re-digested, but cannot change
|
|
92
|
+
// the transition that the prior state and workspace authorize.
|
|
93
|
+
{
|
|
94
|
+
const fixtureState = fixture('action');
|
|
95
|
+
rewrite(fixtureState.root, 'booking.copilot.action.issued', (payload) => {
|
|
96
|
+
payload.action.input.offerRef = 'o2';
|
|
97
|
+
const { actionDigest: _ignored, ...base } = payload.action;
|
|
98
|
+
payload.action.actionDigest = bookingDigest(base);
|
|
99
|
+
});
|
|
100
|
+
assertCorrupt(fixtureState.root, 'wrong action transition');
|
|
101
|
+
rmSync(fixtureState.root, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
// The receipt shape and canonical receipt digest can both be valid while the
|
|
104
|
+
// availability transition is forged; replay must still derive the mismatch.
|
|
105
|
+
{
|
|
106
|
+
const fixtureState = fixture('receipt');
|
|
107
|
+
const runtimeLedger = ensureLedger(fixtureState.root);
|
|
108
|
+
const runtime = new BookingCopilotTaskRuntime(runtimeLedger);
|
|
109
|
+
assert.deepEqual(runtime.resumeTask(fixtureState.task)?.availability.criteria, {}, 'recovery retains the canonical offer criteria after restart');
|
|
110
|
+
const wrongDetour = { schemaVersion: 'booking.surface', kind: 'search.run', actionId: 'detour', contextRef: 'ctx-ledger-binding', expectedRevision: 2, reason: 'detour', factRefs: [], input: {} };
|
|
111
|
+
const activeIntent = runtime.resumeTask(fixtureState.task)?.activeIntent?.projection;
|
|
112
|
+
assert.ok(activeIntent);
|
|
113
|
+
assert.throws(() => runtime.applyDecisionBatch(fixtureState.task, 'detour-batch', [{ kind: 'operation', action: wrongDetour, intent: activeIntent }]), /availability_operation_incompatible/);
|
|
114
|
+
runtimeLedger.close();
|
|
115
|
+
rmSync(fixtureState.root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
{
|
|
118
|
+
const fixtureState = fixture('receipt');
|
|
119
|
+
rewrite(fixtureState.root, 'booking.copilot.receipt.observed', (payload) => {
|
|
120
|
+
payload.receipt.status = 'applied';
|
|
121
|
+
payload.receiptDigest = bookingDigest(payload.receipt);
|
|
122
|
+
});
|
|
123
|
+
assertCorrupt(fixtureState.root, 'wrong receipt transition');
|
|
124
|
+
rmSync(fixtureState.root, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
console.log('BOOKING COPILOT AVAILABILITY LEDGER BINDING: exact replay and candidate/generation/source/ordinal/terminal/action/receipt tamper rejection OK');
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ensureLedger } from '../src/state-ledger.js';
|
|
6
|
+
import { BookingCopilotProofRuntime as BookingCopilotTaskRuntime } from './booking-copilot-proof-runtime.js';
|
|
7
|
+
import { createAvailabilityPolicy, digest, MAX_HOTELS_PER_TASK, MAX_OFFER_CHECKS_PER_HOTEL, MAX_OFFER_QUERIES_PER_HOTEL, recordObservedOffersQuery, recordOfferCheckIssued, recordOfferCheckReceipt, recordOffersGeneration, recordOffersQueryIssued, canIssueOfferCheck } from '../src/booking-surface/availability-policy.js';
|
|
8
|
+
import { bookingSurfaceAllowedActions, bookingSurfaceFleetFeatures, bookingSurfaceNegotiatedActions } from '../src/booking-surface/contracts.js';
|
|
9
|
+
const versionFor = (offerRef, suffix = 'v1') => `${offerRef}:${suffix}`;
|
|
10
|
+
const verifiedCapability = (offerRef, offerVersionRef = versionFor(offerRef), expiresAt = '2026-09-01T10:30:00.000Z') => ({ offerRef, offerVersionRef, verifiedOfferRef: `verified-${offerRef}`, expiresAt });
|
|
11
|
+
const ws = (revision, hotels, offers, shortlistedOfferRefs = [], verifiedOffer) => ({ schemaVersion: 'booking.surface', contextRef: 'ctx-availability', surface: 'tenant', revision, locale: 'en-US', currency: 'AED', searchDraft: {}, results: { status: 'idle' }, visibleHotels: hotels.map(h => ({ hotelRef: h, name: h, factRefs: [] })), loadedOffers: offers.map(([offerRef, hotelRef, offerVersionRef = versionFor(offerRef)]) => ({ offerRef, offerVersionRef, hotelRef, evidenceLevel: 'rate_loaded', factRefs: [] })), shortlistedOfferRefs, capabilities: { surface: 'tenant', allowedActions: ['offers.query', 'offer.check'] }, ...(verifiedOffer ? { verifiedOffer } : {}) });
|
|
12
|
+
const receipt = (actionId, revision, offerRef, status = 'unavailable', available = false, checkedOfferVersionRef = versionFor(offerRef), currentOfferVersionRef) => ({ schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-availability', status, revision, observation: { kind: 'offer.availability', offerRef, checkedOfferVersionRef, currentOfferVersionRef: available ? (currentOfferVersionRef ?? checkedOfferVersionRef) : undefined, available, verifiedOfferRef: available ? 'verified-' + offerRef : undefined, changedFactRefs: [], gapCodes: [] }, resultContract: { outcome: available ? 'complete' : 'empty', hardCriteriaMet: available, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } });
|
|
13
|
+
const foldQuery = (state, hotelRefs, workspace, actionId, queryReceipt, criteriaDigest = '') => recordOffersGeneration(state, hotelRefs, workspace, actionId, digest(queryReceipt), queryReceipt, criteriaDigest);
|
|
14
|
+
const criteria = {};
|
|
15
|
+
const offerVerifiedIntent = { schemaVersion: 'booking.intent.v1', target: 'offer.verified' };
|
|
16
|
+
const offersReceipt = (actionId, revision, hotelRefs, offerRefs, loadedHotelCount, status = 'applied', outcome = 'complete', hardCriteriaMet = true) => ({ schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-availability', status, revision, observation: { kind: 'offers.state', hotelRefs, offerRefs, loadedHotelCount, gapCodes: [] }, resultContract: { outcome, hardCriteriaMet, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } });
|
|
17
|
+
const gapReceipt = (actionId, revision, status = 'failed') => ({ schemaVersion: 'booking.surface', kind: 'action.receipt', actionId, contextRef: 'ctx-availability', status, revision, observation: { kind: 'gap', code: 'hotel_rates_failed', factRefs: [] }, resultContract: { outcome: 'partial', hardCriteriaMet: false, factRefs: [], gapCodes: ['hotel_rates_failed'], blockers: [], relaxationsApplied: [] } });
|
|
18
|
+
// UAT-facing product availability observation: these are exact least-privilege
|
|
19
|
+
// sets, and are intentionally separate from the full tenant action vocabulary.
|
|
20
|
+
assert.deepEqual(bookingSurfaceAllowedActions('storefront'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus']);
|
|
21
|
+
assert.deepEqual(bookingSurfaceAllowedActions('payment_link'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus', 'hotel.select']);
|
|
22
|
+
assert.deepEqual(bookingSurfaceAllowedActions('tenant'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus', 'offers.query', 'offers.view.patch', 'offers.compare', 'offer.select', 'offer.check', 'checkout.prepare', 'order.observe']);
|
|
23
|
+
assert.deepEqual(bookingSurfaceAllowedActions('customer_portal'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus', 'offers.query', 'offers.view.patch', 'offers.compare', 'offer.select', 'offer.check', 'checkout.prepare', 'order.observe']);
|
|
24
|
+
assert.equal(bookingSurfaceAllowedActions('tenant').includes('hotel.select'), false);
|
|
25
|
+
assert.equal(bookingSurfaceAllowedActions('customer_portal').includes('hotel.select'), false);
|
|
26
|
+
// Additive compatibility is negotiated from the fleet intersection, never
|
|
27
|
+
// from a schema hash or a single lucky load-balanced probe.
|
|
28
|
+
assert.deepEqual(bookingSurfaceFleetFeatures([{ features: ['trusted-order-observation-v1'] }]), ['trusted-order-observation-v1']);
|
|
29
|
+
assert.deepEqual(bookingSurfaceFleetFeatures([{ features: ['trusted-order-observation-v1'] }, {}]), [], 'an old peer with no feature advertisement disables the additive field');
|
|
30
|
+
assert.deepEqual(bookingSurfaceFleetFeatures([{ features: ['trusted-order-observation-v1'] }, { features: [] }]), [], 'a mixed rolling fleet uses the safe intersection');
|
|
31
|
+
assert.deepEqual(bookingSurfaceFleetFeatures([{ features: ['trusted-order-observation-v1', 'future-feature'] }]), ['trusted-order-observation-v1'], 'unknown future features do not poison known negotiation');
|
|
32
|
+
assert.equal(bookingSurfaceNegotiatedActions('tenant', [{ schemaSha256: 'same-hash-is-not-authority' }]).includes('order.observe'), false, 'schema hash never enables an additive action');
|
|
33
|
+
assert.equal(bookingSurfaceNegotiatedActions('tenant', [{ features: ['trusted-order-observation-v1'] }]).includes('order.observe'), true, 'the advertised fleet feature enables order observation');
|
|
34
|
+
assert.deepEqual(bookingSurfaceNegotiatedActions('storefront', [{ features: ['trusted-order-observation-v1'] }]), bookingSurfaceAllowedActions('storefront'), 'feature negotiation cannot expand the product surface matrix');
|
|
35
|
+
// Lifetime budget mutation guard: these assertions exercise the transitions,
|
|
36
|
+
// not just the exported constants, so changing a comparison or reset policy
|
|
37
|
+
// makes this proof fail.
|
|
38
|
+
{
|
|
39
|
+
assert.equal(MAX_HOTELS_PER_TASK, 5);
|
|
40
|
+
assert.equal(MAX_OFFER_CHECKS_PER_HOTEL, 2);
|
|
41
|
+
assert.equal(MAX_OFFER_QUERIES_PER_HOTEL, 2);
|
|
42
|
+
const unavailableWorkspace = ws(0, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]);
|
|
43
|
+
let exhausted = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'budget-check-1');
|
|
44
|
+
exhausted = recordOfferCheckReceipt(exhausted, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), receipt('budget-check-1', 1, 'budget-hotel:ratepkg-1'), 'budget-check-1', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
|
|
45
|
+
assert.equal(exhausted.hotels['budget-hotel']?.freshOffersRequired, true, 'first unavailable opens exactly one fresh-generation transition');
|
|
46
|
+
exhausted = recordOffersQueryIssued(exhausted, ['budget-hotel'], ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'budget-query-1');
|
|
47
|
+
const reusedReceipt = offersReceipt('budget-query-1', 2, ['budget-hotel'], ['budget-hotel:ratepkg-1'], 1);
|
|
48
|
+
const reused = recordOffersGeneration(exhausted, ['budget-hotel'], ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'budget-query-1', digest(reusedReceipt), reusedReceipt);
|
|
49
|
+
assert.deepEqual(reused.hotels['budget-hotel']?.currentOfferRefs, [], 'fresh HotelRates cannot re-admit the checked stale OfferRef');
|
|
50
|
+
assert.equal(reused.hotels['budget-hotel']?.freshOffersRequired, true, 'stale OfferRef reuse leaves an explicit refresh transition');
|
|
51
|
+
const refreshedReceipt = offersReceipt('budget-query-1', 2, ['budget-hotel'], ['budget-hotel:ratepkg-2'], 1);
|
|
52
|
+
exhausted = recordOffersGeneration(exhausted, ['budget-hotel'], ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-2', 'budget-hotel']]), 'budget-query-1', digest(refreshedReceipt), refreshedReceipt);
|
|
53
|
+
exhausted = recordOfferCheckIssued(exhausted, ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-2', 'budget-hotel']]), 'budget-hotel:ratepkg-2', versionFor('budget-hotel:ratepkg-2'), 'budget-check-2');
|
|
54
|
+
exhausted = recordOfferCheckReceipt(exhausted, ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-2', 'budget-hotel']]), receipt('budget-check-2', 3, 'budget-hotel:ratepkg-2'), 'budget-check-2', 'budget-hotel:ratepkg-2', versionFor('budget-hotel:ratepkg-2'), 2);
|
|
55
|
+
assert.equal(exhausted.hotels['budget-hotel']?.checksIssued, 2, 'second unavailable consumes the lifetime CheckAvail budget');
|
|
56
|
+
assert.equal(exhausted.terminal?.code, 'availability_exhausted_complete', 'second unavailable reaches conclusive exhaustion');
|
|
57
|
+
assert.throws(() => recordOfferCheckIssued(exhausted, ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'budget-check-3'), /availability_terminal/, 'third CheckAvail cannot be issued after terminal exhaustion');
|
|
58
|
+
const unavailableWithGap = (actionId, revision) => ({
|
|
59
|
+
...receipt(actionId, revision, 'budget-hotel:ratepkg-1'),
|
|
60
|
+
observation: { kind: 'offer.availability', offerRef: 'budget-hotel:ratepkg-1', checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-1'), available: false, changedFactRefs: [], gapCodes: ['hotel_rates_failed'] },
|
|
61
|
+
resultContract: { outcome: 'empty', hardCriteriaMet: false, factRefs: [], gapCodes: ['hotel_rates_failed'], blockers: [], relaxationsApplied: [] },
|
|
62
|
+
});
|
|
63
|
+
let inconclusive = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'gap-check-1');
|
|
64
|
+
inconclusive = recordOfferCheckReceipt(inconclusive, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), unavailableWithGap('gap-check-1', 1), 'gap-check-1', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
|
|
65
|
+
assert.equal(inconclusive.hotels['budget-hotel']?.status, 'inconclusive', 'generic gap is not accepted as a typed negative');
|
|
66
|
+
assert.equal(inconclusive.hotels['budget-hotel']?.tombstonedOfferRefs.includes('budget-hotel:ratepkg-1'), false, 'generic gap cannot tombstone logical offer');
|
|
67
|
+
assert.equal(inconclusive.hotels['budget-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('budget-hotel:ratepkg-1')), false, 'generic gap cannot tombstone offer version');
|
|
68
|
+
inconclusive = recordOffersQueryIssued(inconclusive, ['budget-hotel'], ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'gap-query-1');
|
|
69
|
+
const gapRefreshedReceipt = offersReceipt('gap-query-1', 2, ['budget-hotel'], ['budget-hotel:ratepkg-3'], 1);
|
|
70
|
+
inconclusive = recordOffersGeneration(inconclusive, ['budget-hotel'], ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-3', 'budget-hotel']]), 'gap-query-1', digest(gapRefreshedReceipt), gapRefreshedReceipt);
|
|
71
|
+
inconclusive = recordOfferCheckIssued(inconclusive, ws(2, ['budget-hotel'], [['budget-hotel:ratepkg-3', 'budget-hotel']]), 'budget-hotel:ratepkg-3', versionFor('budget-hotel:ratepkg-3'), 'gap-check-2');
|
|
72
|
+
inconclusive = recordOfferCheckReceipt(inconclusive, ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-3', 'budget-hotel']]), { ...unavailableWithGap('gap-check-2', 3), observation: { kind: 'offer.availability', offerRef: 'budget-hotel:ratepkg-3', checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-3'), available: false, changedFactRefs: [], gapCodes: [] } }, 'gap-check-2', 'budget-hotel:ratepkg-3', versionFor('budget-hotel:ratepkg-3'), 2);
|
|
73
|
+
assert.equal(inconclusive.terminal?.code, 'availability_exhausted_inconclusive', 'an unavailable receipt with a gap never masquerades as conclusive exhaustion');
|
|
74
|
+
for (const status of ['unavailable', 'no_match']) {
|
|
75
|
+
const gapAction = `generic-gap-${status}`;
|
|
76
|
+
const gapState = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), gapAction);
|
|
77
|
+
const gap = { ...receipt(gapAction, 1, 'budget-hotel:ratepkg-1', status), observation: { kind: 'offer.availability', offerRef: 'budget-hotel:ratepkg-1', checkedOfferVersionRef: versionFor('budget-hotel:ratepkg-1'), available: false, changedFactRefs: [], gapCodes: ['hotel_rates_failed'] }, resultContract: { outcome: 'empty', hardCriteriaMet: false, factRefs: [], gapCodes: ['hotel_rates_failed'], blockers: [], relaxationsApplied: [] } };
|
|
78
|
+
const afterGap = recordOfferCheckReceipt(gapState, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), gap, gapAction, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
|
|
79
|
+
assert.equal(afterGap.hotels['budget-hotel']?.status, 'inconclusive', `${status} with generic gap remains inconclusive`);
|
|
80
|
+
assert.equal(afterGap.hotels['budget-hotel']?.tombstonedOfferRefs.includes('budget-hotel:ratepkg-1'), false, `${status} with generic gap cannot tombstone logical offer`);
|
|
81
|
+
assert.equal(afterGap.hotels['budget-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('budget-hotel:ratepkg-1')), false, `${status} with generic gap cannot tombstone version`);
|
|
82
|
+
}
|
|
83
|
+
let queryBudget = recordOfferCheckIssued(createAvailabilityPolicy(unavailableWorkspace), unavailableWorkspace, 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 'query-budget-check');
|
|
84
|
+
queryBudget = recordOfferCheckReceipt(queryBudget, ws(1, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), receipt('query-budget-check', 1, 'budget-hotel:ratepkg-1'), 'query-budget-check', 'budget-hotel:ratepkg-1', versionFor('budget-hotel:ratepkg-1'), 0);
|
|
85
|
+
for (const [actionId, revision] of [['query-budget-1', 2], ['query-budget-2', 3]]) {
|
|
86
|
+
const offerRef = `budget-hotel:query-ratepkg-${revision}`;
|
|
87
|
+
queryBudget = recordOffersQueryIssued(queryBudget, ['budget-hotel'], ws(revision - 1, ['budget-hotel'], [[offerRef, 'budget-hotel']]), actionId);
|
|
88
|
+
const queryReceipt = offersReceipt(actionId, revision, ['budget-hotel'], [offerRef], 1);
|
|
89
|
+
queryBudget = recordOffersGeneration(queryBudget, ['budget-hotel'], ws(revision, ['budget-hotel'], [[offerRef, 'budget-hotel']]), actionId, digest(queryReceipt), queryReceipt);
|
|
90
|
+
}
|
|
91
|
+
assert.equal(queryBudget.hotels['budget-hotel']?.offerQueriesIssued, 2, 'two refresh queries are retained as lifetime usage');
|
|
92
|
+
assert.throws(() => recordOffersQueryIssued(queryBudget, ['budget-hotel'], ws(3, ['budget-hotel'], [['budget-hotel:ratepkg-1', 'budget-hotel']]), 'query-budget-3'), /availability_offer_query_limit_reached/, 'third offers.query is rejected');
|
|
93
|
+
const hotels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
94
|
+
const candidateWorkspace = ws(0, hotels, hotels.map((hotelRef, index) => [`candidate-${index + 1}`, hotelRef]), ['candidate-6', 'candidate-1', 'candidate-2', 'candidate-3', 'candidate-4', 'candidate-5']);
|
|
95
|
+
const candidates = recordOfferCheckIssued(createAvailabilityPolicy(candidateWorkspace), candidateWorkspace, 'candidate-6', versionFor('candidate-6'), 'candidate-check');
|
|
96
|
+
assert.deepEqual(candidates.hotelRefs, ['h6', 'h1', 'h2', 'h3', 'h4'], 'selected hotel is first and recovery freezes at five candidates');
|
|
97
|
+
assert.equal(candidates.hotelRefs.includes('h5'), false, 'sixth shortlisted hotel never enters the recovery epoch');
|
|
98
|
+
const duplicateWorkspace = ws(0, ['dup-a', 'dup-b'], [['dup-offer', 'dup-a'], ['dup-offer', 'dup-b']]);
|
|
99
|
+
assert.throws(() => createAvailabilityPolicy(duplicateWorkspace), /availability_duplicate_offer_ref/, 'loaded OfferRef must be globally unique before any .find-based authority lookup');
|
|
100
|
+
assert.throws(() => canIssueOfferCheck(createAvailabilityPolicy(ws(0, ['dup-a'], [])), duplicateWorkspace, 'dup-offer', versionFor('dup-offer')), /availability_duplicate_offer_ref/);
|
|
101
|
+
}
|
|
102
|
+
// Locked cap and source cases: a selected RoomList offer wins ordering, while
|
|
103
|
+
// a complete recovery generation never admits more than three offers.
|
|
104
|
+
{
|
|
105
|
+
const roomList = ws(0, ['room-hotel'], [['room-1', 'room-hotel'], ['room-2', 'room-hotel'], ['room-3', 'room-hotel'], ['room-4', 'room-hotel']], ['room-4']);
|
|
106
|
+
const selected = recordOfferCheckIssued(createAvailabilityPolicy(roomList), roomList, 'room-4', versionFor('room-4'), 'room-check');
|
|
107
|
+
assert.deepEqual(selected.hotels['room-hotel']?.currentOfferRefs, ['room-4', 'room-1', 'room-2']);
|
|
108
|
+
assert.equal(selected.hotels['room-hotel']?.currentGeneration?.source.kind, 'workspace_snapshot');
|
|
109
|
+
const complete = ws(0, ['complete-hotel'], [['complete-1', 'complete-hotel']]);
|
|
110
|
+
const completeStarted = recordOfferCheckIssued(createAvailabilityPolicy(complete), complete, 'complete-1', versionFor('complete-1'), 'complete-check');
|
|
111
|
+
const completeQuery = recordOffersQueryIssued(completeStarted, ['complete-hotel'], complete, 'complete-query', digest(criteria), criteria);
|
|
112
|
+
const fourOfferWorkspace = ws(1, ['complete-hotel'], [['complete-1', 'complete-hotel'], ['complete-2', 'complete-hotel'], ['complete-3', 'complete-hotel'], ['complete-4', 'complete-hotel']]);
|
|
113
|
+
assert.throws(() => recordOffersGeneration(completeQuery, ['complete-hotel'], fourOfferWorkspace, 'complete-query', digest(offersReceipt('complete-query', 1, ['complete-hotel'], ['complete-1', 'complete-2', 'complete-3', 'complete-4'], 1)), offersReceipt('complete-query', 1, ['complete-hotel'], ['complete-1', 'complete-2', 'complete-3', 'complete-4'], 1), digest(criteria), criteria), /availability_offer_limit_exceeded/);
|
|
114
|
+
}
|
|
115
|
+
// Locked partial/empty reconciliation: a partial receipt may only project an
|
|
116
|
+
// evidenced subset, and a clean empty receipt must agree with workspace truth.
|
|
117
|
+
{
|
|
118
|
+
const partialWorkspace = ws(0, ['partial-hotel'], [['partial-1', 'partial-hotel'], ['partial-2', 'partial-hotel'], ['partial-3', 'partial-hotel']]);
|
|
119
|
+
const partialStarted = recordOfferCheckIssued(createAvailabilityPolicy(partialWorkspace), partialWorkspace, 'partial-1', versionFor('partial-1'), 'partial-check');
|
|
120
|
+
const partialQuery = recordOffersQueryIssued(partialStarted, ['partial-hotel'], partialWorkspace, 'partial-query', digest(criteria), criteria);
|
|
121
|
+
const partialReceipt = offersReceipt('partial-query', 1, ['partial-hotel'], ['partial-2'], 1, 'partial', 'partial', false);
|
|
122
|
+
const partial = recordOffersGeneration(partialQuery, ['partial-hotel'], ws(1, ['partial-hotel'], [['partial-1', 'partial-hotel'], ['partial-2', 'partial-hotel'], ['partial-3', 'partial-hotel']]), 'partial-query', digest(partialReceipt), partialReceipt, digest(criteria), criteria);
|
|
123
|
+
assert.equal(partial.hotels['partial-hotel']?.currentGeneration?.evidence, 'partial');
|
|
124
|
+
assert.equal(partial.hotels['partial-hotel']?.currentOfferRefs.length, 0);
|
|
125
|
+
assert.equal(partial.hotels['partial-hotel']?.freshOffersRequired, true);
|
|
126
|
+
assert.throws(() => recordOffersGeneration(partialQuery, ['partial-hotel'], ws(1, ['partial-hotel'], [['partial-1', 'partial-hotel'], ['partial-2', 'partial-hotel']]), 'partial-query', digest(offersReceipt('partial-query', 1, ['partial-hotel'], ['partial-1'], 2, 'partial', 'partial', false)), offersReceipt('partial-query', 1, ['partial-hotel'], ['partial-1'], 2, 'partial', 'partial', false), digest(criteria), criteria), /availability_offers_count_mismatch/);
|
|
127
|
+
const emptyWorkspace = ws(0, ['empty-hotel'], [['empty-1', 'empty-hotel']]);
|
|
128
|
+
const emptyStarted = recordOfferCheckIssued(createAvailabilityPolicy(emptyWorkspace), emptyWorkspace, 'empty-1', versionFor('empty-1'), 'empty-check');
|
|
129
|
+
const emptyQuery = recordOffersQueryIssued(emptyStarted, ['empty-hotel'], emptyWorkspace, 'empty-query');
|
|
130
|
+
const empty = offersReceipt('empty-query', 1, ['empty-hotel'], [], 0, 'no_match', 'empty', false);
|
|
131
|
+
assert.throws(() => recordOffersGeneration(emptyQuery, ['empty-hotel'], emptyWorkspace, 'empty-query', digest(empty), empty), /availability_offers_workspace_mismatch/);
|
|
132
|
+
}
|
|
133
|
+
// Locked provenance and verification cases: failed ordinary queries erase the
|
|
134
|
+
// seed, while a CheckAvail claim is confirmed only by the current workspace's
|
|
135
|
+
// verifiedOffer capability; changed is inconclusive and requires a fresh generation.
|
|
136
|
+
{
|
|
137
|
+
const seedWorkspace = ws(0, ['seed-hotel'], [['seed-1', 'seed-hotel']]);
|
|
138
|
+
const seed = offersReceipt('seed-query', 1, ['seed-hotel'], ['seed-1'], 1);
|
|
139
|
+
const seeded = recordObservedOffersQuery(createAvailabilityPolicy(seedWorkspace), ['seed-hotel'], digest(criteria), seedWorkspace, seed, 'seed-query', digest(seed), criteria);
|
|
140
|
+
assert.equal(seeded.criteriaDigest, digest(criteria));
|
|
141
|
+
const cleared = recordObservedOffersQuery(seeded, ['seed-hotel'], digest(criteria), ws(2, ['seed-hotel'], []), gapReceipt('failed-query', 2), 'failed-query', digest(gapReceipt('failed-query', 2)), criteria);
|
|
142
|
+
assert.equal(cleared.criteria, undefined);
|
|
143
|
+
assert.equal(cleared.criteriaDigest, undefined);
|
|
144
|
+
assert.equal(cleared.lastQuerySourceActionId, undefined);
|
|
145
|
+
const verificationWorkspace = ws(0, ['verify-hotel'], [['verify-1', 'verify-hotel']]);
|
|
146
|
+
const verificationStarted = recordOfferCheckIssued(createAvailabilityPolicy(verificationWorkspace), verificationWorkspace, 'verify-1', versionFor('verify-1'), 'verify-check');
|
|
147
|
+
const wrongVerified = { ...verificationWorkspace, revision: 1, verifiedOffer: verifiedCapability('other-offer', versionFor('other-offer')) };
|
|
148
|
+
const wrong = recordOfferCheckReceipt(verificationStarted, wrongVerified, receipt('verify-check', 1, 'verify-1', 'applied', true), 'verify-check', 'verify-1', versionFor('verify-1'), 0);
|
|
149
|
+
assert.notEqual(wrong.terminal?.code, 'availability_confirmed');
|
|
150
|
+
assert.equal(wrong.hotels['verify-hotel']?.status, 'inconclusive');
|
|
151
|
+
const matchingStarted = recordOfferCheckIssued(createAvailabilityPolicy(verificationWorkspace), verificationWorkspace, 'verify-1', versionFor('verify-1'), 'verify-match');
|
|
152
|
+
const matchingWorkspace = { ...verificationWorkspace, revision: 1, verifiedOffer: verifiedCapability('verify-1') };
|
|
153
|
+
const matching = recordOfferCheckReceipt(matchingStarted, matchingWorkspace, receipt('verify-match', 1, 'verify-1', 'applied', true), 'verify-match', 'verify-1', versionFor('verify-1'), 0);
|
|
154
|
+
assert.equal(matching.terminal?.code, 'availability_confirmed');
|
|
155
|
+
const verificationSeedReceipt = offersReceipt('verify-seed', 0, ['verify-hotel'], ['verify-1'], 1);
|
|
156
|
+
const verificationSeed = recordObservedOffersQuery(createAvailabilityPolicy(verificationWorkspace), ['verify-hotel'], digest(criteria), verificationWorkspace, verificationSeedReceipt, 'verify-seed', digest(verificationSeedReceipt), criteria);
|
|
157
|
+
const changedStarted = recordOfferCheckIssued(verificationSeed, verificationWorkspace, 'verify-1', versionFor('verify-1'), 'verify-changed');
|
|
158
|
+
const changedReceipt = { ...receipt('verify-changed', 1, 'verify-1', 'changed', true, versionFor('verify-1'), versionFor('verify-1', 'v2')), observation: { kind: 'offer.availability', offerRef: 'verify-1', checkedOfferVersionRef: versionFor('verify-1'), currentOfferVersionRef: versionFor('verify-1', 'v2'), available: true, changedFactRefs: ['price'], gapCodes: [] }, resultContract: { outcome: 'partial', hardCriteriaMet: false, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } };
|
|
159
|
+
const changed = recordOfferCheckReceipt(changedStarted, { ...verificationWorkspace, revision: 1, loadedOffers: [{ offerRef: 'verify-1', offerVersionRef: versionFor('verify-1', 'v2'), hotelRef: 'verify-hotel', evidenceLevel: 'rate_loaded', factRefs: [] }] }, changedReceipt, 'verify-changed', 'verify-1', versionFor('verify-1'), 0);
|
|
160
|
+
assert.equal(changed.hotels['verify-hotel']?.status, 'active');
|
|
161
|
+
assert.deepEqual(changed.hotels['verify-hotel']?.currentOfferRefs, ['verify-1']);
|
|
162
|
+
assert.equal(changed.hotels['verify-hotel']?.tombstonedOfferVersionRefs.includes(versionFor('verify-1')), true);
|
|
163
|
+
assert.equal(changed.hotels['verify-hotel']?.freshOffersRequired, false);
|
|
164
|
+
}
|
|
165
|
+
const initial = ws(0, ['h1', 'h2'], [['o1', 'h1'], ['o2', 'h1'], ['b1', 'h2']], ['b1']);
|
|
166
|
+
assert.throws(() => recordOffersQueryIssued(createAvailabilityPolicy(initial), ['h1'], initial, 'pre-q'), /recovery_not_started/);
|
|
167
|
+
let state = recordOfferCheckIssued(createAvailabilityPolicy(initial), initial, 'o1', versionFor('o1'), 'check-1');
|
|
168
|
+
assert.deepEqual(state.hotelRefs, ['h1', 'h2']);
|
|
169
|
+
assert.equal(state.hotels.h1?.offerQueriesIssued, 0);
|
|
170
|
+
state = recordOfferCheckReceipt(state, ws(1, ['h1', 'h2'], [['o1', 'h1'], ['o2', 'h1'], ['b1', 'h2']]), receipt('check-1', 1, 'o1'), 'check-1', 'o1', versionFor('o1'), 0);
|
|
171
|
+
assert.equal(state.hotels.h1?.freshOffersRequired, true);
|
|
172
|
+
assert.equal(canIssueOfferCheck(state, ws(1, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'b1', versionFor('b1')).ok, false);
|
|
173
|
+
let q = recordOffersQueryIssued(state, ['h1'], ws(1, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q1', 'criteria-a');
|
|
174
|
+
assert.throws(() => recordOffersQueryIssued(q, ['h1'], ws(1, ['h1', 'h2'], []), 'q2', 'criteria-b'), /criteria_changed/);
|
|
175
|
+
const staleQueryReceipt = { ...receipt('q1', 2, 'o2', 'stale'), observation: { kind: 'gap', code: 'stale_revision', factRefs: [] } };
|
|
176
|
+
q = foldQuery(q, ['h1'], ws(2, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q1', staleQueryReceipt, 'criteria-a');
|
|
177
|
+
assert.equal(q.hotels.h1?.generationNo, 1);
|
|
178
|
+
assert.equal(q.hotels.h1?.currentGeneration?.generationId, 'h1:generation:1');
|
|
179
|
+
const completeQueryReceipt = { ...receipt('q2', 3, 'o2', 'applied'), observation: { kind: 'offers.state', hotelRefs: ['h1'], offerRefs: ['o2'], loadedHotelCount: 1 } };
|
|
180
|
+
q = recordOffersQueryIssued(q, ['h1'], ws(2, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q2', 'criteria-a');
|
|
181
|
+
q = foldQuery(q, ['h1'], ws(3, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'q2', completeQueryReceipt, 'criteria-a');
|
|
182
|
+
assert.deepEqual(q.hotels.h1?.currentOfferRefs, ['o2']);
|
|
183
|
+
q = recordOfferCheckIssued(q, ws(3, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), 'o2', versionFor('o2'), 'check-2');
|
|
184
|
+
q = recordOfferCheckReceipt(q, ws(4, ['h1', 'h2'], [['o2', 'h1'], ['b1', 'h2']]), receipt('check-2', 4, 'o2'), 'check-2', 'o2', versionFor('o2'), 3);
|
|
185
|
+
assert.deepEqual(q.hotels.h1?.invalidatedOfferRefs, ['o2']);
|
|
186
|
+
assert.equal(q.hotels.h1?.freshOffersRequired, false);
|
|
187
|
+
assert.equal(q.terminal, undefined);
|
|
188
|
+
assert.throws(() => recordOffersQueryIssued(q, ['h1'], ws(4, ['h1', 'h2'], [['b1', 'h2']]), 'q-after-two-checks', 'criteria-a'), /hotel_not_active/);
|
|
189
|
+
const fw = ws(0, ['a', 'b'], [['a:ratepkg-1', 'a'], ['b:ratepkg-1', 'b']], ['b:ratepkg-1']);
|
|
190
|
+
let fallback = recordOfferCheckIssued(createAvailabilityPolicy(fw), fw, 'a:ratepkg-1', versionFor('a:ratepkg-1'), 'fa');
|
|
191
|
+
fallback = recordOfferCheckReceipt(fallback, ws(1, ['a', 'b'], [['a:ratepkg-1', 'a'], ['b:ratepkg-1', 'b']], ['b:ratepkg-1']), receipt('fa', 1, 'a:ratepkg-1'), 'fa', 'a:ratepkg-1', versionFor('a:ratepkg-1'), 0);
|
|
192
|
+
assert.equal(fallback.hotels.a?.freshOffersRequired, true);
|
|
193
|
+
fallback = recordOffersQueryIssued(fallback, ['a'], ws(1, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq1');
|
|
194
|
+
const fallbackEmpty = { ...receipt('fq1', 2, 'a:ratepkg-1', 'no_match'), observation: { kind: 'offers.state', hotelRefs: ['a'], offerRefs: [], loadedHotelCount: 0 } };
|
|
195
|
+
fallback = foldQuery(fallback, ['a'], ws(2, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq1', fallbackEmpty);
|
|
196
|
+
assert.equal(fallback.activeHotelOrdinal, 1);
|
|
197
|
+
assert.equal(canIssueOfferCheck(fallback, ws(2, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'b:ratepkg-1', versionFor('b:ratepkg-1')).ok, false);
|
|
198
|
+
fallback = recordOffersQueryIssued(fallback, ['b'], ws(2, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq2');
|
|
199
|
+
const fallbackOffers = { ...receipt('fq2', 3, 'b:ratepkg-1', 'applied'), observation: { kind: 'offers.state', hotelRefs: ['b'], offerRefs: ['b:ratepkg-1'], loadedHotelCount: 1 } };
|
|
200
|
+
fallback = foldQuery(fallback, ['b'], ws(3, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'fq2', fallbackOffers);
|
|
201
|
+
assert.equal(canIssueOfferCheck(fallback, ws(3, ['a', 'b'], [['b:ratepkg-1', 'b']]), 'b:ratepkg-1', versionFor('b:ratepkg-1')).ok, true);
|
|
202
|
+
let exhausted = recordOfferCheckIssued(createAvailabilityPolicy(ws(0, ['x'], [['x1', 'x']])), ws(0, ['x'], [['x1', 'x']]), 'x1', versionFor('x1'), 'ex1');
|
|
203
|
+
exhausted = recordOfferCheckReceipt(exhausted, ws(1, ['x'], [['x1', 'x']]), receipt('ex1', 1, 'x1'), 'ex1', 'x1', versionFor('x1'), 0);
|
|
204
|
+
assert.equal(exhausted.terminal, undefined);
|
|
205
|
+
assert.equal(exhausted.hotels.x?.freshOffersRequired, true);
|
|
206
|
+
exhausted = recordOffersQueryIssued(exhausted, ['x'], ws(1, ['x'], [['x1', 'x']]), 'exq');
|
|
207
|
+
const exhaustedReceipt = { ...receipt('exq', 2, 'x1', 'no_match'), observation: { kind: 'offers.state', hotelRefs: ['x'], offerRefs: [], loadedHotelCount: 0 } };
|
|
208
|
+
exhausted = foldQuery(exhausted, ['x'], ws(2, ['x'], []), 'exq', exhaustedReceipt);
|
|
209
|
+
assert.equal(exhausted.hotels.x?.generationNo, 2);
|
|
210
|
+
assert.equal(exhausted.terminal?.code, 'availability_exhausted_complete');
|
|
211
|
+
assert.throws(() => recordOfferCheckIssued(exhausted, ws(2, ['x'], []), 'x1', versionFor('x1'), 'after-terminal'), /availability_terminal/);
|
|
212
|
+
// A failed A@v1 must not poison a same-hotel candidate query containing B@v1.
|
|
213
|
+
// The reducer filters A's tombstone inside the generation, admits only B, and
|
|
214
|
+
// issues the next CheckAvail without getting stuck on the old offer.
|
|
215
|
+
{
|
|
216
|
+
const candidateWorkspace = ws(0, ['candidate-hotel'], [['candidate-a', 'candidate-hotel'], ['candidate-b', 'candidate-hotel']], ['candidate-a', 'candidate-b']);
|
|
217
|
+
let candidateState = recordOfferCheckIssued(createAvailabilityPolicy(candidateWorkspace), candidateWorkspace, 'candidate-a', versionFor('candidate-a'), 'candidate-a-check');
|
|
218
|
+
candidateState = recordOfferCheckReceipt(candidateState, ws(1, ['candidate-hotel'], [['candidate-a', 'candidate-hotel'], ['candidate-b', 'candidate-hotel']], ['candidate-a', 'candidate-b']), receipt('candidate-a-check', 1, 'candidate-a'), 'candidate-a-check', 'candidate-a', versionFor('candidate-a'), 0);
|
|
219
|
+
const queryWorkspace = ws(1, ['candidate-hotel'], [['candidate-a', 'candidate-hotel'], ['candidate-b', 'candidate-hotel']], ['candidate-a', 'candidate-b']);
|
|
220
|
+
const candidateQueryReceipt = offersReceipt('candidate-query', 2, ['candidate-hotel'], ['candidate-a', 'candidate-b'], 1);
|
|
221
|
+
candidateState = recordOffersQueryIssued(candidateState, ['candidate-hotel'], queryWorkspace, 'candidate-query');
|
|
222
|
+
candidateState = recordOffersGeneration(candidateState, ['candidate-hotel'], queryWorkspace, 'candidate-query', digest(candidateQueryReceipt), candidateQueryReceipt);
|
|
223
|
+
assert.equal(candidateState.hotels['candidate-hotel']?.currentOfferRefs.includes('candidate-a'), false, 'unavailable A@v1 is not re-admitted by a same-hotel candidate query');
|
|
224
|
+
assert.deepEqual(candidateState.hotels['candidate-hotel']?.currentOfferRefs, ['candidate-b'], 'same-hotel candidate query admits B@v1 only');
|
|
225
|
+
assert.equal(candidateState.activeHotelOrdinal, 0, 'same-hotel recovery remains on the active hotel after filtering A');
|
|
226
|
+
const bWorkspace = ws(2, ['candidate-hotel'], [['candidate-b', 'candidate-hotel']], ['candidate-b']);
|
|
227
|
+
assert.equal(canIssueOfferCheck(candidateState, bWorkspace, 'candidate-b', versionFor('candidate-b')).ok, true, 'B@v1 is recheckable after A@v1 unavailable');
|
|
228
|
+
candidateState = recordOfferCheckIssued(candidateState, bWorkspace, 'candidate-b', versionFor('candidate-b'), 'candidate-b-check');
|
|
229
|
+
const bVerified = verifiedCapability('candidate-b', versionFor('candidate-b'));
|
|
230
|
+
candidateState = recordOfferCheckReceipt(candidateState, ws(3, ['candidate-hotel'], [['candidate-b', 'candidate-hotel']], ['candidate-b'], bVerified), receipt('candidate-b-check', 3, 'candidate-b', 'applied', true), 'candidate-b-check', 'candidate-b', versionFor('candidate-b'), 2);
|
|
231
|
+
assert.equal(candidateState.terminal?.code, 'availability_confirmed', 'recheck of B@v1 can confirm after A@v1 unavailable');
|
|
232
|
+
}
|
|
233
|
+
console.log('BOOKING COPILOT AVAILABILITY POLICY: admission, criteria CAS, stale no-generation, invalidation, cursor fallback, exhaustion, absorbing terminal OK');
|
|
234
|
+
// Runtime proof: action replay, restart restoration, terminal SSE batch and absorbing seams.
|
|
235
|
+
const ledgerRoot = mkdtempSync(join(tmpdir(), 'gotry-availability-proof-'));
|
|
236
|
+
const ledger = ensureLedger(ledgerRoot);
|
|
237
|
+
const runtime = new BookingCopilotTaskRuntime(ledger, { contextRefFactory: () => 'ctx-availability' });
|
|
238
|
+
const runtimeWorkspace = ws(0, ['rh'], []);
|
|
239
|
+
runtime.startTask({ schemaVersion: 'booking.surface', kind: 'user.turn', taskId: 'runtime-availability', turnId: 'turn-1', workspace: runtimeWorkspace, request: { text: 'find rates' } });
|
|
240
|
+
const query = { schemaVersion: 'booking.surface', kind: 'offers.query', actionId: 'rq-1', contextRef: 'ctx-availability', expectedRevision: 0, reason: 'ordinary query', factRefs: [], input: { hotelRefs: ['rh'], criteria: {} } };
|
|
241
|
+
runtime.issueOperation('runtime-availability', query, offerVerifiedIntent);
|
|
242
|
+
runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: 'runtime-availability', workspace: ws(1, ['rh'], [['ro1', 'rh']]), receipt: { schemaVersion: 'booking.surface', kind: 'action.receipt', actionId: 'rq-1', contextRef: 'ctx-availability', status: 'applied', revision: 1, observation: { kind: 'offers.state', hotelRefs: ['rh'], offerRefs: ['ro1'], loadedHotelCount: 1 }, resultContract: { outcome: 'complete', hardCriteriaMet: true, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } } });
|
|
243
|
+
const check = { schemaVersion: 'booking.surface', kind: 'offer.check', actionId: 'rc-1', contextRef: 'ctx-availability', expectedRevision: 1, reason: 'CheckAvail', factRefs: [], input: { offerRef: 'ro1', offerVersionRef: versionFor('ro1') } };
|
|
244
|
+
runtime.issueOperation('runtime-availability', check, offerVerifiedIntent);
|
|
245
|
+
runtime.issueOperation('runtime-availability', check, offerVerifiedIntent);
|
|
246
|
+
assert.equal(runtime.resumeTask('runtime-availability')?.availability.attempts.length, 1);
|
|
247
|
+
const restarted = new BookingCopilotTaskRuntime(ensureLedger(ledgerRoot), { contextRefFactory: () => 'ctx-availability' });
|
|
248
|
+
assert.equal(restarted.resumeTask('runtime-availability')?.availability.attempts[0]?.actionId, 'rc-1');
|
|
249
|
+
runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: 'runtime-availability', workspace: ws(2, ['rh'], [['ro1', 'rh']]), receipt: { ...receipt('rc-1', 2, 'ro1'), status: 'unavailable' } });
|
|
250
|
+
const query2 = { ...query, actionId: 'rq-2', expectedRevision: 2 };
|
|
251
|
+
runtime.issueOperation('runtime-availability', query2, offerVerifiedIntent);
|
|
252
|
+
runtime.continueWithReceipt({ schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: 'runtime-availability', workspace: ws(3, ['rh'], []), receipt: { schemaVersion: 'booking.surface', kind: 'action.receipt', actionId: 'rq-2', contextRef: 'ctx-availability', status: 'no_match', revision: 3, observation: { kind: 'offers.state', hotelRefs: ['rh'], offerRefs: [], loadedHotelCount: 0 }, resultContract: { outcome: 'empty', hardCriteriaMet: false, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] } } });
|
|
253
|
+
assert.equal(runtime.resumeTask('runtime-availability')?.phase, 'terminal');
|
|
254
|
+
const eventCount = ledger.countEvents();
|
|
255
|
+
const terminalBatchPayload = ledger.db.prepare("SELECT payload FROM events WHERE kind = 'booking.copilot.decision.batch' ORDER BY seq DESC LIMIT 1").get();
|
|
256
|
+
const terminalBatch = JSON.parse(terminalBatchPayload.payload);
|
|
257
|
+
assert.deepEqual(runtime.terminalDecisionBatch('runtime-availability', terminalBatch.requestKey), terminalBatch.events);
|
|
258
|
+
assert.deepEqual(runtime.terminalDecisionBatch('runtime-availability', terminalBatch.requestKey), terminalBatch.events, 'exact terminal batch replay is read-only');
|
|
259
|
+
assert.throws(() => runtime.terminalDecisionBatch('runtime-availability', 'arbitrary-new-terminal-key'), /task_terminal/);
|
|
260
|
+
assert.throws(() => runtime.issueOperation('runtime-availability', { ...query, actionId: 'late', expectedRevision: 3 }, offerVerifiedIntent), /task_terminal/);
|
|
261
|
+
assert.throws(() => runtime.emitEvent('runtime-availability', { kind: 'status', status: 'working' }), /task_terminal/);
|
|
262
|
+
assert.equal(ledger.countEvents(), eventCount);
|
|
263
|
+
ledger.close();
|
|
264
|
+
restarted['ledger'].close();
|
|
265
|
+
rmSync(ledgerRoot, { recursive: true, force: true });
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Production executable proof for the standalone Booking Copilot server. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
const root = resolve(import.meta.dirname, '../..');
|
|
8
|
+
const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
9
|
+
assert.equal(packageJson.bin?.['gotry-booking-copilot'], 'bin/gotry-booking-copilot.js');
|
|
10
|
+
const build = spawnSync(process.execPath, ['scripts/build-dist.mjs'], {
|
|
11
|
+
cwd: root,
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
});
|
|
14
|
+
assert.equal(build.status, 0, build.stderr || build.stdout);
|
|
15
|
+
const bin = join(root, 'bin/gotry-booking-copilot.js');
|
|
16
|
+
const missingKey = spawnSync(process.execPath, [bin], {
|
|
17
|
+
cwd: root,
|
|
18
|
+
env: { PATH: process.env.PATH },
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
});
|
|
21
|
+
assert.equal(missingKey.status, 1);
|
|
22
|
+
assert.match(missingKey.stderr, /booking_copilot_api_key_required/);
|
|
23
|
+
const stateRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-bin-'));
|
|
24
|
+
try {
|
|
25
|
+
const child = spawn(process.execPath, [bin], {
|
|
26
|
+
cwd: root,
|
|
27
|
+
env: {
|
|
28
|
+
PATH: process.env.PATH,
|
|
29
|
+
GOTRY_BOOKING_COPILOT_API_KEY: 'fixture-bff-key',
|
|
30
|
+
GOTRY_BOOKING_COPILOT_STATE_ROOT: stateRoot,
|
|
31
|
+
GOTRY_BOOKING_COPILOT_HOST: '127.0.0.1',
|
|
32
|
+
GOTRY_BOOKING_COPILOT_PORT: '0',
|
|
33
|
+
DEEPSEEK_API_KEY: 'fixture-model-key',
|
|
34
|
+
},
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
let stderr = '';
|
|
38
|
+
child.stderr.setEncoding('utf8');
|
|
39
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
40
|
+
await new Promise((resolveReady, reject) => {
|
|
41
|
+
const timeout = setTimeout(() => reject(new Error(`booking copilot bin did not listen: ${stderr}`)), 10_000);
|
|
42
|
+
const inspect = () => {
|
|
43
|
+
if (!stderr.includes('/a2a/booking-copilot/turn'))
|
|
44
|
+
return;
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
resolveReady();
|
|
47
|
+
};
|
|
48
|
+
child.stderr.on('data', inspect);
|
|
49
|
+
child.once('exit', (code) => {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
reject(new Error(`booking copilot bin exited before listen: ${code}: ${stderr}`));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
assert.match(stderr, /listening on http:\/\/127\.0\.0\.1:\d+\/a2a\/booking-copilot\/turn/);
|
|
55
|
+
assert.ok(!stderr.includes('fixture-bff-key'));
|
|
56
|
+
assert.ok(!stderr.includes('fixture-model-key'));
|
|
57
|
+
child.kill('SIGTERM');
|
|
58
|
+
const exit = await new Promise((resolveExit) => {
|
|
59
|
+
child.once('exit', (code, signal) => resolveExit({ code, signal }));
|
|
60
|
+
});
|
|
61
|
+
assert.deepEqual(exit, { code: 0, signal: null }, `graceful shutdown failed: ${stderr}`);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
rmSync(stateRoot, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
console.log('BOOKING COPILOT BIN PROOF: startup failure/listen/SIGTERM lifecycle OK');
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
/**
|
|
3
|
+
* Keep a body failure while attempting every independent cleanup operation.
|
|
4
|
+
* Each attempt is awaited in dependency order, while its failure is retained
|
|
5
|
+
* so a later cleanup attempt still runs.
|
|
6
|
+
*/
|
|
7
|
+
export async function throwWithCleanupFailures(bodyFailed, bodyError, cleanupAttempts, settlement) {
|
|
8
|
+
const cleanupErrors = [];
|
|
9
|
+
for (const attempt of cleanupAttempts) {
|
|
10
|
+
try {
|
|
11
|
+
await attempt();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
cleanupErrors.push(error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await (cleanupErrors.length === 0 ? settlement?.onSuccess : settlement?.onFailure)?.();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
cleanupErrors.push(error);
|
|
22
|
+
}
|
|
23
|
+
if (!bodyFailed && cleanupErrors.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
if (bodyFailed && cleanupErrors.length === 0)
|
|
26
|
+
throw bodyError;
|
|
27
|
+
throw new AggregateError(bodyFailed ? [bodyError, ...cleanupErrors] : cleanupErrors, 'booking_copilot_proof_cleanup_failed');
|
|
28
|
+
}
|
|
29
|
+
if (process.argv[2] === '--negative-control') {
|
|
30
|
+
const cleanupOrder = [];
|
|
31
|
+
const bodyError = new Error('proof body failure');
|
|
32
|
+
const plannerCleanupError = new Error('proof planner cleanup failure');
|
|
33
|
+
const portCleanupError = new Error('proof port cleanup failure');
|
|
34
|
+
let legacyCaught;
|
|
35
|
+
try {
|
|
36
|
+
try {
|
|
37
|
+
throw bodyError;
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
cleanupOrder.push('legacy-planner');
|
|
41
|
+
throw plannerCleanupError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
legacyCaught = error;
|
|
46
|
+
}
|
|
47
|
+
assert.equal(legacyCaught, plannerCleanupError, 'legacy sequential finally masks the body error');
|
|
48
|
+
assert.deepEqual(cleanupOrder, ['legacy-planner'], 'legacy sequential finally skips cleanup after the first failure');
|
|
49
|
+
cleanupOrder.length = 0;
|
|
50
|
+
let caught;
|
|
51
|
+
try {
|
|
52
|
+
await throwWithCleanupFailures(true, bodyError, [
|
|
53
|
+
async () => { cleanupOrder.push('planner'); throw plannerCleanupError; },
|
|
54
|
+
async () => { cleanupOrder.push('port'); throw portCleanupError; },
|
|
55
|
+
async () => { cleanupOrder.push('server'); },
|
|
56
|
+
], {
|
|
57
|
+
onSuccess: () => { cleanupOrder.push('state-deleted'); },
|
|
58
|
+
onFailure: () => { cleanupOrder.push('state-retained'); },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
caught = error;
|
|
63
|
+
}
|
|
64
|
+
assert.ok(caught instanceof AggregateError, 'body and cleanup failures are aggregated');
|
|
65
|
+
assert.deepEqual(caught.errors, [bodyError, plannerCleanupError, portCleanupError], 'body error remains first and all cleanup errors remain');
|
|
66
|
+
assert.deepEqual(cleanupOrder, ['planner', 'port', 'server', 'state-retained'], 'later cleanup runs and the failed tree keeps its evidence');
|
|
67
|
+
let cleanStateRemoved = false;
|
|
68
|
+
await assert.rejects(throwWithCleanupFailures(true, bodyError, [async () => { }], {
|
|
69
|
+
onSuccess: () => { cleanStateRemoved = true; },
|
|
70
|
+
}), (error) => error === bodyError);
|
|
71
|
+
assert.equal(cleanStateRemoved, true, 'successful cleanup removes state even when the body failed');
|
|
72
|
+
console.log(JSON.stringify({
|
|
73
|
+
legacy: { caught: plannerCleanupError.message, cleanupOrder: ['legacy-planner'] },
|
|
74
|
+
repaired: { body: bodyError.message, cleanup: [plannerCleanupError.message, portCleanupError.message], cleanupOrder },
|
|
75
|
+
}));
|
|
76
|
+
}
|