@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,1221 +1,206 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { createHash } from 'node:crypto';
4
- import { BOOKING_READ_ACTION_KINDS, BOOKING_GAP_CODES, BOOKING_BLOCKER_CODES, BOOKING_SURFACE_SCHEMA_SHA256, bookingSurfaceAllowedActions } from '../src/booking-surface/contracts.js';
5
- import { validateBookingSurface, validateBookingReadAction, validateBookingSurfaceEvent, validateCriterionBlocker, validateRelaxationApproval, validateApprovalAgainstBlocker } from '../src/booking-surface/validation.js';
4
+ import { BOOKING_READ_ACTION_KINDS, BOOKING_GAP_CODES, BOOKING_SURFACE_SCHEMA_SHA256, bookingSurfaceAllowedActions } from '../src/booking-surface/contracts.js';
5
+ import { validateBookingSurface, validateBookingSurfaceEvent, validateCriterionBlocker, validateRelaxationApproval, validateApprovalAgainstBlocker } from '../src/booking-surface/validation.js';
6
6
  let positive = 0;
7
7
  let negative = 0;
8
- function accept(label, value) {
9
- assert.equal(validateBookingSurface(value).ok, true, label);
10
- positive++;
11
- }
12
- function reject(label, value) {
13
- assert.equal(validateBookingSurface(value).ok, false, label);
14
- negative++;
15
- }
16
- assert.deepEqual(bookingSurfaceAllowedActions('storefront'), [
17
- 'search.patch',
18
- 'search.run',
19
- 'results.view.patch',
20
- 'hotel.focus'
21
- ], 'storefront UAT availability observation is least privilege');
22
- assert.deepEqual(bookingSurfaceAllowedActions('payment_link'), [
23
- 'search.patch',
24
- 'search.run',
25
- 'results.view.patch',
26
- 'hotel.focus',
27
- 'hotel.select'
28
- ], 'payment_link UAT availability observation is least privilege');
8
+ function accept(label, value) { assert.equal(validateBookingSurface(value).ok, true, label); positive++; }
9
+ function reject(label, value) { assert.equal(validateBookingSurface(value).ok, false, label); negative++; }
10
+ assert.deepEqual(bookingSurfaceAllowedActions('storefront'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus'], 'storefront UAT availability observation is least privilege');
11
+ assert.deepEqual(bookingSurfaceAllowedActions('payment_link'), ['search.patch', 'search.run', 'results.view.patch', 'hotel.focus', 'hotel.select'], 'payment_link UAT availability observation is least privilege');
29
12
  assert.ok(!bookingSurfaceAllowedActions('storefront').includes('offers.query'), 'storefront availability cannot expand into offers');
30
13
  assert.ok(!bookingSurfaceAllowedActions('payment_link').includes('checkout.prepare'), 'payment_link availability cannot expand into checkout');
31
14
  const hash = 'a'.repeat(64);
32
- const workspace = {
33
- schemaVersion: 'booking.surface',
34
- contextRef: 'ctx-1',
35
- surface: 'tenant',
36
- revision: 0,
37
- locale: 'zh-CN',
38
- currency: 'AED',
39
- searchDraft: {},
40
- results: {
41
- status: 'idle'
42
- },
43
- visibleHotels: [],
44
- loadedOffers: [],
45
- shortlistedOfferRefs: [],
46
- capabilities: {
47
- surface: 'tenant',
48
- allowedActions: [
49
- ...BOOKING_READ_ACTION_KINDS
50
- ]
51
- }
52
- };
53
- const ingressWorkspace = {
54
- ...workspace
55
- };
15
+ const workspace = { schemaVersion: 'booking.surface', contextRef: 'ctx-1', surface: 'tenant', revision: 0, locale: 'zh-CN', currency: 'AED', searchDraft: {}, results: { status: 'idle' }, visibleHotels: [], loadedOffers: [], shortlistedOfferRefs: [], capabilities: { surface: 'tenant', allowedActions: [...BOOKING_READ_ACTION_KINDS] } };
16
+ const ingressWorkspace = { ...workspace };
56
17
  delete ingressWorkspace.contextRef;
57
18
  delete ingressWorkspace.surface;
58
19
  delete ingressWorkspace.capabilities;
59
20
  const inputs = {
60
- 'search.patch': {
61
- patch: {
62
- destination: {
63
- query: 'Dubai'
64
- }
65
- }
66
- },
67
- 'search.run': {},
68
- 'results.view.patch': {
69
- patch: {
70
- sort: 'recommended'
71
- }
72
- },
73
- 'hotel.focus': {
74
- hotelRef: 'hotel-1'
75
- },
76
- 'hotel.select': {
77
- hotelRef: 'hotel-1'
78
- },
79
- 'offers.query': {
80
- hotelRefs: [
81
- 'hotel-1'
82
- ],
83
- criteria: {}
84
- },
85
- 'offers.view.patch': {
86
- hotelRef: 'hotel-1',
87
- criteria: {}
88
- },
89
- 'offers.compare': {
90
- offerRefs: [
91
- 'offer-1'
92
- ],
93
- requestedCount: 1
94
- },
95
- 'offer.select': {
96
- offerRef: 'offer-1',
97
- offerVersionRef: 'offer-1:v1'
98
- },
99
- 'offer.check': {
100
- offerRef: 'offer-1',
101
- offerVersionRef: 'offer-1:v1'
102
- },
103
- 'checkout.prepare': {
104
- offerRef: 'offer-1',
105
- offerVersionRef: 'offer-1:v1',
106
- verifiedOfferRef: 'verified-1'
107
- },
108
- 'order.observe': {
109
- orderRef: 'order-1'
110
- }
111
- };
112
- const actions = Object.fromEntries(BOOKING_READ_ACTION_KINDS.map((kind)=>[
113
- kind,
114
- {
115
- schemaVersion: 'booking.surface',
116
- kind,
117
- actionId: `a-${kind.replaceAll('.', '-')}`,
118
- contextRef: 'ctx-1',
119
- expectedRevision: 0,
120
- reason: 'typed request',
121
- factRefs: [],
122
- input: inputs[kind]
123
- }
124
- ]));
125
- const approvalRef = {
126
- approvalId: 'ap-1',
127
- blockerId: 'b-1',
128
- contextRef: 'ctx-1',
129
- sourceTurnId: 'turn-1',
130
- presentationRequestKey: 'approval:task-1:turn-1:a-search-run:' + hash,
131
- sourceActionId: 'a-search-run',
132
- targetActionId: 'a-search-run',
133
- sourceRevision: 0,
134
- targetActionKind: 'search.run',
135
- to: 'prefer',
136
- expiresAt: '2099-01-01T00:00:00.000Z',
137
- nonce: 'nonce-1',
138
- sourceReceiptDigest: hash,
139
- scope: 'offer',
140
- code: 'hotel_rates_failed',
141
- criterionPath: 'offers.totalPriceMax',
142
- valueDigest: hash
143
- };
144
- accept('valid relaxation approval ref action', {
145
- ...actions['search.run'],
146
- relaxationApprovalRef: approvalRef
147
- });
148
- reject('approval context replay', {
149
- ...actions['search.run'],
150
- relaxationApprovalRef: {
151
- ...approvalRef,
152
- contextRef: 'ctx-other'
153
- }
154
- });
155
- reject('approval action replay', {
156
- ...actions['search.run'],
157
- relaxationApprovalRef: {
158
- ...approvalRef,
159
- targetActionId: 'a-other'
160
- }
161
- });
162
- reject('approval future revision replay', {
163
- ...actions['search.run'],
164
- relaxationApprovalRef: {
165
- ...approvalRef,
166
- sourceRevision: 1
167
- }
168
- });
169
- reject('approval old revision replay', {
170
- ...actions['search.run'],
171
- expectedRevision: 1,
172
- relaxationApprovalRef: {
173
- ...approvalRef,
174
- sourceRevision: 0
175
- }
176
- });
177
- reject('approval target kind replay', {
178
- ...actions['search.run'],
179
- relaxationApprovalRef: {
180
- ...approvalRef,
181
- targetActionKind: 'offer.check'
182
- }
183
- });
184
- reject('approval bad nonce', {
185
- ...actions['search.run'],
186
- relaxationApprovalRef: {
187
- ...approvalRef,
188
- nonce: 'Bearer token'
189
- }
190
- });
191
- reject('approval bad expiry', {
192
- ...actions['search.run'],
193
- relaxationApprovalRef: {
194
- ...approvalRef,
195
- expiresAt: 'not-a-date'
196
- }
197
- });
198
- const openInput = (kind)=>{
21
+ 'search.patch': { patch: { destination: { query: 'Dubai' } } }, 'search.run': {}, 'results.view.patch': { patch: { sort: 'recommended' } }, 'hotel.focus': { hotelRef: 'hotel-1' }, 'hotel.select': { hotelRef: 'hotel-1' },
22
+ 'offers.query': { hotelRefs: ['hotel-1'], criteria: {} }, 'offers.view.patch': { hotelRef: 'hotel-1', criteria: {} }, 'offers.compare': { offerRefs: ['offer-1'], requestedCount: 1 },
23
+ 'offer.select': { offerRef: 'offer-1', offerVersionRef: 'offer-1:v1' }, 'offer.check': { offerRef: 'offer-1', offerVersionRef: 'offer-1:v1' }, 'checkout.prepare': { offerRef: 'offer-1', offerVersionRef: 'offer-1:v1', verifiedOfferRef: 'verified-1' }, 'order.observe': { orderRef: 'order-1' },
24
+ };
25
+ const actions = Object.fromEntries(BOOKING_READ_ACTION_KINDS.map((kind) => [kind, { schemaVersion: 'booking.surface', kind, actionId: `a-${kind.replaceAll('.', '-')}`, contextRef: 'ctx-1', expectedRevision: 0, reason: 'typed request', factRefs: [], input: inputs[kind] }]));
26
+ const approvalRef = { approvalId: 'ap-1', blockerId: 'b-1', contextRef: 'ctx-1', sourceTurnId: 'turn-1', presentationRequestKey: 'approval:task-1:turn-1:a-search-run:' + hash, sourceActionId: 'a-search-run', targetActionId: 'a-search-run', sourceRevision: 0, targetActionKind: 'search.run', to: 'prefer', expiresAt: '2099-01-01T00:00:00.000Z', nonce: 'nonce-1', sourceReceiptDigest: hash, scope: 'offer', code: 'hotel_rates_failed', criterionPath: 'offers.totalPriceMax', valueDigest: hash };
27
+ accept('valid relaxation approval ref action', { ...actions['search.run'], relaxationApprovalRef: approvalRef });
28
+ reject('approval context replay', { ...actions['search.run'], relaxationApprovalRef: { ...approvalRef, contextRef: 'ctx-other' } });
29
+ reject('approval action replay', { ...actions['search.run'], relaxationApprovalRef: { ...approvalRef, targetActionId: 'a-other' } });
30
+ reject('approval future revision replay', { ...actions['search.run'], relaxationApprovalRef: { ...approvalRef, sourceRevision: 1 } });
31
+ reject('approval old revision replay', { ...actions['search.run'], expectedRevision: 1, relaxationApprovalRef: { ...approvalRef, sourceRevision: 0 } });
32
+ reject('approval target kind replay', { ...actions['search.run'], relaxationApprovalRef: { ...approvalRef, targetActionKind: 'offer.check' } });
33
+ reject('approval bad nonce', { ...actions['search.run'], relaxationApprovalRef: { ...approvalRef, nonce: 'Bearer token' } });
34
+ reject('approval bad expiry', { ...actions['search.run'], relaxationApprovalRef: { ...approvalRef, expiresAt: 'not-a-date' } });
35
+ const openInput = (kind) => {
199
36
  const input = inputs[kind];
200
- return input && typeof input === 'object' && !Array.isArray(input) ? {
201
- ...input,
202
- open: true
203
- } : {
204
- open: true
205
- };
37
+ return input && typeof input === 'object' && !Array.isArray(input) ? { ...input, open: true } : { open: true };
206
38
  };
207
- for (const kind of BOOKING_READ_ACTION_KINDS){
39
+ for (const kind of BOOKING_READ_ACTION_KINDS) {
208
40
  accept(`valid action ${kind}`, actions[kind]);
209
- reject(`missing input ${kind}`, {
210
- ...actions[kind],
211
- input: undefined
212
- });
213
- reject(`open input ${kind}`, {
214
- ...actions[kind],
215
- input: openInput(kind)
216
- });
41
+ reject(`missing input ${kind}`, { ...actions[kind], input: undefined });
42
+ reject(`open input ${kind}`, { ...actions[kind], input: openInput(kind) });
217
43
  }
218
- reject('offer.select missing version', {
219
- ...actions['offer.select'],
220
- input: {
221
- offerRef: 'offer-1'
222
- }
223
- });
224
- reject('offer.check missing version', {
225
- ...actions['offer.check'],
226
- input: {
227
- offerRef: 'offer-1'
228
- }
229
- });
230
- reject('checkout.prepare missing version', {
231
- ...actions['checkout.prepare'],
232
- input: {
233
- offerRef: 'offer-1',
234
- verifiedOfferRef: 'verified-1'
235
- }
236
- });
44
+ reject('offer.select missing version', { ...actions['offer.select'], input: { offerRef: 'offer-1' } });
45
+ reject('offer.check missing version', { ...actions['offer.check'], input: { offerRef: 'offer-1' } });
46
+ reject('checkout.prepare missing version', { ...actions['checkout.prepare'], input: { offerRef: 'offer-1', verifiedOfferRef: 'verified-1' } });
237
47
  const versionedWorkspace = {
238
48
  ...workspace,
239
- visibleHotels: [
240
- {
241
- hotelRef: 'hotel-1',
242
- name: 'Hotel 1',
243
- factRefs: [
244
- 'hotel-fact-1'
245
- ]
246
- }
247
- ],
248
- loadedOffers: [
249
- {
250
- offerRef: 'offer-1',
251
- offerVersionRef: 'offer-1:v1',
252
- hotelRef: 'hotel-1',
253
- evidenceLevel: 'rate_loaded',
254
- factRefs: [
255
- 'offer-fact-1'
256
- ]
257
- }
258
- ],
49
+ visibleHotels: [{ hotelRef: 'hotel-1', name: 'Hotel 1', factRefs: ['hotel-fact-1'] }],
50
+ loadedOffers: [{ offerRef: 'offer-1', offerVersionRef: 'offer-1:v1', hotelRef: 'hotel-1', evidenceLevel: 'rate_loaded', factRefs: ['offer-fact-1'] }],
259
51
  selectedOfferRef: 'offer-1',
260
- verifiedOffer: {
261
- offerRef: 'offer-1',
262
- offerVersionRef: 'offer-1:v1',
263
- verifiedOfferRef: 'verified-1',
264
- expiresAt: '2030-02-28T23:59:59+04:00'
265
- }
266
- };
267
- const versionedTurn = {
268
- schemaVersion: 'booking.surface',
269
- kind: 'user.turn',
270
- taskId: 'versioned-task',
271
- turnId: 'versioned-turn',
272
- workspace: versionedWorkspace,
273
- request: {
274
- text: 'find hotels'
275
- }
52
+ verifiedOffer: { offerRef: 'offer-1', offerVersionRef: 'offer-1:v1', verifiedOfferRef: 'verified-1', expiresAt: '2030-02-28T23:59:59+04:00' },
276
53
  };
54
+ const versionedTurn = { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: 'versioned-task', turnId: 'versioned-turn', workspace: versionedWorkspace, request: { text: 'find hotels' } };
277
55
  accept('versioned loaded offer and verified tuple', versionedTurn);
278
- reject('verified offer invalid calendar expiry', {
279
- ...versionedTurn,
280
- workspace: {
281
- ...versionedWorkspace,
282
- verifiedOffer: {
283
- ...versionedWorkspace.verifiedOffer,
284
- expiresAt: '2026-02-30T00:00:00.000Z'
285
- }
286
- }
287
- });
288
- reject('verified offer malformed expiry', {
289
- ...versionedTurn,
290
- workspace: {
291
- ...versionedWorkspace,
292
- verifiedOffer: {
293
- ...versionedWorkspace.verifiedOffer,
294
- expiresAt: 'tomorrow'
295
- }
296
- }
297
- });
298
- reject('verified offer missing expiry', {
299
- ...versionedTurn,
300
- workspace: {
301
- ...versionedWorkspace,
302
- verifiedOffer: {
303
- ...versionedWorkspace.verifiedOffer,
304
- expiresAt: undefined
305
- }
306
- }
307
- });
308
- reject('verified offer mismatched logical tuple', {
309
- ...versionedTurn,
310
- workspace: {
311
- ...versionedWorkspace,
312
- verifiedOffer: {
313
- ...versionedWorkspace.verifiedOffer,
314
- offerRef: 'offer-other',
315
- offerVersionRef: 'offer-other:v1'
316
- }
317
- }
318
- });
319
- reject('verified offer mismatched version tuple', {
320
- ...versionedTurn,
321
- workspace: {
322
- ...versionedWorkspace,
323
- verifiedOffer: {
324
- ...versionedWorkspace.verifiedOffer,
325
- offerVersionRef: 'offer-1:v2'
326
- }
327
- }
328
- });
329
- reject('workspace duplicate logical offer refs', {
330
- ...versionedTurn,
331
- workspace: {
332
- ...versionedWorkspace,
333
- loadedOffers: [
334
- ...versionedWorkspace.loadedOffers,
335
- {
336
- ...versionedWorkspace.loadedOffers[0]
337
- }
338
- ]
339
- }
340
- });
341
- reject('workspace duplicate offer version refs', {
342
- ...versionedTurn,
343
- workspace: {
344
- ...versionedWorkspace,
345
- loadedOffers: [
346
- ...versionedWorkspace.loadedOffers,
347
- {
348
- offerRef: 'offer-2',
349
- offerVersionRef: 'offer-1:v1',
350
- hotelRef: 'hotel-1',
351
- evidenceLevel: 'rate_loaded',
352
- factRefs: []
353
- }
354
- ]
355
- }
356
- });
357
- reject('Book', {
358
- ...actions['search.run'],
359
- kind: 'book'
360
- });
361
- reject('root minimum', {
362
- schemaVersion: 'booking.surface'
363
- });
364
- reject('workspace nested extra', {
365
- ...workspace,
366
- results: {
367
- status: 'idle',
368
- open: true
369
- }
370
- });
371
- reject('workspace capability Book', {
372
- ...workspace,
373
- capabilities: {
374
- ...workspace.capabilities,
375
- allowedActions: [
376
- 'book'
377
- ]
378
- }
379
- });
380
- for (const field of [
381
- 'schemaVersion',
382
- 'kind',
383
- 'actionId',
384
- 'contextRef',
385
- 'expectedRevision'
386
- ])reject(`action missing ${field}`, Object.fromEntries(Object.entries(actions['search.run']).filter(([key])=>key !== field)));
387
- const blocker = {
388
- blockerId: 'b-1',
389
- sourceActionId: 'a-offers-query',
390
- sourceReceiptDigest: hash,
391
- scope: 'offer',
392
- code: 'hotel_rates_failed',
393
- criterionPath: 'offers.totalPriceMax',
394
- strength: 'must',
395
- valueDigest: hash,
396
- evidence: {
397
- factRefs: [
398
- 'fact-1'
399
- ],
400
- gapCodes: [
401
- 'hotel_rates_failed'
402
- ],
403
- requested: 2,
404
- actual: 1
405
- }
406
- };
407
- const approval = {
408
- taskId: 'task-1',
409
- contextRef: 'ctx-1',
410
- sourceTurnId: 'turn-1',
411
- presentationRequestKey: 'approval:task-1:turn-1:a-offers-query:' + hash,
412
- optionDigest: hash,
413
- approvalId: 'ap-1',
414
- deliveryNonce: 'delivery-nonce-1',
415
- blockerId: 'b-1',
416
- sourceActionId: blocker.sourceActionId,
417
- sourceReceiptDigest: hash,
418
- scope: 'offer',
419
- code: blocker.code,
420
- criterionPath: blocker.criterionPath,
421
- valueDigest: hash,
422
- from: 'must',
423
- to: 'prefer',
424
- approved: true
425
- };
56
+ reject('verified offer invalid calendar expiry', { ...versionedTurn, workspace: { ...versionedWorkspace, verifiedOffer: { ...versionedWorkspace.verifiedOffer, expiresAt: '2026-02-30T00:00:00.000Z' } } });
57
+ reject('verified offer malformed expiry', { ...versionedTurn, workspace: { ...versionedWorkspace, verifiedOffer: { ...versionedWorkspace.verifiedOffer, expiresAt: 'tomorrow' } } });
58
+ reject('verified offer missing expiry', { ...versionedTurn, workspace: { ...versionedWorkspace, verifiedOffer: { ...versionedWorkspace.verifiedOffer, expiresAt: undefined } } });
59
+ reject('verified offer mismatched logical tuple', { ...versionedTurn, workspace: { ...versionedWorkspace, verifiedOffer: { ...versionedWorkspace.verifiedOffer, offerRef: 'offer-other', offerVersionRef: 'offer-other:v1' } } });
60
+ reject('verified offer mismatched version tuple', { ...versionedTurn, workspace: { ...versionedWorkspace, verifiedOffer: { ...versionedWorkspace.verifiedOffer, offerVersionRef: 'offer-1:v2' } } });
61
+ reject('workspace duplicate logical offer refs', { ...versionedTurn, workspace: { ...versionedWorkspace, loadedOffers: [...versionedWorkspace.loadedOffers, { ...versionedWorkspace.loadedOffers[0] }] } });
62
+ reject('workspace duplicate offer version refs', { ...versionedTurn, workspace: { ...versionedWorkspace, loadedOffers: [...versionedWorkspace.loadedOffers, { offerRef: 'offer-2', offerVersionRef: 'offer-1:v1', hotelRef: 'hotel-1', evidenceLevel: 'rate_loaded', factRefs: [] }] } });
63
+ reject('Book', { ...actions['search.run'], kind: 'book' });
64
+ reject('root minimum', { schemaVersion: 'booking.surface' });
65
+ reject('workspace nested extra', { ...workspace, results: { status: 'idle', open: true } });
66
+ reject('workspace capability Book', { ...workspace, capabilities: { ...workspace.capabilities, allowedActions: ['book'] } });
67
+ for (const field of ['schemaVersion', 'kind', 'actionId', 'contextRef', 'expectedRevision'])
68
+ reject(`action missing ${field}`, Object.fromEntries(Object.entries(actions['search.run']).filter(([key]) => key !== field)));
69
+ const blocker = { blockerId: 'b-1', sourceActionId: 'a-offers-query', sourceReceiptDigest: hash, scope: 'offer', code: 'hotel_rates_failed', criterionPath: 'offers.totalPriceMax', strength: 'must', valueDigest: hash, evidence: { factRefs: ['fact-1'], gapCodes: ['hotel_rates_failed'], requested: 2, actual: 1 } };
70
+ const approval = { taskId: 'task-1', contextRef: 'ctx-1', sourceTurnId: 'turn-1', presentationRequestKey: 'approval:task-1:turn-1:a-offers-query:' + hash, optionDigest: hash, approvalId: 'ap-1', deliveryNonce: 'delivery-nonce-1', blockerId: 'b-1', sourceActionId: blocker.sourceActionId, sourceReceiptDigest: hash, scope: 'offer', code: blocker.code, criterionPath: blocker.criterionPath, valueDigest: hash, from: 'must', to: 'prefer', approved: true };
426
71
  assert.equal(validateCriterionBlocker(blocker).ok, true);
427
72
  positive++;
428
73
  assert.equal(validateRelaxationApproval(approval).ok, true);
429
74
  positive++;
430
75
  assert.equal(validateApprovalAgainstBlocker(approval, blocker).ok, true);
431
76
  positive++;
432
- for (const field of [
433
- 'sourceReceiptDigest',
434
- 'valueDigest'
435
- ]){
436
- assert.equal(validateCriterionBlocker({
437
- ...blocker,
438
- [field]: 'bad'
439
- }).ok, false);
77
+ for (const field of ['sourceReceiptDigest', 'valueDigest']) {
78
+ assert.equal(validateCriterionBlocker({ ...blocker, [field]: 'bad' }).ok, false);
440
79
  negative++;
441
80
  }
442
- for (const field of [
443
- 'code',
444
- 'scope',
445
- 'criterionPath'
446
- ]){
447
- assert.equal(validateCriterionBlocker({
448
- ...blocker,
449
- [field]: field === 'criterionPath' ? 'open.path' : 'dynamic'
450
- }).ok, false);
81
+ for (const field of ['code', 'scope', 'criterionPath']) {
82
+ assert.equal(validateCriterionBlocker({ ...blocker, [field]: field === 'criterionPath' ? 'open.path' : 'dynamic' }).ok, false);
451
83
  negative++;
452
84
  }
453
- assert.equal(validateCriterionBlocker({
454
- ...blocker,
455
- evidence: {
456
- ...blocker.evidence,
457
- factRefs: [
458
- 'fact-1',
459
- 'fact-1'
460
- ]
461
- }
462
- }).ok, false);
85
+ assert.equal(validateCriterionBlocker({ ...blocker, evidence: { ...blocker.evidence, factRefs: ['fact-1', 'fact-1'] } }).ok, false);
463
86
  negative++;
464
- assert.equal(validateCriterionBlocker({
465
- ...blocker,
466
- evidence: {
467
- ...blocker.evidence,
468
- open: true
469
- }
470
- }).ok, false);
87
+ assert.equal(validateCriterionBlocker({ ...blocker, evidence: { ...blocker.evidence, open: true } }).ok, false);
471
88
  negative++;
472
- const receipt = {
473
- schemaVersion: 'booking.surface',
474
- kind: 'action.receipt',
475
- actionId: 'a-search-run',
476
- contextRef: 'ctx-1',
477
- status: 'partial',
478
- revision: 1,
479
- observation: {
480
- kind: 'gap',
481
- code: 'search_failed',
482
- factRefs: [
483
- 'fact-1'
484
- ]
485
- },
486
- resultContract: {
487
- outcome: 'partial',
488
- hardCriteriaMet: false,
489
- factRefs: [
490
- 'fact-1'
491
- ],
492
- gapCodes: [
493
- ...BOOKING_GAP_CODES
494
- ],
495
- blockers: [
496
- blocker
497
- ],
498
- relaxationsApplied: []
499
- }
500
- };
89
+ const receipt = { schemaVersion: 'booking.surface', kind: 'action.receipt', actionId: 'a-search-run', contextRef: 'ctx-1', status: 'partial', revision: 1, observation: { kind: 'gap', code: 'search_failed', factRefs: ['fact-1'] }, resultContract: { outcome: 'partial', hardCriteriaMet: false, factRefs: ['fact-1'], gapCodes: [...BOOKING_GAP_CODES], blockers: [blocker], relaxationsApplied: [] } };
501
90
  accept('valid receipt all gap codes', receipt);
502
- reject('bad status', {
503
- ...receipt,
504
- status: 'unknown'
505
- });
506
- reject('bad revision', {
507
- ...receipt,
508
- revision: -1
509
- });
510
- reject('bad outcome', {
511
- ...receipt,
512
- resultContract: {
513
- ...receipt.resultContract,
514
- outcome: 'bad'
515
- }
516
- });
517
- reject('dynamic gap', {
518
- ...receipt,
519
- resultContract: {
520
- ...receipt.resultContract,
521
- gapCodes: [
522
- 'dynamic'
523
- ]
524
- }
525
- });
526
- reject('receipt observation extra', {
527
- ...receipt,
528
- observation: {
529
- ...receipt.observation,
530
- open: true
531
- }
532
- });
533
- for (const code of BOOKING_GAP_CODES){
534
- accept(`valid declared gap ${code}`, {
535
- ...receipt,
536
- resultContract: {
537
- ...receipt.resultContract,
538
- gapCodes: [
539
- code
540
- ]
541
- }
542
- });
543
- reject(`dynamic gap ${code}`, {
544
- ...receipt,
545
- resultContract: {
546
- ...receipt.resultContract,
547
- gapCodes: [
548
- `${code}:dynamic`
549
- ]
550
- }
551
- });
91
+ reject('bad status', { ...receipt, status: 'unknown' });
92
+ reject('bad revision', { ...receipt, revision: -1 });
93
+ reject('bad outcome', { ...receipt, resultContract: { ...receipt.resultContract, outcome: 'bad' } });
94
+ reject('dynamic gap', { ...receipt, resultContract: { ...receipt.resultContract, gapCodes: ['dynamic'] } });
95
+ reject('receipt observation extra', { ...receipt, observation: { ...receipt.observation, open: true } });
96
+ for (const code of BOOKING_GAP_CODES) {
97
+ accept(`valid declared gap ${code}`, { ...receipt, resultContract: { ...receipt.resultContract, gapCodes: [code] } });
98
+ reject(`dynamic gap ${code}`, { ...receipt, resultContract: { ...receipt.resultContract, gapCodes: [`${code}:dynamic`] } });
552
99
  }
553
100
  const observations = [
554
- {
555
- kind: 'search.state',
556
- searchSessionRef: 'session-1',
557
- resultCount: 1
558
- },
559
- {
560
- kind: 'results.state',
561
- matchedHotelRefs: [
562
- 'hotel-1'
563
- ],
564
- visibleCount: 1
565
- },
566
- {
567
- kind: 'hotel.focus',
568
- hotelRef: 'hotel-1'
569
- },
570
- {
571
- kind: 'hotel.selection',
572
- hotelRef: 'hotel-1'
573
- },
574
- {
575
- kind: 'offers.state',
576
- hotelRefs: [
577
- 'hotel-1'
578
- ],
579
- offerRefs: [
580
- 'offer-1'
581
- ],
582
- loadedHotelCount: 1
583
- },
584
- {
585
- kind: 'offer.selection',
586
- offerRef: 'offer-1',
587
- offerVersionRef: 'offer-1:v1'
588
- },
589
- {
590
- kind: 'offer.availability',
591
- offerRef: 'offer-1',
592
- checkedOfferVersionRef: 'offer-1:v1',
593
- currentOfferVersionRef: 'offer-1:v1',
594
- available: true,
595
- changedFactRefs: []
596
- },
597
- {
598
- kind: 'checkout.handoff',
599
- offerRef: 'offer-1',
600
- offerVersionRef: 'offer-1:v1',
601
- verifiedOfferRef: 'verified-1',
602
- handoffRef: 'handoff-1'
603
- },
604
- {
605
- kind: 'order.state',
606
- orderRef: 'order-1',
607
- state: 'pending'
608
- },
609
- {
610
- kind: 'gap',
611
- code: 'search_failed',
612
- factRefs: [
613
- 'fact-1'
614
- ]
615
- }
101
+ { kind: 'search.state', searchSessionRef: 'session-1', resultCount: 1 }, { kind: 'results.state', matchedHotelRefs: ['hotel-1'], visibleCount: 1 },
102
+ { kind: 'hotel.focus', hotelRef: 'hotel-1' }, { kind: 'hotel.selection', hotelRef: 'hotel-1' }, { kind: 'offers.state', hotelRefs: ['hotel-1'], offerRefs: ['offer-1'], loadedHotelCount: 1 },
103
+ { kind: 'offer.selection', offerRef: 'offer-1', offerVersionRef: 'offer-1:v1' }, { kind: 'offer.availability', offerRef: 'offer-1', checkedOfferVersionRef: 'offer-1:v1', currentOfferVersionRef: 'offer-1:v1', available: true, changedFactRefs: [] },
104
+ { kind: 'checkout.handoff', offerRef: 'offer-1', offerVersionRef: 'offer-1:v1', verifiedOfferRef: 'verified-1', handoffRef: 'handoff-1' }, { kind: 'order.state', orderRef: 'order-1', state: 'pending' }, { kind: 'gap', code: 'search_failed', factRefs: ['fact-1'] },
616
105
  ];
617
- for (const observation of observations){
618
- accept(`valid observation ${observation.kind}`, {
619
- ...receipt,
620
- observation
621
- });
622
- reject(`observation extra ${observation.kind}`, {
623
- ...receipt,
624
- observation: {
625
- ...observation,
626
- extra: true
627
- }
628
- });
106
+ for (const observation of observations) {
107
+ accept(`valid observation ${observation.kind}`, { ...receipt, observation });
108
+ reject(`observation extra ${observation.kind}`, { ...receipt, observation: { ...observation, extra: true } });
629
109
  }
630
- for (const observation of observations.filter((item)=>[
631
- 'search.state',
632
- 'results.state',
633
- 'offers.state',
634
- 'offer.availability',
635
- 'order.state'
636
- ].includes(item.kind))){
637
- reject(`observation dynamic gap ${observation.kind}`, {
638
- ...receipt,
639
- observation: {
640
- ...observation,
641
- gapCodes: [
642
- 'dynamic'
643
- ]
644
- }
645
- });
110
+ for (const observation of observations.filter((item) => ['search.state', 'results.state', 'offers.state', 'offer.availability', 'order.state'].includes(item.kind))) {
111
+ reject(`observation dynamic gap ${observation.kind}`, { ...receipt, observation: { ...observation, gapCodes: ['dynamic'] } });
646
112
  }
647
- reject('gap observation dynamic code', {
648
- ...receipt,
649
- observation: {
650
- kind: 'gap',
651
- code: 'dynamic',
652
- factRefs: []
653
- }
654
- });
655
- const turn = {
656
- schemaVersion: 'booking.surface',
657
- kind: 'user.turn',
658
- taskId: 'task-1',
659
- turnId: 'turn-1',
660
- workspace,
661
- request: {
662
- text: 'find hotels'
663
- }
664
- };
665
- const ingress = {
666
- schemaVersion: 'booking.surface',
667
- kind: 'user.turn.ingress',
668
- requestKey: 'request-1',
669
- taskHandle: 'opaque-task-handle-1',
670
- surfaceHint: 'tenant',
671
- workspace: ingressWorkspace,
672
- request: {
673
- text: 'find hotels'
674
- }
675
- };
676
- const continuation = {
677
- schemaVersion: 'booking.surface',
678
- kind: 'action.receipt.continuation',
679
- taskId: 'task-1',
680
- workspace,
681
- receipt
682
- };
113
+ reject('gap observation dynamic code', { ...receipt, observation: { kind: 'gap', code: 'dynamic', factRefs: [] } });
114
+ const turn = { schemaVersion: 'booking.surface', kind: 'user.turn', taskId: 'task-1', turnId: 'turn-1', workspace, request: { text: 'find hotels' } };
115
+ const ingress = { schemaVersion: 'booking.surface', kind: 'user.turn.ingress', requestKey: 'request-1', taskHandle: 'opaque-task-handle-1', surfaceHint: 'tenant', workspace: ingressWorkspace, request: { text: 'find hotels' } };
116
+ const continuation = { schemaVersion: 'booking.surface', kind: 'action.receipt.continuation', taskId: 'task-1', workspace, receipt };
683
117
  accept('valid user turn', turn);
684
118
  accept('valid ingress', ingress);
685
- accept('valid ingress approval carries executable typed approval', {
686
- ...ingress,
687
- request: {
688
- text: 'relax must criterion',
689
- approval
690
- }
691
- });
119
+ accept('valid ingress approval carries executable typed approval', { ...ingress, request: { text: 'relax must criterion', approval } });
692
120
  accept('valid continuation', continuation);
693
- reject('empty text', {
694
- ...turn,
695
- request: {
696
- text: ''
697
- }
698
- });
699
- reject('multiple approvals', {
700
- ...turn,
701
- request: {
702
- text: 'x',
703
- approval: [
704
- approval,
705
- approval
706
- ]
707
- }
708
- });
709
- reject('ingress multiple approvals', {
710
- ...ingress,
711
- request: {
712
- text: 'x',
713
- approval: [
714
- approval,
715
- approval
716
- ]
717
- }
718
- });
719
- accept('text only remains non-approval', {
720
- ...turn,
721
- request: {
722
- text: 'please relax must'
723
- }
724
- });
725
- const selectedIngressWorkspace = {
726
- ...ingressWorkspace,
727
- visibleHotels: [
728
- {
729
- hotelRef: 'hotel-1',
730
- name: 'Hotel 1',
731
- factRefs: [
732
- 'hotel-fact-1'
733
- ]
734
- }
735
- ],
736
- loadedOffers: [
737
- {
738
- offerRef: 'offer-1',
739
- offerVersionRef: 'offer-1:v1',
740
- hotelRef: 'hotel-1',
741
- evidenceLevel: 'rate_loaded',
742
- factRefs: [
743
- 'offer-fact-1'
744
- ]
745
- }
746
- ],
747
- focusedHotelRef: 'hotel-1',
748
- selectedOfferRef: 'offer-1'
749
- };
750
- accept('ingress preserves visible focus and loaded selection', {
751
- ...ingress,
752
- workspace: selectedIngressWorkspace
753
- });
754
- reject('ingress rejects invisible focus', {
755
- ...ingress,
756
- workspace: {
757
- ...selectedIngressWorkspace,
758
- focusedHotelRef: 'hotel-other'
759
- }
760
- });
761
- reject('ingress rejects unloaded selection', {
762
- ...ingress,
763
- workspace: {
764
- ...selectedIngressWorkspace,
765
- selectedOfferRef: 'offer-other'
766
- }
767
- });
768
- reject('ingress rejects selection whose hotel is not visible', {
769
- ...ingress,
770
- workspace: {
771
- ...selectedIngressWorkspace,
772
- visibleHotels: [
773
- {
774
- hotelRef: 'hotel-other',
775
- name: 'Other',
776
- factRefs: []
777
- }
778
- ]
779
- }
780
- });
781
- reject('ingress never accepts verified authority', {
782
- ...ingress,
783
- workspace: {
784
- ...selectedIngressWorkspace,
785
- verifiedOfferRef: 'verified-offer-1'
786
- }
787
- });
788
- reject('missing user turn identity', {
789
- ...turn,
790
- turnId: undefined
791
- });
792
- reject('missing ingress request key', {
793
- ...ingress,
794
- requestKey: undefined
795
- });
796
- reject('unsafe user turn identity', {
797
- ...turn,
798
- turnId: 'Bearer token'
799
- });
800
- reject('unsafe ingress task handle', {
801
- ...ingress,
802
- taskHandle: 'user@example.com'
803
- });
804
- reject('missing user task identity', {
805
- ...turn,
806
- taskId: undefined
807
- });
808
- reject('browser task identity injection', {
809
- ...ingress,
810
- taskId: 'browser-task'
811
- });
812
- reject('browser turn identity injection', {
813
- ...ingress,
814
- turnId: 'browser-turn'
815
- });
816
- reject('browser context identity injection', {
817
- ...ingress,
818
- contextRef: null
819
- });
820
- reject('browser verified authority injection', {
821
- ...ingress,
822
- workspace: {
823
- ...ingressWorkspace,
824
- verifiedOfferRef: 'verified-offer-1'
825
- }
826
- });
827
- assert.equal(validateCriterionBlocker({
828
- ...blocker,
829
- evidence: {
830
- factRefs: [],
831
- gapCodes: []
832
- }
833
- }).ok, false);
121
+ reject('empty text', { ...turn, request: { text: '' } });
122
+ reject('multiple approvals', { ...turn, request: { text: 'x', approval: [approval, approval] } });
123
+ reject('ingress multiple approvals', { ...ingress, request: { text: 'x', approval: [approval, approval] } });
124
+ accept('text only remains non-approval', { ...turn, request: { text: 'please relax must' } });
125
+ const selectedIngressWorkspace = { ...ingressWorkspace, visibleHotels: [{ hotelRef: 'hotel-1', name: 'Hotel 1', factRefs: ['hotel-fact-1'] }], loadedOffers: [{ offerRef: 'offer-1', offerVersionRef: 'offer-1:v1', hotelRef: 'hotel-1', evidenceLevel: 'rate_loaded', factRefs: ['offer-fact-1'] }], focusedHotelRef: 'hotel-1', selectedOfferRef: 'offer-1' };
126
+ accept('ingress preserves visible focus and loaded selection', { ...ingress, workspace: selectedIngressWorkspace });
127
+ reject('ingress rejects invisible focus', { ...ingress, workspace: { ...selectedIngressWorkspace, focusedHotelRef: 'hotel-other' } });
128
+ reject('ingress rejects unloaded selection', { ...ingress, workspace: { ...selectedIngressWorkspace, selectedOfferRef: 'offer-other' } });
129
+ reject('ingress rejects selection whose hotel is not visible', { ...ingress, workspace: { ...selectedIngressWorkspace, visibleHotels: [{ hotelRef: 'hotel-other', name: 'Other', factRefs: [] }] } });
130
+ reject('ingress never accepts verified authority', { ...ingress, workspace: { ...selectedIngressWorkspace, verifiedOfferRef: 'verified-offer-1' } });
131
+ reject('missing user turn identity', { ...turn, turnId: undefined });
132
+ reject('missing ingress request key', { ...ingress, requestKey: undefined });
133
+ reject('unsafe user turn identity', { ...turn, turnId: 'Bearer token' });
134
+ reject('unsafe ingress task handle', { ...ingress, taskHandle: 'user@example.com' });
135
+ reject('missing user task identity', { ...turn, taskId: undefined });
136
+ reject('browser task identity injection', { ...ingress, taskId: 'browser-task' });
137
+ reject('browser turn identity injection', { ...ingress, turnId: 'browser-turn' });
138
+ reject('browser context identity injection', { ...ingress, contextRef: null });
139
+ reject('browser verified authority injection', { ...ingress, workspace: { ...ingressWorkspace, verifiedOfferRef: 'verified-offer-1' } });
140
+ assert.equal(validateCriterionBlocker({ ...blocker, evidence: { factRefs: [], gapCodes: [] } }).ok, false);
834
141
  negative++;
835
- assert.equal(validateApprovalAgainstBlocker({
836
- ...approval,
837
- blockerId: 'wrong'
838
- }, blocker).ok, false);
142
+ assert.equal(validateApprovalAgainstBlocker({ ...approval, blockerId: 'wrong' }, blocker).ok, false);
839
143
  negative++;
840
- const continuationBlocked = {
841
- ...continuation,
842
- receipt: {
843
- ...receipt,
844
- resultContract: {
845
- ...receipt.resultContract,
846
- hardCriteriaMet: true
847
- }
848
- }
849
- };
144
+ const continuationBlocked = { ...continuation, receipt: { ...receipt, resultContract: { ...receipt.resultContract, hardCriteriaMet: true } } };
850
145
  reject('continuation hard criteria with blocker', continuationBlocked);
851
- reject('formatted phone', {
852
- ...turn,
853
- request: {
854
- text: '+971 (50) 123 4567'
855
- }
856
- });
857
- reject('dotted phone', {
858
- ...turn,
859
- request: {
860
- text: '+971.50.123.4567'
861
- }
862
- });
863
- reject('slash phone', {
864
- ...turn,
865
- request: {
866
- text: 'call +971/50/123/4567'
867
- }
868
- });
869
- reject('comma phone', {
870
- ...turn,
871
- request: {
872
- text: 'call +971,50,123,4567'
873
- }
874
- });
875
- reject('unicode dash phone', {
876
- ...turn,
877
- request: {
878
- text: 'call +971—50—123—4567'
879
- }
880
- });
881
- reject('nbsp phone', {
882
- ...turn,
883
- request: {
884
- text: '+971\u00a050\u00a0123\u00a04567'
885
- }
886
- });
887
- reject('embedded phone', {
888
- ...turn,
889
- request: {
890
- text: 'call +971 50 123 4567 please'
891
- }
892
- });
893
- reject('payment card', {
894
- ...turn,
895
- request: {
896
- text: '4111 1111 1111 1111'
897
- }
898
- });
899
- reject('supplier cost', {
900
- ...turn,
901
- request: {
902
- text: 'supplier net cost AED 42'
903
- }
904
- });
905
- reject('supplier cost colon', {
906
- ...turn,
907
- request: {
908
- text: 'supplier net cost: AED 42'
909
- }
910
- });
911
- reject('supplier possessive cost', {
912
- ...turn,
913
- request: {
914
- text: "supplier's net cost AED 42"
915
- }
916
- });
917
- reject('supplier cost reversed currency', {
918
- ...turn,
919
- request: {
920
- text: 'supplier cost 42 AED'
921
- }
922
- });
923
- reject('supplier wholesale', {
924
- ...turn,
925
- request: {
926
- text: 'supplier wholesale rate AED 42'
927
- }
928
- });
929
- reject('supplier base bypass', {
930
- ...turn,
931
- request: {
932
- text: 'supplier base cost AED 42'
933
- }
934
- });
935
- reject('vendor cost', {
936
- ...turn,
937
- request: {
938
- text: 'vendor net cost AED 42'
939
- }
940
- });
941
- reject('vendor compact amount', {
942
- ...turn,
943
- request: {
944
- text: 'vendor price AED42'
945
- }
946
- });
947
- reject('internal supplier cost', {
948
- ...turn,
949
- request: {
950
- text: 'internal supplier cost AED 42'
951
- }
952
- });
953
- reject('internal reversed amount', {
954
- ...turn,
955
- request: {
956
- text: 'internal rate 42 USD'
957
- }
958
- });
959
- reject('contact email bypass', {
960
- ...turn,
961
- request: {
962
- text: 'contact user@hotelbyte.com'
963
- }
964
- });
965
- reject('quoted email local part', {
966
- ...turn,
967
- request: {
968
- text: 'contact "user"@hotelbyte.com'
969
- }
970
- });
971
- reject('unicode email local part', {
972
- ...turn,
973
- request: {
974
- text: '联系 用户@hotelbyte.com'
975
- }
976
- });
977
- reject('technical-looking real mailbox', {
978
- ...turn,
979
- request: {
980
- text: 'version@company.com'
981
- }
982
- });
983
- accept('benign trace identifier', {
984
- ...turn,
985
- request: {
986
- text: 'trace 0000000000000000'
987
- }
988
- });
989
- accept('benign sk-ills sentence', {
990
- ...turn,
991
- request: {
992
- text: 'sk-ills are useful'
993
- }
994
- });
995
- accept('benign version email', {
996
- ...turn,
997
- request: {
998
- text: 'version@release.example'
999
- }
1000
- });
1001
- accept('benign schema identifier', {
1002
- ...turn,
1003
- request: {
1004
- text: 'schema@v2.example'
1005
- }
1006
- });
1007
- accept('benign budget', {
1008
- ...turn,
1009
- request: {
1010
- text: 'budget AED 1000'
1011
- }
1012
- });
1013
- accept('benign total price', {
1014
- ...turn,
1015
- request: {
1016
- text: 'total price under AED 1000'
1017
- }
1018
- });
1019
- const common = {
1020
- schemaVersion: 'booking.surface',
1021
- eventId: 'e-1',
1022
- taskId: 'task-1',
1023
- contextRef: 'ctx-1',
1024
- sequence: 1,
1025
- emittedAt: '2026-08-30T12:00:00.000Z'
1026
- };
1027
- const questionEvent = {
1028
- ...common,
1029
- kind: 'question',
1030
- question: {
1031
- questionId: 'q-1',
1032
- prompt: 'Relax?',
1033
- missingFields: [],
1034
- type: 'relaxation_approval_required',
1035
- blocker,
1036
- approvalOptions: [
1037
- {
1038
- approval
1039
- }
1040
- ]
1041
- }
1042
- };
1043
- const events = [
1044
- {
1045
- ...common,
1046
- kind: 'status',
1047
- status: 'working'
1048
- },
1049
- questionEvent,
1050
- {
1051
- ...common,
1052
- kind: 'operation',
1053
- action: actions['search.run']
1054
- },
1055
- {
1056
- ...common,
1057
- kind: 'explanation',
1058
- explanation: {
1059
- text: 'x',
1060
- factRefs: []
1061
- }
1062
- },
1063
- {
1064
- ...common,
1065
- kind: 'terminal',
1066
- terminal: {
1067
- status: 'completed',
1068
- summary: 'x',
1069
- factRefs: []
1070
- }
1071
- },
1072
- {
1073
- ...common,
1074
- kind: 'error',
1075
- error: {
1076
- code: 'ERR',
1077
- message: 'x',
1078
- retryable: false
1079
- }
1080
- }
1081
- ];
1082
- assert.equal(validateBookingSurfaceEvent({
1083
- ...questionEvent,
1084
- question: {
1085
- ...questionEvent.question,
1086
- approvalOptions: [
1087
- {
1088
- approval: {
1089
- ...approval,
1090
- blockerId: 'wrong'
1091
- }
1092
- }
1093
- ]
1094
- }
1095
- }).ok, false);
146
+ reject('formatted phone', { ...turn, request: { text: '+971 (50) 123 4567' } });
147
+ reject('dotted phone', { ...turn, request: { text: '+971.50.123.4567' } });
148
+ reject('slash phone', { ...turn, request: { text: 'call +971/50/123/4567' } });
149
+ reject('comma phone', { ...turn, request: { text: 'call +971,50,123,4567' } });
150
+ reject('unicode dash phone', { ...turn, request: { text: 'call +971—50—123—4567' } });
151
+ reject('nbsp phone', { ...turn, request: { text: '+971\u00a050\u00a0123\u00a04567' } });
152
+ reject('embedded phone', { ...turn, request: { text: 'call +971 50 123 4567 please' } });
153
+ reject('payment card', { ...turn, request: { text: '4111 1111 1111 1111' } });
154
+ reject('supplier cost', { ...turn, request: { text: 'supplier net cost AED 42' } });
155
+ reject('supplier cost colon', { ...turn, request: { text: 'supplier net cost: AED 42' } });
156
+ reject('supplier possessive cost', { ...turn, request: { text: "supplier's net cost AED 42" } });
157
+ reject('supplier cost reversed currency', { ...turn, request: { text: 'supplier cost 42 AED' } });
158
+ reject('supplier wholesale', { ...turn, request: { text: 'supplier wholesale rate AED 42' } });
159
+ reject('supplier base bypass', { ...turn, request: { text: 'supplier base cost AED 42' } });
160
+ reject('vendor cost', { ...turn, request: { text: 'vendor net cost AED 42' } });
161
+ reject('vendor compact amount', { ...turn, request: { text: 'vendor price AED42' } });
162
+ reject('internal supplier cost', { ...turn, request: { text: 'internal supplier cost AED 42' } });
163
+ reject('internal reversed amount', { ...turn, request: { text: 'internal rate 42 USD' } });
164
+ reject('contact email bypass', { ...turn, request: { text: 'contact user@hotelbyte.com' } });
165
+ reject('quoted email local part', { ...turn, request: { text: 'contact "user"@hotelbyte.com' } });
166
+ reject('unicode email local part', { ...turn, request: { text: '联系 用户@hotelbyte.com' } });
167
+ reject('technical-looking real mailbox', { ...turn, request: { text: 'version@company.com' } });
168
+ accept('benign trace identifier', { ...turn, request: { text: 'trace 0000000000000000' } });
169
+ accept('benign sk-ills sentence', { ...turn, request: { text: 'sk-ills are useful' } });
170
+ accept('benign version email', { ...turn, request: { text: 'version@release.example' } });
171
+ accept('benign schema identifier', { ...turn, request: { text: 'schema@v2.example' } });
172
+ accept('benign budget', { ...turn, request: { text: 'budget AED 1000' } });
173
+ accept('benign total price', { ...turn, request: { text: 'total price under AED 1000' } });
174
+ const common = { schemaVersion: 'booking.surface', eventId: 'e-1', taskId: 'task-1', contextRef: 'ctx-1', sequence: 1, emittedAt: '2026-08-30T12:00:00.000Z' };
175
+ const questionEvent = { ...common, kind: 'question', question: { questionId: 'q-1', prompt: 'Relax?', missingFields: [], type: 'relaxation_approval_required', blocker, approvalOptions: [{ approval }] } };
176
+ const events = [{ ...common, kind: 'status', status: 'working' }, questionEvent, { ...common, kind: 'operation', action: actions['search.run'] }, { ...common, kind: 'explanation', explanation: { text: 'x', factRefs: [] } }, { ...common, kind: 'terminal', terminal: { status: 'completed', summary: 'x', factRefs: [] } }, { ...common, kind: 'error', error: { code: 'ERR', message: 'x', retryable: false } }];
177
+ assert.equal(validateBookingSurfaceEvent({ ...questionEvent, question: { ...questionEvent.question, approvalOptions: [{ approval: { ...approval, blockerId: 'wrong' } }] } }).ok, false);
1096
178
  negative++;
1097
- assert.equal(validateBookingSurfaceEvent({
1098
- ...questionEvent,
1099
- question: {
1100
- ...questionEvent.question,
1101
- approvalOptions: [
1102
- {
1103
- approval: {
1104
- ...approval,
1105
- taskId: 'task-other'
1106
- }
1107
- }
1108
- ]
1109
- }
1110
- }).ok, false);
179
+ assert.equal(validateBookingSurfaceEvent({ ...questionEvent, question: { ...questionEvent.question, approvalOptions: [{ approval: { ...approval, taskId: 'task-other' } }] } }).ok, false);
1111
180
  negative++;
1112
- assert.equal(validateBookingSurfaceEvent({
1113
- ...questionEvent,
1114
- question: {
1115
- ...questionEvent.question,
1116
- approvalOptions: [
1117
- {
1118
- approval: {
1119
- ...approval,
1120
- contextRef: 'ctx-other'
1121
- }
1122
- }
1123
- ]
1124
- }
1125
- }).ok, false);
181
+ assert.equal(validateBookingSurfaceEvent({ ...questionEvent, question: { ...questionEvent.question, approvalOptions: [{ approval: { ...approval, contextRef: 'ctx-other' } }] } }).ok, false);
1126
182
  negative++;
1127
- for (const event of events)accept(`valid event ${event.kind}`, event);
1128
- assert.equal(validateBookingSurfaceEvent({
1129
- ...events[5],
1130
- eventId: 'event-4242-4242-4242-4242'
1131
- }).ok, true);
183
+ for (const event of events)
184
+ accept(`valid event ${event.kind}`, event);
185
+ assert.equal(validateBookingSurfaceEvent({ ...events[5], eventId: 'event-4242-4242-4242-4242' }).ok, true);
1132
186
  positive++;
1133
- assert.equal(validateBookingSurfaceEvent({
1134
- ...events[3],
1135
- explanation: {
1136
- text: 'card 4111 1111 1111 1111',
1137
- factRefs: []
1138
- }
1139
- }).ok, false);
187
+ assert.equal(validateBookingSurfaceEvent({ ...events[3], explanation: { text: 'card 4111 1111 1111 1111', factRefs: [] } }).ok, false);
1140
188
  negative++;
1141
- reject('event mixed payload', {
1142
- ...events[0],
1143
- question: {}
1144
- });
1145
- reject('event missing payload', {
1146
- ...events[1],
1147
- question: undefined
1148
- });
1149
- reject('event open nested', {
1150
- ...events[4],
1151
- terminal: {
1152
- status: 'completed',
1153
- summary: 'x',
1154
- factRefs: [],
1155
- open: true
1156
- }
1157
- });
1158
- reject('request key whitespace', {
1159
- ...ingress,
1160
- requestKey: 'request key'
1161
- });
1162
- reject('request key newline', {
1163
- ...ingress,
1164
- requestKey: 'request-1\nnext'
1165
- });
1166
- reject('request key path', {
1167
- ...ingress,
1168
- requestKey: 'request/path'
1169
- });
1170
- reject('request key email', {
1171
- ...ingress,
1172
- requestKey: 'user@example.com'
1173
- });
1174
- reject('request key secret-like', {
1175
- ...ingress,
1176
- requestKey: 'sk-live-abcdefghijkl'
1177
- });
1178
- reject('request key too short', {
1179
- ...ingress,
1180
- requestKey: 'r1'
1181
- });
1182
- accept('ISO date and currency', {
1183
- ...workspace,
1184
- searchDraft: {
1185
- stay: {
1186
- checkIn: '2026-09-01',
1187
- checkOut: '2026-09-03'
1188
- }
1189
- }
1190
- });
1191
- reject('email secret', {
1192
- ...turn,
1193
- request: {
1194
- text: 'user@example.com'
1195
- }
1196
- });
1197
- reject('Bearer secret', {
1198
- ...turn,
1199
- request: {
1200
- text: 'Bearer abc'
1201
- }
1202
- });
1203
- reject('sk secret', {
1204
- ...turn,
1205
- request: {
1206
- text: 'sk-live-abcdefghijkl'
1207
- }
1208
- });
1209
- reject('JWT secret', {
1210
- ...turn,
1211
- request: {
1212
- text: 'eyJabc.def.ghi'
1213
- }
1214
- });
189
+ reject('event mixed payload', { ...events[0], question: {} });
190
+ reject('event missing payload', { ...events[1], question: undefined });
191
+ reject('event open nested', { ...events[4], terminal: { status: 'completed', summary: 'x', factRefs: [], open: true } });
192
+ reject('request key whitespace', { ...ingress, requestKey: 'request key' });
193
+ reject('request key newline', { ...ingress, requestKey: 'request-1\nnext' });
194
+ reject('request key path', { ...ingress, requestKey: 'request/path' });
195
+ reject('request key email', { ...ingress, requestKey: 'user@example.com' });
196
+ reject('request key secret-like', { ...ingress, requestKey: 'sk-live-abcdefghijkl' });
197
+ reject('request key too short', { ...ingress, requestKey: 'r1' });
198
+ accept('ISO date and currency', { ...workspace, searchDraft: { stay: { checkIn: '2026-09-01', checkOut: '2026-09-03' } } });
199
+ reject('email secret', { ...turn, request: { text: 'user@example.com' } });
200
+ reject('Bearer secret', { ...turn, request: { text: 'Bearer abc' } });
201
+ reject('sk secret', { ...turn, request: { text: 'sk-live-abcdefghijkl' } });
202
+ reject('JWT secret', { ...turn, request: { text: 'eyJabc.def.ghi' } });
1215
203
  accept('E164-like ordinary date safe', turn);
1216
204
  assert.equal(createHash('sha256').update(readFileSync(new URL('../../schemas/booking.surface.schema.json', import.meta.url))).digest('hex'), BOOKING_SURFACE_SCHEMA_SHA256);
1217
205
  console.log(`BOOKING SURFACE CONTRACT PROOF: positive=${positive} negative=${negative}`);
1218
206
  assert.ok(positive >= 65 && negative >= 105);
1219
-
1220
-
1221
- //# sourceURL=ts/scripts/booking-surface-contract-proof-tests.ts