@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,20 +1,33 @@
1
+ /**
2
+ * 效应解译器测试(effect_interpreter.v1,issue #16 采纳,ADR-18;纯离线零网络):
3
+ * 1. 注册表封闭性:未登记效应 → 结构化拒绝面(不抛错,declined=unknown-effect)
4
+ * 2. 指数退避 withRetry:500→1000→2000 封顶链;累计记账;非瞬时失败不重试
5
+ * 3. 断路器三态:closed→(连续失败达阈)→open→(冷却)→half-open 单探测→成功回 closed/失败再开
6
+ * 4. 生产解译器×瞬时失败:重试后成功(trace.attempts/backoff 记账,evidence 行)
7
+ * 5. FlyAI×Sentinel:上游说不永不重试(attempts=1);连续失败计入熔断,熔断后零执行成本
8
+ * 6. 断路拒绝面:不执行、不抛错、平铺失败观察;冷却后 half-open 单探测成功 → closed
9
+ * 7. mock 解译器:夹具回放确定性;未登记夹具=结构化拒绝
10
+ * 8. SESSION 通道策略:永不重试、不熔断(风控红线),verdict 原样透传
11
+ * 9. 真实 handler 离线冒烟:hbcli 不可达 → 静态包降级(永不抛错,证据链非空)
12
+ * 11. HBCLI×timeout(2026-09-02 迪拜 session:冷启动建后端 session 超 30s):
13
+ * timeout 类失败重试 1 次即恢复;ENOENT/退码类仍永不重试(切换不是重试)
14
+ *
15
+ * 运行: cd ts && npx tsx scripts/effect-tests.ts
16
+ */
1
17
  import assert from 'node:assert/strict';
2
18
  import { mkdtemp, rm } from 'node:fs/promises';
3
19
  import { tmpdir } from 'node:os';
4
20
  import { join } from 'node:path';
5
21
  import { backoffDelayMs, CircuitBreaker, withRetry } from '../capabilities/resilience.js';
6
- import { declinedObservation, makeMockInterpreter, makeProductionInterpreter, __resetEffectBreakersForTest } from '../capabilities/effect.js';
7
- const sleep0 = async ()=>{};
22
+ import { declinedObservation, makeMockInterpreter, makeProductionInterpreter, __resetEffectBreakersForTest, } from '../capabilities/effect.js';
23
+ const sleep0 = async () => { }; // 回退即时放行(确定性,不真睡)
8
24
  let clock = 1_000_000;
9
- const now = ()=>clock;
10
- const interp = makeProductionInterpreter({
11
- sleep: sleep0,
12
- now
13
- });
14
- const badItp = await interp({
15
- effect: 'CTRIP_DIRECT_QUERY',
16
- params: {}
17
- });
25
+ const now = () => clock;
26
+ // ---------------------------------------------------------------------------
27
+ // 1. 注册表封闭性:未登记效应 → 结构化拒绝(不抛错)
28
+ // ---------------------------------------------------------------------------
29
+ const interp = makeProductionInterpreter({ sleep: sleep0, now });
30
+ const badItp = await interp({ effect: 'CTRIP_DIRECT_QUERY', params: {} });
18
31
  assert.equal(badItp.result, null, '未登记效应 result=null');
19
32
  assert.equal(badItp.trace.declined, 'unknown-effect');
20
33
  assert.equal(badItp.trace.attempts, 0, '未登记效应零执行');
@@ -23,57 +36,25 @@ assert.equal(badObs.ok, false);
23
36
  assert.match(badObs.summary, /未登记效应/, '拒绝面人话 summary');
24
37
  assert.match(badObs.evidence, /\[效应:CTRIP_DIRECT_QUERY@/, '拒绝面带证据链');
25
38
  console.log('1. 注册表封闭性:未登记效应 → 结构化拒绝面(不抛错)OK');
26
- assert.equal(backoffDelayMs({
27
- baseDelayMs: 500,
28
- maxDelayMs: 8000
29
- }, 1), 500);
30
- assert.equal(backoffDelayMs({
31
- baseDelayMs: 500,
32
- maxDelayMs: 8000
33
- }, 2), 1000);
34
- assert.equal(backoffDelayMs({
35
- baseDelayMs: 500,
36
- maxDelayMs: 8000
37
- }, 4), 4000);
38
- assert.equal(backoffDelayMs({
39
- baseDelayMs: 500,
40
- maxDelayMs: 2000
41
- }, 6), 2000, '封顶');
39
+ // ---------------------------------------------------------------------------
40
+ // 2. 指数退避:500→1000→2000 封顶;累计记账;非瞬时失败不重试
41
+ // ---------------------------------------------------------------------------
42
+ assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 8000 }, 1), 500);
43
+ assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 8000 }, 2), 1000);
44
+ assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 8000 }, 4), 4000);
45
+ assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 2000 }, 6), 2000, '封顶');
42
46
  const retried = [];
43
- const rt = await withRetry(async ()=>({
44
- ok: false
45
- }), {
46
- maxAttempts: 4,
47
- baseDelayMs: 500,
48
- maxDelayMs: 2000,
49
- isRetryable: ()=>true,
50
- sleep: sleep0
51
- }, (_failN, delayMs)=>retried.push(delayMs));
52
- assert.deepEqual(retried, [
53
- 500,
54
- 1000,
55
- 2000
56
- ], '退避序列 500/1000/2000');
47
+ const rt = await withRetry(async () => ({ ok: false }), { maxAttempts: 4, baseDelayMs: 500, maxDelayMs: 2000, isRetryable: () => true, sleep: sleep0 }, (_failN, delayMs) => retried.push(delayMs));
48
+ assert.deepEqual(retried, [500, 1000, 2000], '退避序列 500/1000/2000');
57
49
  assert.equal(rt.attempts, 4, '重试到上限');
58
50
  assert.equal(rt.backoffMs, 3500, '累计回退记账');
59
- const noRetry = await withRetry(async ()=>({
60
- ok: false,
61
- verdict: 'error',
62
- error: '{"message":"SentinelBlockException"}'
63
- }), {
64
- maxAttempts: 3,
65
- baseDelayMs: 500,
66
- maxDelayMs: 2000,
67
- isRetryable: ()=>false,
68
- sleep: sleep0
69
- });
51
+ const noRetry = await withRetry(async () => ({ ok: false, verdict: 'error', error: '{"message":"SentinelBlockException"}' }), { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 2000, isRetryable: () => false, sleep: sleep0 });
70
52
  assert.equal(noRetry.attempts, 1, '瞬时判定说不重试就 1 次');
71
53
  console.log('2. 指数退避(封顶链/累计记账/非瞬时止步)OK');
72
- const br = new CircuitBreaker({
73
- failureThreshold: 2,
74
- openMs: 60_000,
75
- now
76
- });
54
+ // ---------------------------------------------------------------------------
55
+ // 3. 断路器三态(时钟注入确定性)
56
+ // ---------------------------------------------------------------------------
57
+ const br = new CircuitBreaker({ failureThreshold: 2, openMs: 60_000, now });
77
58
  assert.equal(br.canAttempt().state, 'closed');
78
59
  br.onFailure();
79
60
  br.onFailure();
@@ -94,49 +75,35 @@ assert.equal(br.canAttempt().allowed, true);
94
75
  br.onFailure();
95
76
  assert.equal(br.state(), 'open', '探测失败 → 重新 open(冷却重启)');
96
77
  console.log('3. 断路器三态(closed→open→half-open 单探测→双向收敛)OK');
78
+ // ---------------------------------------------------------------------------
79
+ // 4. 生产解译器×瞬时失败:重试后成功,trace 记账(WEATHER 免费源策略 2 次上限)
80
+ // ---------------------------------------------------------------------------
97
81
  let flaky = 0;
98
82
  const ip = makeProductionInterpreter({
99
83
  sleep: sleep0,
100
84
  breakers: new Map(),
101
85
  handlers: {
102
- WEATHER_FORECAST: async ()=>{
86
+ WEATHER_FORECAST: async () => {
103
87
  flaky += 1;
104
- return flaky < 2 ? {
105
- ok: false,
106
- via: 'open-meteo-error',
107
- evidence: '[实时API:open-meteo@error]',
108
- latencyMs: 1,
109
- error: 'socket hang up'
110
- } : {
111
- ok: true,
112
- via: 'open-meteo',
113
- evidence: '[实时API:open-meteo@ts]',
114
- latencyMs: 2,
115
- daily: []
116
- };
117
- }
118
- }
119
- });
120
- const itp4 = await ip({
121
- effect: 'WEATHER_FORECAST',
122
- params: {
123
- latitude: 25.6,
124
- longitude: 100.2
125
- }
88
+ return flaky < 2
89
+ ? { ok: false, via: 'open-meteo-error', evidence: '[实时API:open-meteo@error]', latencyMs: 1, error: 'socket hang up' }
90
+ : { ok: true, via: 'open-meteo', evidence: '[实时API:open-meteo@ts]', latencyMs: 2, daily: [] };
91
+ },
92
+ },
126
93
  });
94
+ const itp4 = await ip({ effect: 'WEATHER_FORECAST', params: { latitude: 25.6, longitude: 100.2 } });
127
95
  assert.equal(itp4.trace.attempts, 2, '瞬时失败重试第 2 次成功(免费源上限 2)');
128
96
  assert.equal(itp4.trace.backoffMs, 400, '回退按 API 策略记账');
129
97
  assert.ok(itp4.result.ok, '成功 observation 原样透传');
130
98
  assert.match(itp4.trace.evidence[0] ?? '', /attempts=2 backoff=400ms breaker=closed/, 'trace 证据行');
131
99
  console.log('4. 生产解译器×瞬时失败:重试后成功 + trace 记账 OK');
100
+ // ---------------------------------------------------------------------------
101
+ // 5. FlyAI×Sentinel:不重试,但连续失败计入熔断
102
+ // ---------------------------------------------------------------------------
132
103
  const sentinelResult = {
133
- ok: false,
134
- via: 'flyai-error',
135
- verdict: 'error',
136
- kind: 'flight',
137
- latencyMs: 5,
104
+ ok: false, via: 'flyai-error', verdict: 'error', kind: 'flight', latencyMs: 5,
138
105
  evidence: '[实时API:flyai@error@ts] parse failed(SentinelBlockException)',
139
- error: 'failed to parse flyai output as JSON (incomplete FlyAI JSON object): {"message":"SentinelBlockException"}'
106
+ error: 'failed to parse flyai output as JSON (incomplete FlyAI JSON object): {"message":"SentinelBlockException"}',
140
107
  };
141
108
  let sentinelCalls = 0;
142
109
  const flyaiBreakers = new Map();
@@ -144,364 +111,169 @@ const ipS = makeProductionInterpreter({
144
111
  sleep: sleep0,
145
112
  breakers: flyaiBreakers,
146
113
  now,
147
- handlers: {
148
- FLYAI_SEARCH: async ()=>{
149
- sentinelCalls += 1;
150
- return structuredClone(sentinelResult);
151
- }
152
- }
153
- });
154
- const q = {
155
- kind: 'flight',
156
- origin: '上海',
157
- destination: '丽江',
158
- depDate: '2026-10-01'
159
- };
160
- await ipS({
161
- effect: 'FLYAI_SEARCH',
162
- params: q
163
- });
164
- await ipS({
165
- effect: 'FLYAI_SEARCH',
166
- params: q
114
+ handlers: { FLYAI_SEARCH: async () => { sentinelCalls += 1; return structuredClone(sentinelResult); } },
167
115
  });
116
+ const q = { kind: 'flight', origin: '上海', destination: '丽江', depDate: '2026-10-01' };
117
+ await ipS({ effect: 'FLYAI_SEARCH', params: q });
118
+ await ipS({ effect: 'FLYAI_SEARCH', params: q });
168
119
  assert.equal(sentinelCalls, 2, '每次调用只打一次上游(Sentinel 不重试)');
169
- const itp3 = await ipS({
170
- effect: 'FLYAI_SEARCH',
171
- params: q
172
- });
120
+ const itp3 = await ipS({ effect: 'FLYAI_SEARCH', params: q });
173
121
  assert.equal(itp3.trace.attempts, 1);
174
122
  assert.equal(itp3.trace.breaker, 'open', '连续 3 次失败(FlyAI 阈值 3)→ 断路器开启');
175
123
  assert.equal(sentinelCalls, 3);
176
- const itp4th = await ipS({
177
- effect: 'FLYAI_SEARCH',
178
- params: q
179
- });
124
+ const itp4th = await ipS({ effect: 'FLYAI_SEARCH', params: q });
180
125
  assert.equal(itp4th.trace.declined, 'circuit-open', '第 4 次:熔断中零执行成本拒绝');
181
126
  assert.equal(itp4th.result, null);
182
127
  assert.equal(sentinelCalls, 3, '熔断后不再打上游');
183
128
  console.log('5. FlyAI×Sentinel:永不重试 + 连环失败触发熔断保护配额 OK');
129
+ // ---------------------------------------------------------------------------
130
+ // 6. 断路拒绝面 + 冷却后 half-open 单探测成功恢复 closed
131
+ // ---------------------------------------------------------------------------
184
132
  const declined = declinedObservation('FLYAI_SEARCH', itp4th.trace);
185
133
  assert.equal(declined.ok, false);
186
134
  assert.match(declined.summary, /断路器开启/, '拒绝面指引「不要立即重试」,不伪装成 miss');
187
- clock += 120_000;
135
+ clock += 120_000; // 冷却满(FlyAI 60s)→ half-open
188
136
  const ipOk = makeProductionInterpreter({
189
- sleep: sleep0,
190
- breakers: flyaiBreakers,
191
- now,
192
- handlers: {
193
- FLYAI_SEARCH: async ()=>({
194
- ok: true,
195
- via: 'flyai',
196
- verdict: 'hit',
197
- kind: 'flight',
198
- latencyMs: 1,
199
- evidence: 'e',
200
- options: []
201
- })
202
- }
203
- });
204
- const itp6 = await ipOk({
205
- effect: 'FLYAI_SEARCH',
206
- params: {
207
- kind: 'flight'
208
- }
137
+ sleep: sleep0, breakers: flyaiBreakers, now, // 复用同一断路器 Map——状态在 Map 里存活
138
+ handlers: { FLYAI_SEARCH: async () => ({ ok: true, via: 'flyai', verdict: 'hit', kind: 'flight', latencyMs: 1, evidence: 'e', options: [] }) },
209
139
  });
140
+ const itp6 = await ipOk({ effect: 'FLYAI_SEARCH', params: { kind: 'flight' } });
210
141
  assert.equal(itp6.trace.breaker, 'closed', 'half-open 单探测成功 → closed 恢复');
211
142
  assert.ok(itp6.result != null && !itp6.trace.declined);
212
143
  console.log('6. 断路拒绝面(结构化/零执行)+ 冷却后单探测恢复 closed OK');
144
+ // ---------------------------------------------------------------------------
145
+ // 7. mock 解译器:夹具回放确定性(CI 无网)
146
+ // ---------------------------------------------------------------------------
213
147
  const mock = makeMockInterpreter({
214
- FLYAI_SEARCH: {
215
- ok: true,
216
- via: 'flyai',
217
- verdict: 'hit',
218
- kind: 'flight',
219
- latencyMs: 0,
220
- evidence: '[实时API:flyai@2026-08-29T00:00:00Z] 2/2 flight options',
221
- options: [
222
- {
223
- no: '9C6617',
224
- name: '吉祥航空',
225
- depDateTime: '2026-10-01 07:55',
226
- arrDateTime: '2026-10-01 11:20',
227
- depStation: '浦东T2',
228
- arrStation: '丽江三义',
229
- durationMin: 205,
230
- price: 580
231
- }
232
- ]
233
- }
148
+ FLYAI_SEARCH: { ok: true, via: 'flyai', verdict: 'hit', kind: 'flight', latencyMs: 0, evidence: '[实时API:flyai@2026-08-29T00:00:00Z] 2/2 flight options', options: [{ no: '9C6617', name: '吉祥航空', depDateTime: '2026-10-01 07:55', arrDateTime: '2026-10-01 11:20', depStation: '浦东T2', arrStation: '丽江三义', durationMin: 205, price: 580 }] },
234
149
  });
235
- const mockFx = {
236
- effect: 'FLYAI_SEARCH',
237
- params: {
238
- kind: 'flight',
239
- origin: '上海',
240
- destination: '丽江',
241
- depDate: '2026-10-01'
242
- }
243
- };
150
+ const mockFx = { effect: 'FLYAI_SEARCH', params: { kind: 'flight', origin: '上海', destination: '丽江', depDate: '2026-10-01' } };
244
151
  const mockA = await mock(mockFx);
245
152
  const mockB = await mock(mockFx);
246
153
  assert.deepEqual(mockA.result, mockB.result, '同一夹具两次回放同值');
247
154
  assert.equal(mockA.result.verdict, 'hit');
248
155
  assert.match(mockA.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
249
- const mockMiss = await mock({
250
- effect: 'WEATHER_FORECAST',
251
- params: {}
252
- });
156
+ const mockMiss = await mock({ effect: 'WEATHER_FORECAST', params: {} });
253
157
  assert.equal(mockMiss.trace.declined, 'unknown-effect', 'mock 未登记夹具 = 拒绝面');
254
158
  console.log('7. mock 解译器(夹具回放/未登记拒绝/确定性)OK');
159
+ // ---------------------------------------------------------------------------
160
+ // 8. SESSION 通道策略:永不重试、不熔断(风控红线),一次调用一次透传
161
+ // ---------------------------------------------------------------------------
255
162
  let sessionCalls = 0;
256
163
  const ipSess = makeProductionInterpreter({
257
164
  sleep: sleep0,
258
165
  breakers: new Map(),
259
166
  handlers: {
260
- SESSION_FLIGHT_SEARCH: async ()=>{
167
+ SESSION_FLIGHT_SEARCH: async () => {
261
168
  sessionCalls += 1;
262
- return {
263
- ok: false,
264
- via: 'session-ctrip-flight-error',
265
- verdict: 'challenged',
266
- evidence: '[会话:ctrip-flight@error@ts] 风控命中',
267
- latencyMs: 9,
268
- error: 'challenge'
269
- };
270
- }
271
- }
272
- });
273
- const sessQ = {
274
- from: '上海',
275
- to: '丽江',
276
- date: '2026-10-01'
277
- };
278
- const itp8 = await ipSess({
279
- effect: 'SESSION_FLIGHT_SEARCH',
280
- params: sessQ
169
+ return { ok: false, via: 'session-ctrip-flight-error', verdict: 'challenged', evidence: '[会话:ctrip-flight@error@ts] 风控命中', latencyMs: 9, error: 'challenge' };
170
+ },
171
+ },
281
172
  });
173
+ const sessQ = { from: '上海', to: '丽江', date: '2026-10-01' };
174
+ const itp8 = await ipSess({ effect: 'SESSION_FLIGHT_SEARCH', params: sessQ });
282
175
  assert.equal(itp8.result.verdict, 'challenged', '渠道 verdict 原样透传');
283
176
  assert.equal(itp8.trace.attempts, 1, 'SESSION 永不重试(挑战=红线)');
284
177
  assert.equal(itp8.trace.breaker, 'off', 'SESSION 不参与断路器');
285
- for(let i = 0; i < 5; i++)await ipSess({
286
- effect: 'SESSION_FLIGHT_SEARCH',
287
- params: sessQ
288
- });
289
- const itp8z = await ipSess({
290
- effect: 'SESSION_FLIGHT_SEARCH',
291
- params: sessQ
292
- });
178
+ for (let i = 0; i < 5; i++)
179
+ await ipSess({ effect: 'SESSION_FLIGHT_SEARCH', params: sessQ });
180
+ const itp8z = await ipSess({ effect: 'SESSION_FLIGHT_SEARCH', params: sessQ });
293
181
  assert.ok(itp8z.trace.breaker === 'off' && !itp8z.trace.declined, 'SESSION 无断路拒绝(治理在节律闸与授权闸)');
294
182
  assert.equal(sessionCalls, 7);
295
183
  console.log('8. SESSION 通道策略(永不重试/不熔断/verdict 透传)OK');
184
+ // ---------------------------------------------------------------------------
185
+ // 9. 真实 handler 离线冒烟:hbcli 显式不可达 → 静态包降级(永不抛错,零网络)
186
+ // ---------------------------------------------------------------------------
296
187
  __resetEffectBreakersForTest();
297
188
  const tmp = await mkdtemp(join(tmpdir(), 'effect-test-'));
298
189
  const itp9 = await interp({
299
190
  effect: 'HBCLI_HOTEL_SEARCH',
300
- params: {
301
- destination: '大理',
302
- hbcliBin: join(tmp, 'no-such-hbcli'),
303
- fallbackPath: join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json')
304
- }
191
+ // 显式不存在的自定义 bin:hbcliBinCandidates 对自定义名不回退已知安装位 → ENOENT → 静态包降级
192
+ params: { destination: '大理', hbcliBin: join(tmp, 'no-such-hbcli'), fallbackPath: join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json') },
305
193
  });
306
194
  const hb9 = itp9.result;
307
195
  assert.ok(hb9 != null, '真实 handler 降级仍返回 observation');
308
196
  assert.ok(typeof hb9.summary === 'string' && hb9.summary.length > 0, '降级带人话 summary');
309
197
  assert.ok(itp9.trace.evidence[0]?.startsWith('[效应:HBCLI_HOTEL_SEARCH@'), '解译层证据行');
310
- await rm(tmp, {
311
- recursive: true,
312
- force: true
313
- });
198
+ await rm(tmp, { recursive: true, force: true });
314
199
  console.log(`9. 真实 handler 离线冒烟(HBCLI 静态包降级,summary=${(hb9.summary ?? '').slice(0, 40)}…)OK`);
200
+ // ---------------------------------------------------------------------------
201
+ // 10. M0 预订链读效应:HBCLI_HOTEL_RATES / HBCLI_CHECK_AVAIL
202
+ // (注入 handler 验策略面;真实 handler 离线冒烟验价格面 fail-closed;mock 夹具)
203
+ // ---------------------------------------------------------------------------
204
+ // 10a. 注册进生产解译器:注入 handler 派发 / 永不重试 / 失败计熔断 / 三连错开闸
315
205
  let ratesDispatches = 0;
316
206
  const ipM0 = makeProductionInterpreter({
317
207
  sleep: sleep0,
318
208
  breakers: new Map(),
319
209
  handlers: {
320
- HBCLI_HOTEL_RATES: async ()=>{
210
+ HBCLI_HOTEL_RATES: async () => {
321
211
  ratesDispatches += 1;
322
- return {
323
- via: 'hbcli-realtime',
324
- exitCode: 0,
325
- result: {
326
- rooms: []
327
- },
328
- evidence: '[实时API:hbcli@ts]',
329
- latencyMs: 1,
330
- rates: {
331
- rooms: []
332
- },
333
- summary: 'ok'
334
- };
212
+ return { via: 'hbcli-realtime', exitCode: 0, result: { rooms: [] }, evidence: '[实时API:hbcli@ts]', latencyMs: 1, rates: { rooms: [] }, summary: 'ok' };
335
213
  },
336
- HBCLI_CHECK_AVAIL: async ()=>({
337
- via: 'hbcli-error',
338
- exitCode: 1,
339
- result: null,
340
- evidence: '[实时API:hbcli@error@ts]',
341
- latencyMs: 1,
342
- error: 'boom',
343
- avail: null,
344
- summary: 'err'
345
- })
346
- }
347
- });
348
- const itp10 = await ipM0({
349
- effect: 'HBCLI_HOTEL_RATES',
350
- params: {
351
- hotelId: '900000001',
352
- checkIn: '2026-09-05',
353
- checkOut: '2026-09-07',
354
- adults: 2
355
- }
214
+ HBCLI_CHECK_AVAIL: async () => ({ via: 'hbcli-error', exitCode: 1, result: null, evidence: '[实时API:hbcli@error@ts]', latencyMs: 1, error: 'boom', avail: null, summary: 'err' }),
215
+ },
356
216
  });
217
+ const itp10 = await ipM0({ effect: 'HBCLI_HOTEL_RATES', params: { hotelId: '900000001', checkIn: '2026-09-05', checkOut: '2026-09-07', adults: 2 } });
357
218
  assert.equal(itp10.trace.attempts, 1, 'RATES 永不重试(候选路径切换不是重试)');
358
219
  assert.ok(itp10.result != null && !itp10.trace.declined, 'RATES 命中注入 handler,observation 透传');
359
220
  assert.ok(itp10.trace.evidence[0]?.startsWith('[效应:HBCLI_HOTEL_RATES@'), '解译层证据行');
360
- const itp10b = await ipM0({
361
- effect: 'HBCLI_CHECK_AVAIL',
362
- params: {
363
- ratePkgId: 'rp-1'
364
- }
365
- });
221
+ const itp10b = await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-1' } });
366
222
  assert.equal(itp10b.result?.via, 'hbcli-error', '渠道失败观察原样透传(平铺失败面,isFailure 计熔断)');
367
- await ipM0({
368
- effect: 'HBCLI_CHECK_AVAIL',
369
- params: {
370
- ratePkgId: 'rp-2'
371
- }
372
- });
373
- await ipM0({
374
- effect: 'HBCLI_CHECK_AVAIL',
375
- params: {
376
- ratePkgId: 'rp-3'
377
- }
378
- });
379
- const itp10c = await ipM0({
380
- effect: 'HBCLI_CHECK_AVAIL',
381
- params: {
382
- ratePkgId: 'rp-4'
383
- }
384
- });
223
+ await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-2' } });
224
+ await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-3' } });
225
+ const itp10c = await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-4' } });
385
226
  assert.equal(itp10c.trace.declined, 'circuit-open', '3 连错熔断保护(同 HBCLI 族策略)');
386
227
  assert.equal(ratesDispatches, 1, '成功通道不受失败通道熔断影响(per-effect 隔离)');
387
228
  console.log('10a. 预订链读效应注册(派发/永不重试/熔断族/per-effect 隔离)OK');
229
+ // 10b. 真实 handler 离线冒烟:不可达 bin → 诚实失败,价格面无静态降级(fail-closed)
388
230
  __resetEffectBreakersForTest();
389
231
  const tmp10 = await mkdtemp(join(tmpdir(), 'effect-m0-'));
390
232
  const itp10d = await interp({
391
233
  effect: 'HBCLI_HOTEL_RATES',
392
- params: {
393
- hotelId: '900000001',
394
- hbcliBin: join(tmp10, 'no-such-hbcli')
395
- }
234
+ // 自定义不存在的 bin:候选路径不回退已知安装位 → ENOENT → 诚实失败(hotel-list 才有静态包降级)
235
+ params: { hotelId: '900000001', hbcliBin: join(tmp10, 'no-such-hbcli') },
396
236
  });
397
237
  const hr10 = itp10d.result;
398
238
  assert.ok(hr10 != null, '不可达仍返回 observation(永不抛错)');
399
239
  assert.equal(hr10.via, 'hbcli-error', '未伪装成功');
400
240
  assert.match(hr10.summary ?? '', /不可用/, 'summary 明示不可用');
401
241
  assert.ok(!/静态包/.test(hr10.summary ?? '') || /无静态降级/.test(hr10.summary ?? ''), '价格面不降级到静态包');
402
- const itp10e = await interp({
403
- effect: 'HBCLI_CHECK_AVAIL',
404
- params: {
405
- ratePkgId: 'rp-x',
406
- hbcliBin: join(tmp10, 'no-such-hbcli')
407
- }
408
- });
242
+ const itp10e = await interp({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-x', hbcliBin: join(tmp10, 'no-such-hbcli') } });
409
243
  assert.equal(itp10e.result?.via, 'hbcli-error', 'check-avail 同口径诚实失败');
410
- await rm(tmp10, {
411
- recursive: true,
412
- force: true
413
- });
244
+ await rm(tmp10, { recursive: true, force: true });
414
245
  console.log('10b. 真实 handler 离线冒烟(价格面 fail-closed,无静态降级)OK');
246
+ // 10c. mock 解译器夹具回放(预订链 CI 形态;录制形态 = 渠道 observation 原样)
415
247
  const mock10 = makeMockInterpreter({
416
- HBCLI_HOTEL_RATES: {
417
- via: 'hbcli-realtime',
418
- exitCode: 0,
419
- result: {
420
- rooms: [
421
- {
422
- ratePkgId: 'rp-fx-1',
423
- roomTypeName: 'Deluxe King'
424
- }
425
- ]
426
- },
427
- evidence: '[实时API:hbcli@2026-08-30T00:00:00Z]',
428
- latencyMs: 1800,
429
- rates: {
430
- rooms: [
431
- {
432
- ratePkgId: 'rp-fx-1',
433
- roomTypeName: 'Deluxe King'
434
- }
435
- ]
436
- },
437
- summary: 'hotel 900000001:hbcli 实时报价(2026-09-05 → 2026-09-07)'
438
- },
439
- HBCLI_CHECK_AVAIL: {
440
- via: 'hbcli-realtime',
441
- exitCode: 0,
442
- result: {
443
- ratePkgId: 'rp-fx-1',
444
- avail: true
445
- },
446
- evidence: '[实时API:hbcli@2026-08-30T00:00:01Z]',
447
- latencyMs: 900,
448
- avail: {
449
- ratePkgId: 'rp-fx-1',
450
- avail: true
451
- },
452
- summary: 'ratePkg rp-fx-1:hbcli 实时验价(库存与价格以后端为准)'
453
- }
454
- });
455
- const m10a = await mock10({
456
- effect: 'HBCLI_HOTEL_RATES',
457
- params: {
458
- hotelId: '900000001'
459
- }
460
- });
461
- const m10b = await mock10({
462
- effect: 'HBCLI_CHECK_AVAIL',
463
- params: {
464
- ratePkgId: 'rp-fx-1'
465
- }
248
+ HBCLI_HOTEL_RATES: { via: 'hbcli-realtime', exitCode: 0, result: { rooms: [{ ratePkgId: 'rp-fx-1', roomTypeName: 'Deluxe King' }] }, evidence: '[实时API:hbcli@2026-08-30T00:00:00Z]', latencyMs: 1800, rates: { rooms: [{ ratePkgId: 'rp-fx-1', roomTypeName: 'Deluxe King' }] }, summary: 'hotel 900000001:hbcli 实时报价(2026-09-05 → 2026-09-07)' },
249
+ HBCLI_CHECK_AVAIL: { via: 'hbcli-realtime', exitCode: 0, result: { ratePkgId: 'rp-fx-1', avail: true }, evidence: '[实时API:hbcli@2026-08-30T00:00:01Z]', latencyMs: 900, avail: { ratePkgId: 'rp-fx-1', avail: true }, summary: 'ratePkg rp-fx-1:hbcli 实时验价(库存与价格以后端为准)' },
466
250
  });
251
+ const m10a = await mock10({ effect: 'HBCLI_HOTEL_RATES', params: { hotelId: '900000001' } });
252
+ const m10b = await mock10({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-fx-1' } });
467
253
  assert.equal(m10a.result.rates?.rooms?.length, 1, 'RATES 夹具回放');
468
254
  assert.equal(m10b.result.avail?.avail, true, 'CHECK_AVAIL 夹具回放');
469
255
  assert.match(m10a.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
470
256
  console.log('10c. mock 解译器夹具回放(预订链 CI 形态)OK');
257
+ // ---------------------------------------------------------------------------
258
+ // 11. HBCLI×timeout 冷启动重试(2026-09-02 迪拜 session 复盘:hotel-list 首调
259
+ // 30s 超时,LLM 手动立即重试即成功——上游建后端 session 可超时;策略改为
260
+ // 「仅 timeout 类重试 1 次」,ENOENT/退码类维持「切换不是重试」)
261
+ // ---------------------------------------------------------------------------
471
262
  __resetEffectBreakersForTest();
472
263
  let flakyCalls = 0;
473
264
  const ipTimeout = makeProductionInterpreter({
474
265
  sleep: sleep0,
475
266
  breakers: new Map(),
476
267
  handlers: {
477
- HBCLI_HOTEL_SEARCH: async ()=>{
268
+ HBCLI_HOTEL_SEARCH: async () => {
478
269
  flakyCalls += 1;
479
- if (flakyCalls === 1) return {
480
- via: 'hbcli-error',
481
- exitCode: -1,
482
- result: null,
483
- evidence: '[实时API:hbcli@timeout@ts]',
484
- latencyMs: 1,
485
- error: 'timeout after 30000ms'
486
- };
487
- return {
488
- via: 'hbcli-realtime',
489
- exitCode: 0,
490
- result: {
491
- hotels: []
492
- },
493
- evidence: '[实时API:hbcli@ts]',
494
- latencyMs: 1
495
- };
496
- }
497
- }
498
- });
499
- const itp11 = await ipTimeout({
500
- effect: 'HBCLI_HOTEL_SEARCH',
501
- params: {
502
- destination: '迪拜'
503
- }
270
+ if (flakyCalls === 1)
271
+ return { via: 'hbcli-error', exitCode: -1, result: null, evidence: '[实时API:hbcli@timeout@ts]', latencyMs: 1, error: 'timeout after 30000ms' };
272
+ return { via: 'hbcli-realtime', exitCode: 0, result: { hotels: [] }, evidence: '[实时API:hbcli@ts]', latencyMs: 1 };
273
+ },
274
+ },
504
275
  });
276
+ const itp11 = await ipTimeout({ effect: 'HBCLI_HOTEL_SEARCH', params: { destination: '迪拜' } });
505
277
  assert.equal(itp11.trace.attempts, 2, `timeout 重试一次(attempts=2),实际 ${itp11.trace.attempts}`);
506
278
  assert.equal(itp11.result?.via, 'hbcli-realtime', '重试后恢复实时');
507
279
  assert.equal(itp11.trace.breaker, 'closed', '恢复即成功,断路不计数');
@@ -510,238 +282,79 @@ const ipEnoent = makeProductionInterpreter({
510
282
  sleep: sleep0,
511
283
  breakers: new Map(),
512
284
  handlers: {
513
- HBCLI_HOTEL_SEARCH: async ()=>{
285
+ HBCLI_HOTEL_SEARCH: async () => {
514
286
  hardCalls += 1;
515
- return {
516
- via: 'hbcli-error',
517
- exitCode: -1,
518
- result: null,
519
- evidence: '[实时API:hbcli@spawn_error@ts]',
520
- latencyMs: 1,
521
- error: 'spawn hbcli ENOENT'
522
- };
523
- }
524
- }
525
- });
526
- const itp11b = await ipEnoent({
527
- effect: 'HBCLI_HOTEL_SEARCH',
528
- params: {
529
- destination: '迪拜'
530
- }
287
+ return { via: 'hbcli-error', exitCode: -1, result: null, evidence: '[实时API:hbcli@spawn_error@ts]', latencyMs: 1, error: 'spawn hbcli ENOENT' };
288
+ },
289
+ },
531
290
  });
291
+ const itp11b = await ipEnoent({ effect: 'HBCLI_HOTEL_SEARCH', params: { destination: '迪拜' } });
532
292
  assert.equal(itp11b.trace.attempts, 1, 'ENOENT 类永不重试(切换不是重试契约不变)');
533
293
  console.log('11. HBCLI×timeout 冷启动重试(timeout→attempts=2 恢复实时;ENOENT→attempts=1)OK');
294
+ // ---------------------------------------------------------------------------
295
+ // 12. D-23 收尾(issue #115):六渠道入注册表——策略表行存在 + 横切语义正确
296
+ // (anything/web/github/video/agent_reach/session_login;没有策略表行就没有效应)
297
+ // ---------------------------------------------------------------------------
534
298
  __resetEffectBreakersForTest();
299
+ // 12a. 六效应注册在案:handler 注入 → 原样透传,trace.channel 与策略表一致
535
300
  const sixChannels = [
536
- [
537
- 'ANYTHING_SEARCH',
538
- 'cli',
539
- {
540
- ok: true,
541
- via: 'hbcli-anything',
542
- verdict: 'hit',
543
- evidence: 'e',
544
- latencyMs: 1
545
- }
546
- ],
547
- [
548
- 'WEB_READ',
549
- 'api',
550
- {
551
- ok: true,
552
- via: 'r.jina.ai',
553
- evidence: 'e',
554
- latencyMs: 1
555
- }
556
- ],
557
- [
558
- 'GITHUB_SEARCH',
559
- 'cli',
560
- {
561
- ok: true,
562
- verdict: 'found',
563
- evidence: 'e',
564
- latencyMs: 1
565
- }
566
- ],
567
- [
568
- 'VIDEO_SUBTITLE',
569
- 'cli',
570
- {
571
- ok: true,
572
- verdict: 'found',
573
- evidence: 'e',
574
- latencyMs: 1
575
- }
576
- ],
577
- [
578
- 'AGENT_REACH',
579
- 'cli',
580
- {
581
- ok: true,
582
- verdict: 'found',
583
- channel: 'v2ex',
584
- method: 'read',
585
- evidence: 'e',
586
- latencyMs: 1
587
- }
588
- ],
589
- [
590
- 'SESSION_LOGIN',
591
- 'browser',
592
- {
593
- ok: true,
594
- via: 'session-login',
595
- verdict: 'pending',
596
- evidence: 'e',
597
- latencyMs: 1
598
- }
599
- ]
301
+ ['ANYTHING_SEARCH', 'cli', { ok: true, via: 'hbcli-anything', verdict: 'hit', evidence: 'e', latencyMs: 1 }],
302
+ ['WEB_READ', 'api', { ok: true, via: 'r.jina.ai', evidence: 'e', latencyMs: 1 }],
303
+ ['GITHUB_SEARCH', 'cli', { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }],
304
+ ['VIDEO_SUBTITLE', 'cli', { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }],
305
+ ['AGENT_REACH', 'cli', { ok: true, verdict: 'found', channel: 'v2ex', method: 'read', evidence: 'e', latencyMs: 1 }],
306
+ ['SESSION_LOGIN', 'browser', { ok: true, via: 'session-login', verdict: 'pending', evidence: 'e', latencyMs: 1 }],
600
307
  ];
601
- for (const [fx, channel, obs] of sixChannels){
602
- const ip = makeProductionInterpreter({
603
- sleep: sleep0,
604
- breakers: new Map(),
605
- handlers: {
606
- [fx]: async ()=>obs
607
- }
608
- });
609
- const out = await ip({
610
- effect: fx,
611
- params: {}
612
- });
308
+ for (const [fx, channel, obs] of sixChannels) {
309
+ const ip = makeProductionInterpreter({ sleep: sleep0, breakers: new Map(), handlers: { [fx]: async () => obs } });
310
+ const out = await ip({ effect: fx, params: {} });
613
311
  assert.equal(out.result, obs, `${fx} 透传原样`);
614
312
  assert.equal(out.trace.channel, channel, `${fx} trace.channel=${channel}(策略表行在案)`);
615
313
  assert.equal(out.trace.attempts, 1, `${fx} 成功单次执行`);
616
314
  }
617
315
  console.log('12a. 六渠道入表(透传 + trace.channel 与策略表一致)OK');
618
- for (const fx of [
619
- 'AGENT_REACH',
620
- 'SESSION_LOGIN'
621
- ]){
622
- const ip = makeProductionInterpreter({
623
- sleep: sleep0,
624
- breakers: new Map(),
625
- handlers: {
626
- [fx]: async ()=>{
627
- throw new Error('upstream says no');
628
- }
629
- }
630
- });
631
- const out = await ip({
632
- effect: fx,
633
- params: {}
634
- });
316
+ // 12b. 永不重试族:AGENT_REACH / SESSION_LOGIN handler 恒抛错 → attempts=1(透传面不放大上游消耗)
317
+ for (const fx of ['AGENT_REACH', 'SESSION_LOGIN']) {
318
+ const ip = makeProductionInterpreter({ sleep: sleep0, breakers: new Map(), handlers: { [fx]: async () => { throw new Error('upstream says no'); } } });
319
+ const out = await ip({ effect: fx, params: {} });
635
320
  assert.equal(out.result, null, `${fx} 抛错 → result=null`);
636
321
  assert.equal(out.trace.attempts, 1, `${fx} 永不重试(attempts=1)`);
637
322
  }
638
323
  console.log('12b. AGENT_REACH/SESSION_LOGIN 永不重试(反射桥透传/浏览器风控红线)OK');
324
+ // 12c. WEB_READ 瞬时抖动重试 1 次(throw → 成功,attempts=2);GITHUB timeout 重试 / not-installed 不重试
639
325
  __resetEffectBreakersForTest();
640
326
  let webCalls = 0;
641
327
  const ipWeb = makeProductionInterpreter({
642
- sleep: sleep0,
643
- breakers: new Map(),
644
- handlers: {
645
- WEB_READ: async ()=>{
646
- webCalls += 1;
647
- if (webCalls === 1) throw new Error('socket hang up');
648
- return {
649
- ok: true,
650
- via: 'r.jina.ai',
651
- evidence: 'e',
652
- latencyMs: 1
653
- };
654
- }
655
- }
656
- });
657
- const outWeb = await ipWeb({
658
- effect: 'WEB_READ',
659
- params: {
660
- url: 'https://example.com'
661
- }
328
+ sleep: sleep0, breakers: new Map(),
329
+ handlers: { WEB_READ: async () => { webCalls += 1; if (webCalls === 1)
330
+ throw new Error('socket hang up'); return { ok: true, via: 'r.jina.ai', evidence: 'e', latencyMs: 1 }; } },
662
331
  });
332
+ const outWeb = await ipWeb({ effect: 'WEB_READ', params: { url: 'https://example.com' } });
663
333
  assert.equal(outWeb.trace.attempts, 2, `WEB_READ 瞬时抖动重试 1 次,实际 ${outWeb.trace.attempts}`);
664
334
  let ghTimeoutCalls = 0;
665
335
  const ipGhTimeout = makeProductionInterpreter({
666
- sleep: sleep0,
667
- breakers: new Map(),
668
- handlers: {
669
- GITHUB_SEARCH: async ()=>{
670
- ghTimeoutCalls += 1;
671
- if (ghTimeoutCalls === 1) return {
672
- ok: false,
673
- verdict: 'timeout',
674
- evidence: 'e',
675
- latencyMs: 1,
676
- stdout: '',
677
- stderr: ''
678
- };
679
- return {
680
- ok: true,
681
- verdict: 'found',
682
- evidence: 'e',
683
- latencyMs: 1
684
- };
685
- }
686
- }
687
- });
688
- const outGhT = await ipGhTimeout({
689
- effect: 'GITHUB_SEARCH',
690
- params: {
691
- query: 'x'
692
- }
336
+ sleep: sleep0, breakers: new Map(),
337
+ handlers: { GITHUB_SEARCH: async () => { ghTimeoutCalls += 1; if (ghTimeoutCalls === 1)
338
+ return { ok: false, verdict: 'timeout', evidence: 'e', latencyMs: 1, stdout: '', stderr: '' }; return { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }; } },
693
339
  });
340
+ const outGhT = await ipGhTimeout({ effect: 'GITHUB_SEARCH', params: { query: 'x' } });
694
341
  assert.equal(outGhT.trace.attempts, 2, 'GITHUB timeout 类重试 1 次(冷启动同 HBCLI 实况)');
695
342
  let ghNiCalls = 0;
696
343
  const ipGhNi = makeProductionInterpreter({
697
- sleep: sleep0,
698
- breakers: new Map(),
699
- handlers: {
700
- GITHUB_SEARCH: async ()=>{
701
- ghNiCalls += 1;
702
- return {
703
- ok: false,
704
- verdict: 'not-installed',
705
- evidence: 'e',
706
- latencyMs: 0
707
- };
708
- }
709
- }
710
- });
711
- const outGhNi = await ipGhNi({
712
- effect: 'GITHUB_SEARCH',
713
- params: {
714
- query: 'x'
715
- }
344
+ sleep: sleep0, breakers: new Map(),
345
+ handlers: { GITHUB_SEARCH: async () => { ghNiCalls += 1; return { ok: false, verdict: 'not-installed', evidence: 'e', latencyMs: 0 }; } },
716
346
  });
347
+ const outGhNi = await ipGhNi({ effect: 'GITHUB_SEARCH', params: { query: 'x' } });
717
348
  assert.equal(outGhNi.trace.attempts, 1, 'not-installed 是配置态(needs-setup 族)不重试');
718
349
  console.log('12c. WEB_READ/GITHUB/VIDEO 策略行(timeout 重试/配置态不重试)OK');
350
+ // 12d. mock 面:新效应夹具回放 + 未登记夹具结构化拒绝(与既有效应同语义)
719
351
  __resetEffectBreakersForTest();
720
- const mockIp = makeMockInterpreter({
721
- ANYTHING_SEARCH: {
722
- ok: true,
723
- via: 'hbcli-anything',
724
- verdict: 'hit',
725
- evidence: '[实时API:hbcli-anything@ts]',
726
- latencyMs: 1
727
- }
728
- });
729
- const outMock = await mockIp({
730
- effect: 'ANYTHING_SEARCH',
731
- params: {
732
- keyword: '大理'
733
- }
734
- });
352
+ const mockIp = makeMockInterpreter({ ANYTHING_SEARCH: { ok: true, via: 'hbcli-anything', verdict: 'hit', evidence: '[实时API:hbcli-anything@ts]', latencyMs: 1 } });
353
+ const outMock = await mockIp({ effect: 'ANYTHING_SEARCH', params: { keyword: '大理' } });
735
354
  assert.equal(outMock.result.verdict, 'hit', 'mock 夹具回放原样');
736
355
  assert.equal(outMock.trace.channel, 'mock', 'mock 通道标注');
737
- const outMockBad = await mockIp({
738
- effect: 'SESSION_LOGIN',
739
- params: {}
740
- });
356
+ const outMockBad = await mockIp({ effect: 'SESSION_LOGIN', params: {} });
741
357
  assert.equal(outMockBad.result, null, '未登记夹具 → result=null');
742
358
  assert.equal(outMockBad.trace.declined, 'unknown-effect', '未登记夹具 → 结构化拒绝');
743
359
  console.log('12d. mock 夹具回放/未登记拒绝(新效应同语义)OK');
744
360
  console.log('EFFECT INTERPRETER TESTS: 12/12 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应/HBCLI timeout 重试/D-23 六渠道入表,纯离线)');
745
-
746
-
747
- //# sourceURL=ts/scripts/effect-tests.ts