@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,99 +1,362 @@
1
+ /**
2
+ * flyai 能力层测试(离线,临时假 CLI 脚本经 cliBin 注入,零网络):
3
+ * 1. Sentinel 限流形状 {"message":"SentinelBlockException..."}——合法 JSON 但无 data.itemList,
4
+ * 曾被 `data?.itemList ?? []` 吞成 0/0 静默 miss(issue #24)→ 应判 error 且保留 sentinel 字样
5
+ * 2. 业务空形状 {"data":{"itemList":[]}} → verdict=miss(0/0,evidence 标注)
6
+ * 3. 非空 transport itemList 只要有 malformed sibling → registered effect error,不落负事实
7
+ * (flight/train 各一条有效+畸形 fixture;无 partial-completeness hit)
8
+ * 4. 非空 hotel itemList 有 malformed sibling → registered effect error,不落酒店事实
9
+ * 5. transport/hotel 字段类型与空白校验不接受 truthy 数字/对象,并覆盖 official Alibaba shape
10
+ * 6. 完整合法 flight/train/hotel → verdict=hit;有效空列表仍 miss
11
+ * 7. exit≠0 → verdict=error
12
+ * 8. 试用额度达限(exit 1 + MCP HTTP 429 "Trial limit reached",2026-09-02 迪拜
13
+ * session 实况)→ verdict=needs-setup + setup 指引(不再当通用 error 盲重试)
14
+ *
15
+ * 运行: cd ts && npx tsx scripts/flyai-tests.ts
16
+ */
1
17
  import assert from 'node:assert/strict';
2
18
  import { writeFile, mkdtemp, rm } from 'node:fs/promises';
3
19
  import { tmpdir } from 'node:os';
4
20
  import { join } from 'node:path';
5
21
  import { flyaiSearch } from '../capabilities/flyai.js';
22
+ import { makeProductionInterpreter } from '../capabilities/effect.js';
23
+ import { loadFactRegistry } from '../capabilities/fact-log.js';
24
+ import { apply } from '../src/index.js';
25
+ import { factsFromFlyai, factsFromHotel } from '../src/bookable-facts.js';
6
26
  const tmp = await mkdtemp(join(tmpdir(), 'flyai-test-'));
7
27
  async function fakeCli(name, code, payload) {
8
28
  const p = join(tmp, name);
9
- await writeFile(p, `#!/bin/sh\necho '${payload}'\nexit ${code}\n`, {
10
- mode: 0o755
11
- });
29
+ await writeFile(p, `#!/bin/sh\necho '${payload}'\nexit ${code}\n`, { mode: 0o755 });
12
30
  return p;
13
31
  }
14
- const base = {
15
- kind: 'flight',
16
- origin: '上海',
17
- destination: '丽江',
18
- depDate: '2026-10-01'
32
+ const base = { kind: 'flight', origin: '上海', destination: '丽江', depDate: '2026-10-01' };
33
+ const trainBase = { kind: 'train', origin: '上海', destination: '大理', depDate: '2026-10-01' };
34
+ const hotelBase = { kind: 'hotel', destName: '大理', checkInDate: '2026-10-01', checkOutDate: '2026-10-03' };
35
+ async function registeredSearch(q) {
36
+ const registeredFlyai = makeProductionInterpreter({ breakers: new Map(), sleep: async () => { } });
37
+ const observation = await registeredFlyai({ effect: 'FLYAI_SEARCH', params: q });
38
+ assert.ok(observation.result, 'registered FlyAI effect 应返回结构化 observation');
39
+ return observation.result;
40
+ }
41
+ const registeredStateRoot = await mkdtemp(join(tmpdir(), 'flyai-registered-e2e-'));
42
+ let registeredCliBin = '';
43
+ const registeredTools = [];
44
+ const registeredBreakers = new Map();
45
+ const registeredContext = {
46
+ tools: { register: (tool) => registeredTools.push(tool) },
47
+ systemPrompt: { variable: () => { } },
48
+ on: () => () => { },
49
+ get: () => undefined,
19
50
  };
20
- const sentinelBin = await fakeCli('flyai-sentinel', 0, '{"message":"SentinelBlockException: flow control"}');
21
- const s = await flyaiSearch({
22
- ...base,
23
- cliBin: sentinelBin,
24
- timeoutMs: 5000
51
+ const isolatedConfig = {
52
+ stateRoot: registeredStateRoot,
53
+ timeoutMs: 5000,
54
+ hbcliBin: 'hbcli-not-on-path',
55
+ sessionAccess: 'off',
56
+ };
57
+ const registeredProductionEffect = makeProductionInterpreter({
58
+ breakers: registeredBreakers,
59
+ sleep: async () => { },
60
+ handlers: {
61
+ FLYAI_SEARCH: async (params) => flyaiSearch({ ...params, cliBin: registeredCliBin, timeoutMs: 5000 }),
62
+ },
25
63
  });
64
+ apply(registeredContext, isolatedConfig, { effect: registeredProductionEffect });
65
+ const registeredFlyaiTool = registeredTools.find(tool => tool.name === 'gotry_flyai_search');
66
+ assert.ok(registeredFlyaiTool, 'apply 应注册 gotry_flyai_search');
67
+ async function registeredToolSearch(name, args, itemList) {
68
+ registeredCliBin = await fakeCli(`registered-${name}`, 0, JSON.stringify({ data: { itemList } }));
69
+ return await registeredFlyaiTool.execute(args, null);
70
+ }
71
+ async function assertRegisteredFactLogEmpty(label) {
72
+ const facts = await loadFactRegistry(registeredStateRoot);
73
+ assert.equal(facts.length, 0, `${label} 不得落 positive/negative fact`);
74
+ registeredBreakers.clear();
75
+ }
76
+ // 1. 真正 registered tool E2E:apply → gotry_flyai_search → production effect/parser → fact sidecar
77
+ const registeredMalformedCases = [
78
+ { name: 'all-malformed-flight', args: { kind: 'flight', from: '北京', to: '上海', date: '2027-04-01' }, itemList: [{}] },
79
+ { name: 'all-malformed-train', args: { kind: 'train', from: '北京', to: '上海', date: '2027-04-02' }, itemList: [null] },
80
+ { name: 'all-malformed-hotel', args: { kind: 'hotel', to: '大理', checkIn: '2027-04-03', checkOut: '2027-04-05' }, itemList: [{ name: 42 }] },
81
+ ];
82
+ for (const testCase of registeredMalformedCases) {
83
+ const result = await registeredToolSearch(testCase.name, testCase.args, testCase.itemList);
84
+ assert.equal(result.ok, false, `${testCase.name} 应返回 structured error(ok=false)`);
85
+ assert.equal(result.verdict, 'error', `${testCase.name} 应经 registered tool 返回 error`);
86
+ assert.match(String(result.evidence ?? ''), /flyai@error|malformed/i, `${testCase.name} 应保留结构化 error evidence`);
87
+ await assertRegisteredFactLogEmpty(testCase.name);
88
+ }
89
+ const registeredMixedCases = [
90
+ {
91
+ name: 'mixed-flight',
92
+ args: { kind: 'flight', from: '北京', to: '上海', date: '2027-04-06' },
93
+ itemList: [{ adultPrice: '¥400.0', journeys: [{ segments: [{ marketingTransportNo: 'CA1883', marketingTransportName: '国航', depDateTime: '2027-04-06 21:00:00', arrDateTime: '2027-04-06 23:20:00', depStationName: '首都国际机场', arrStationName: '浦东国际机场', duration: '140分钟' }] }] }, {}],
94
+ },
95
+ {
96
+ name: 'mixed-train',
97
+ args: { kind: 'train', from: '北京', to: '上海', date: '2027-04-07' },
98
+ itemList: [{ adultPrice: '¥553.0', journeys: [{ segments: [{ marketingTransportNo: 'G11', depDateTime: '2027-04-07 08:00:00', arrDateTime: '2027-04-07 12:28:00', depStationName: '北京南', arrStationName: '上海虹桥', duration: '268分钟' }] }] }, null],
99
+ },
100
+ {
101
+ name: 'mixed-hotel',
102
+ args: { kind: 'hotel', to: '大理', checkIn: '2027-04-08', checkOut: '2027-04-10' },
103
+ itemList: [{ name: '大理A 酒店', star: '高档型', price: '¥7xx', shId: 'hotel-a', detailUrl: 'https://example.test/hotel-a' }, { name: null }],
104
+ },
105
+ ];
106
+ for (const testCase of registeredMixedCases) {
107
+ const result = await registeredToolSearch(testCase.name, testCase.args, testCase.itemList);
108
+ assert.equal(result.ok, false, `${testCase.name} 应返回 structured error(ok=false)`);
109
+ assert.equal(result.verdict, 'error', `${testCase.name} 应经 registered tool 返回整体 error`);
110
+ assert.match(String(result.evidence ?? ''), /flyai@error|malformed/i, `${testCase.name} 应保留结构化 error evidence`);
111
+ await assertRegisteredFactLogEmpty(testCase.name);
112
+ }
113
+ const registeredMiss = await registeredToolSearch('exact-empty-flight', { kind: 'flight', from: '深圳', to: '普吉', date: '2027-04-11' }, []);
114
+ assert.equal(registeredMiss.verdict, 'miss', `registered exact empty list 应返回 miss: ${JSON.stringify(registeredMiss)}`);
115
+ const factsAfterMiss = await loadFactRegistry(registeredStateRoot);
116
+ assert.equal(factsAfterMiss.length, 1, 'registered miss 只应产生一条 exact-date negative fact');
117
+ assert.equal(factsAfterMiss[0]?.kind, 'flight');
118
+ assert.equal(factsAfterMiss[0]?.bookability, 'unavailable_exact_date');
119
+ const registeredHit = await registeredToolSearch('official-train-hit', { kind: 'train', from: '北京', to: '上海', date: '2027-04-12' }, [{ adultPrice: '¥553.0', journeys: [{ journeyType: '直达', segments: [{ depStationName: '北京南', depDateTime: '2027-04-12 08:00:00', arrStationName: '上海虹桥', arrDateTime: '2027-04-12 12:28:00', duration: '268分钟', marketingTransportNo: 'G11', seatClassName: '二等座' }] }], jumpUrl: 'https://example.test/train' }]);
120
+ assert.equal(registeredHit.verdict, 'hit', 'registered official train batch 应返回 hit');
121
+ const factsAfterHit = await loadFactRegistry(registeredStateRoot);
122
+ const typedPositive = factsAfterHit.find((f) => f.kind === 'train' && f.query_id.endsWith(':2027-04-12'));
123
+ assert.ok(typedPositive, 'registered hit 应产生 typed positive fact');
124
+ assert.equal(typedPositive?.bookability, 'bookable_exact_date');
125
+ assert.equal(typedPositive?.flight_no, 'G11');
126
+ await rm(registeredStateRoot, { recursive: true, force: true });
127
+ console.log('1. registered E2E → mixed/all-malformed zero facts / empty miss negative / official train hit positiveOK');
128
+ // 1. Sentinel 限流:合法 JSON 的非业务形状 → error(不是静默 miss)
129
+ const sentinelBin = await fakeCli('flyai-sentinel', 0, '{"message":"SentinelBlockException: flow control"}');
130
+ const s = await flyaiSearch({ ...base, cliBin: sentinelBin, timeoutMs: 5000 });
26
131
  assert.equal(s.ok, false, 'Sentinel 形状应 ok=false');
27
132
  assert.equal(s.verdict, 'error', `Sentinel 形状应判 error,实际 ${s.verdict}`);
28
133
  assert.match(s.error ?? '', /sentinel/i, `error 应保留 sentinel 字样(供上层限流识别),实际 ${s.error}`);
29
134
  assert.match(s.evidence, /\[实时API:flyai@error@/, 'error 证据链标注');
30
135
  console.log('1. Sentinel 非业务形状 → error(非静默 miss)OK');
136
+ // 2. 业务空形状 → miss(上游正常,确无航班)
31
137
  const missBin = await fakeCli('flyai-miss', 0, '{"data":{"itemList":[]}}');
32
- const m = await flyaiSearch({
33
- ...base,
34
- cliBin: missBin,
35
- timeoutMs: 5000
36
- });
138
+ const m = await flyaiSearch({ ...base, cliBin: missBin, timeoutMs: 5000 });
37
139
  assert.equal(m.ok, true, '业务空形状 ok=true');
38
140
  assert.equal(m.verdict, 'miss', `空 itemList 应判 miss,实际 ${m.verdict}`);
39
141
  assert.match(m.evidence, /0\/0 flight options/, 'miss 证据链 0/0');
40
142
  console.log('2. 业务空形状 → miss(0/0)OK');
143
+ // 3. 非空但全 malformed transport item → registered effect error,不落负事实
144
+ const malformedBin = await fakeCli('flyai-malformed', 0, '{"data":{"itemList":[{}]}}');
145
+ const malformed = await registeredSearch({ ...base, cliBin: malformedBin, timeoutMs: 5000 });
146
+ assert.equal(malformed.ok, false, '全 malformed transport item 不应报告成功');
147
+ assert.equal(malformed.verdict, 'error', '非空全 malformed transport item 应判 error');
148
+ assert.match(malformed.error ?? '', /malformed|valid typed transport/i, 'error 应保留 transport shape 原因');
149
+ assert.match(malformed.evidence, /flyai@error@.*transport itemList/i, 'evidence 应保留结构化 transport 错误');
150
+ assert.deepEqual(factsFromFlyai({ kind: base.kind, origin: base.origin, destination: base.destination, date: base.depDate }, malformed, new Date().toISOString()), [], 'registered path 的 malformed error 不得生成负库存事实');
151
+ console.log('3. 非空全 malformed transport item → registered error,不落负事实OK');
152
+ // 4. flight 有效+malformed sibling → 整体 error,不落负事实
41
153
  const hitPayload = JSON.stringify({
42
154
  data: {
43
- itemList: [
44
- {
45
- journeys: [
46
- {
47
- segments: [
48
- {
49
- marketingTransportNo: '9C6617',
50
- marketingTransportName: '吉祥航空',
51
- depDateTime: '2026-10-01 07:55',
52
- arrDateTime: '2026-10-01 11:20',
53
- depStationName: '浦东T2',
54
- arrStationName: '丽江三义',
55
- duration: 205
56
- }
57
- ]
58
- }
59
- ],
155
+ itemList: [{
156
+ journeys: [{
157
+ segments: [{
158
+ marketingTransportNo: '9C6617', marketingTransportName: '吉祥航空',
159
+ depDateTime: '2026-10-01 07:55', arrDateTime: '2026-10-01 11:20',
160
+ depStationName: '浦东T2', arrStationName: '丽江三义', duration: 205,
161
+ }],
162
+ }],
60
163
  ticketPrice: '580',
61
- jumpUrl: 'https://www.fliggy.com/demo'
62
- }
63
- ]
64
- }
164
+ jumpUrl: 'https://www.fliggy.com/demo',
165
+ }],
166
+ },
65
167
  });
168
+ const mixedBin = await fakeCli('flyai-mixed', 0, JSON.stringify({
169
+ data: {
170
+ itemList: [{}, {
171
+ journeys: [{ segments: [{
172
+ marketingTransportNo: 'G201', marketingTransportName: '高铁',
173
+ depDateTime: '2026-10-01 09:00', arrDateTime: '2026-10-01 12:00',
174
+ depStationName: '上海虹桥', arrStationName: '丽江站', duration: 180,
175
+ }] }],
176
+ ticketPrice: '480',
177
+ }],
178
+ },
179
+ }));
180
+ const flightMixed = await registeredSearch({ ...base, cliBin: mixedBin, timeoutMs: 5000 });
181
+ assert.equal(flightMixed.ok, false, 'flight 混合响应不应报告成功');
182
+ assert.equal(flightMixed.verdict, 'error', 'flight 有效+malformed sibling 应整体 error');
183
+ assert.equal(flightMixed.options, undefined, 'flight malformed error 不应暴露 options');
184
+ assert.match(flightMixed.evidence, /transport itemList malformed.*1\/2/i, 'flight error 应暴露 malformed/总条目比例');
185
+ assert.deepEqual(factsFromFlyai({ kind: base.kind, origin: base.origin, destination: base.destination, date: base.depDate }, flightMixed, new Date().toISOString()), [], 'flight mixed error 不得生成负库存事实');
186
+ console.log('4. flight 有效+malformed sibling → 整体 error,不落负事实OK');
187
+ // 5. train 有效+malformed sibling → 整体 error,不落负事实
188
+ const trainMixedBin = await fakeCli('flyai-train-mixed', 0, JSON.stringify({
189
+ data: {
190
+ itemList: [{
191
+ journeys: [{ segments: [{
192
+ marketingTransportNo: 'G201', marketingTransportName: '高铁',
193
+ depDateTime: '2026-10-01 09:00', arrDateTime: '2026-10-01 12:00',
194
+ depStationName: '上海虹桥', arrStationName: '大理站', duration: '180',
195
+ }] }],
196
+ ticketPrice: '480',
197
+ }, null],
198
+ },
199
+ }));
200
+ const trainMixed = await registeredSearch({ ...trainBase, cliBin: trainMixedBin, timeoutMs: 5000 });
201
+ assert.equal(trainMixed.ok, false, 'train 混合响应不应报告成功');
202
+ assert.equal(trainMixed.verdict, 'error', 'train 有效+malformed sibling 应整体 error');
203
+ assert.equal(trainMixed.options, undefined, 'train malformed error 不应暴露 options');
204
+ assert.match(trainMixed.evidence, /transport itemList malformed.*1\/2/i, 'train error 应暴露 malformed/总条目比例');
205
+ assert.deepEqual(factsFromFlyai({ kind: trainBase.kind, origin: trainBase.origin, destination: trainBase.destination, date: trainBase.depDate }, trainMixed, new Date().toISOString()), [], 'train mixed error 不得生成负库存事实');
206
+ console.log('5. train 有效+malformed sibling → 整体 error,不落负事实OK');
207
+ // 6. hotel 有效+malformed sibling → 整体 error,不落酒店事实
208
+ const hotelMixedBin = await fakeCli('flyai-hotel-mixed', 0, JSON.stringify({
209
+ data: {
210
+ itemList: [{ name: '大理A 酒店', star: '高档型', price: '¥7xx', shId: 'hotel-a' }, { name: null }],
211
+ },
212
+ }));
213
+ const hotelMixed = await registeredSearch({ ...hotelBase, cliBin: hotelMixedBin, timeoutMs: 5000 });
214
+ assert.equal(hotelMixed.ok, false, 'hotel 混合响应不应报告成功');
215
+ assert.equal(hotelMixed.verdict, 'error', 'hotel 有效+malformed sibling 应整体 error');
216
+ assert.equal(hotelMixed.hotels, undefined, 'hotel malformed error 不应暴露 hotels');
217
+ assert.match(hotelMixed.evidence, /hotel itemList malformed.*1\/2/i, 'hotel error 应暴露 malformed/总条目比例');
218
+ assert.deepEqual(factsFromHotel({ source: 'flyai-hotel', destination: hotelBase.destName, checkIn: hotelBase.checkInDate, checkOut: hotelBase.checkOutDate, verdict: hotelMixed.verdict, options: 0, evidence: hotelMixed.evidence, fetchedAt: new Date().toISOString() }), [], 'hotel mixed error 不得生成酒店事实');
219
+ console.log('6. hotel 有效+malformed sibling → 整体 error,不落酒店事实OK');
220
+ // 7. typed transport/hotel 字段拒绝 truthy 数字与空白
221
+ const typedTransportMalformedBin = await fakeCli('flyai-typed-transport-malformed', 0, JSON.stringify({
222
+ data: { itemList: [{
223
+ journeys: [{ segments: [{
224
+ marketingTransportNo: 12345, marketingTransportName: '吉祥航空',
225
+ depDateTime: ' ', arrDateTime: '2026-10-01 11:20',
226
+ depStationName: '浦东T2', arrStationName: '丽江三义', duration: 205,
227
+ }] }],
228
+ ticketPrice: '580',
229
+ }] },
230
+ }));
231
+ const typedTransportMalformed = await registeredSearch({ ...base, cliBin: typedTransportMalformedBin, timeoutMs: 5000 });
232
+ assert.equal(typedTransportMalformed.verdict, 'error', '数字 transportNo/空白 depDateTime 应判 error');
233
+ assert.equal(typedTransportMalformed.options, undefined, '字段类型 malformed 不应暴露 options');
234
+ const typedHotelMalformedBin = await fakeCli('flyai-typed-hotel-malformed', 0, JSON.stringify({ data: { itemList: [{ name: { value: '酒店' }, price: '¥7xx' }] } }));
235
+ const typedHotelMalformed = await registeredSearch({ ...hotelBase, cliBin: typedHotelMalformedBin, timeoutMs: 5000 });
236
+ assert.equal(typedHotelMalformed.verdict, 'error', '对象 hotel name 应判 error');
237
+ assert.equal(typedHotelMalformed.hotels, undefined, '字段类型 malformed 不应暴露 hotels');
238
+ console.log('7. typed transport/hotel 字段拒绝 truthy 数字、对象与空白OK');
239
+ // 8. official Alibaba shape → adultPrice、带“分钟”时长与 train 缺名称兼容
240
+ const officialFlightBin = await fakeCli('flyai-official-flight', 0, JSON.stringify({
241
+ data: { itemList: [{
242
+ adultPrice: '¥400.0',
243
+ journeys: [{ journeyType: '直达', segments: [{
244
+ depStationName: '首都国际机场', depDateTime: '2026-03-28 21:00:00',
245
+ arrStationName: '浦东国际机场', arrDateTime: '2026-03-28 23:20:00',
246
+ duration: '140分钟', marketingTransportName: '国航', marketingTransportNo: 'CA1883', seatClassName: '经济舱',
247
+ }] }],
248
+ jumpUrl: 'https://example.test/flight',
249
+ }] },
250
+ }));
251
+ const officialFlight = await registeredSearch({ ...base, cliBin: officialFlightBin, timeoutMs: 5000 });
252
+ assert.equal(officialFlight.verdict, 'hit', 'official flight shape 应保持 hit');
253
+ assert.equal(officialFlight.options?.[0]?.price, 400, 'official adultPrice 应解析为 400');
254
+ assert.equal(officialFlight.options?.[0]?.durationMin, 140, 'official 中文分钟时长应解析为 140');
255
+ const officialTrainBin = await fakeCli('flyai-official-train', 0, JSON.stringify({
256
+ data: { itemList: [{
257
+ adultPrice: '¥553.0',
258
+ journeys: [{ journeyType: '直达', segments: [{
259
+ depStationName: '北京南', depDateTime: '2026-03-15 08:00:00',
260
+ arrStationName: '上海虹桥', arrDateTime: '2026-03-15 12:28:00',
261
+ duration: '268分钟', marketingTransportNo: 'G11', seatClassName: '二等座',
262
+ }] }],
263
+ jumpUrl: 'https://example.test/train',
264
+ }] },
265
+ }));
266
+ const officialTrain = await registeredSearch({ ...trainBase, cliBin: officialTrainBin, timeoutMs: 5000 });
267
+ assert.equal(officialTrain.verdict, 'hit', 'official train shape 应保持 hit');
268
+ assert.equal(officialTrain.options?.[0]?.price, 553, 'official train adultPrice 应解析为 553');
269
+ assert.equal(officialTrain.options?.[0]?.durationMin, 268, 'official train 中文分钟时长应解析为 268');
270
+ assert.equal(officialTrain.options?.[0]?.name, '', '缺 marketingTransportName 不猜名称且保留空字符串');
271
+ console.log('8. official flight/train shape → adultPrice/分钟时长/缺名称兼容OK');
272
+ // 9. official typed fields:invalid price/duration/name type → structured error
273
+ const invalidPriceBin = await fakeCli('flyai-invalid-price', 0, JSON.stringify({
274
+ data: { itemList: [{
275
+ adultPrice: 'not-a-price',
276
+ journeys: [{ segments: [{
277
+ marketingTransportNo: 'CA1883', marketingTransportName: '国航',
278
+ depDateTime: '2026-03-28 21:00:00', arrDateTime: '2026-03-28 23:20:00',
279
+ depStationName: '首都国际机场', arrStationName: '浦东国际机场', duration: '140分钟',
280
+ }] }],
281
+ }] },
282
+ }));
283
+ const invalidPrice = await registeredSearch({ ...base, cliBin: invalidPriceBin, timeoutMs: 5000 });
284
+ assert.equal(invalidPrice.verdict, 'error', '非法文本价格应判 error');
285
+ const invalidDurationBin = await fakeCli('flyai-invalid-duration', 0, JSON.stringify({
286
+ data: { itemList: [{
287
+ adultPrice: '¥400.0',
288
+ journeys: [{ segments: [{
289
+ marketingTransportNo: 'CA1883', marketingTransportName: '国航',
290
+ depDateTime: '2026-03-28 21:00:00', arrDateTime: '2026-03-28 23:20:00',
291
+ depStationName: '首都国际机场', arrStationName: '浦东国际机场', duration: { value: 140 },
292
+ }] }],
293
+ }] },
294
+ }));
295
+ const invalidDuration = await registeredSearch({ ...base, cliBin: invalidDurationBin, timeoutMs: 5000 });
296
+ assert.equal(invalidDuration.verdict, 'error', '对象时长应判 error');
297
+ const invalidNameBin = await fakeCli('flyai-invalid-name', 0, JSON.stringify({
298
+ data: { itemList: [{
299
+ adultPrice: '¥553.0',
300
+ journeys: [{ segments: [{
301
+ depStationName: '北京南', depDateTime: '2026-03-15 08:00:00',
302
+ arrStationName: '上海虹桥', arrDateTime: '2026-03-15 12:28:00',
303
+ duration: '268分钟', marketingTransportNo: 'G11', marketingTransportName: 123,
304
+ }] }],
305
+ }] },
306
+ }));
307
+ const invalidName = await registeredSearch({ ...trainBase, cliBin: invalidNameBin, timeoutMs: 5000 });
308
+ assert.equal(invalidName.verdict, 'error', '数字 marketingTransportName 应判 error');
309
+ console.log('9. official typed 字段非法 price/duration/name → errorOK');
310
+ // 10. 完整合法 flight → hit,字段解析
66
311
  const hitBin = await fakeCli('flyai-hit', 0, hitPayload);
67
- const h = await flyaiSearch({
68
- ...base,
69
- cliBin: hitBin,
70
- timeoutMs: 5000
71
- });
72
- assert.equal(h.verdict, 'hit', `业务条目应判 hit,实际 ${h.verdict}`);
312
+ const h = await flyaiSearch({ ...base, cliBin: hitBin, timeoutMs: 5000 });
313
+ assert.equal(h.ok, true, '完整合法 flight ok=true');
314
+ assert.equal(h.verdict, 'hit', `完整合法 flight 应判 hit,实际 ${h.verdict}`);
73
315
  assert.equal(h.options?.length, 1, '1 个选项');
74
316
  assert.equal(h.options[0].no, '9C6617');
75
317
  assert.equal(h.options[0].price, 580, '价格数值解析');
76
318
  assert.equal(h.options[0].depStation, '浦东T2');
77
319
  assert.match(h.evidence, /1\/1 flight options/, 'hit 证据链 1/1');
78
- console.log('3. 业务命中 → hit(9C6617 ¥580)OK');
320
+ console.log('10. 完整合法 flight → hit(9C6617 ¥580)OK');
321
+ // 11. 完整合法 train → hit,共享 transport typed 合同
322
+ const trainHitBin = await fakeCli('flyai-train-hit', 0, JSON.stringify({
323
+ data: {
324
+ itemList: [{
325
+ journeys: [{ segments: [{
326
+ marketingTransportNo: 'G201', marketingTransportName: '高铁',
327
+ depDateTime: '2026-10-01 09:00', arrDateTime: '2026-10-01 12:00',
328
+ depStationName: '上海虹桥', arrStationName: '大理站', duration: '180', seatClassName: '二等座',
329
+ }] }],
330
+ ticketPrice: '480',
331
+ }],
332
+ },
333
+ }));
334
+ const trainHit = await flyaiSearch({ ...trainBase, cliBin: trainHitBin, timeoutMs: 5000 });
335
+ assert.equal(trainHit.ok, true, '完整合法 train ok=true');
336
+ assert.equal(trainHit.verdict, 'hit', '完整合法 train 应判 hit');
337
+ assert.equal(trainHit.options?.length, 1, '完整合法 train 产出 1 个选项');
338
+ assert.equal(trainHit.options?.[0]?.no, 'G201');
339
+ console.log('11. 完整合法 train → hit(G201)OK');
340
+ // 12. 完整合法 hotel → hit,保持酒店语义
341
+ const hotelHitBin = await fakeCli('flyai-hotel-hit', 0, JSON.stringify({ data: { itemList: [{ name: '大理A 酒店', star: '高档型', price: '¥7xx', rate: null, address: 'addr', interestsPoi: '近洱海', shId: 'hotel-a', detailUrl: 'https://example.test/hotel-a' }] } }));
342
+ const hotelHit = await flyaiSearch({ ...hotelBase, cliBin: hotelHitBin, timeoutMs: 5000 });
343
+ assert.equal(hotelHit.ok, true, '完整合法 hotel ok=true');
344
+ assert.equal(hotelHit.verdict, 'hit', '完整合法 hotel 应判 hit');
345
+ assert.equal(hotelHit.hotels?.length, 1, '1 个酒店选项');
346
+ assert.equal(hotelHit.hotels?.[0]?.name, '大理A 酒店');
347
+ assert.equal(hotelHit.hotels?.[0]?.priceRaw, '¥7xx', '酒店打码价原值保留');
348
+ console.log('12. 完整合法 hotel → hit(打码价保真)OK');
349
+ // 13. exit≠0 → error
79
350
  const failBin = await fakeCli('flyai-fail', 1, '');
80
- const f = await flyaiSearch({
81
- ...base,
82
- cliBin: failBin,
83
- timeoutMs: 5000
84
- });
351
+ const f = await flyaiSearch({ ...base, cliBin: failBin, timeoutMs: 5000 });
85
352
  assert.equal(f.ok, false);
86
353
  assert.equal(f.verdict, 'error', '非零退出应判 error');
87
- console.log('4. 非零退出 → error OK');
354
+ console.log('13. 非零退出 → error OK');
355
+ // 14. 试用额度达限(2026-09-02 迪拜 session 实况:exit 1 + MCP HTTP 429 Trial limit
356
+ // reached)→ needs-setup 而非通用 error——阻断 LLM 拿同一把 429 跨轮盲重试
88
357
  const trialBin = join(tmp, 'flyai-trial');
89
- await writeFile(trialBin, `#!/bin/sh\necho 'search-hotel: MCP HTTP 429: Body: {"jsonrpc":"2.0","id":"1","error":{"code":-32603,"message":"Trial limit reached. Please visit the console at flyai.open.fliggy.com to get a formal API Key"}}' >&2\nexit 1\n`, {
90
- mode: 0o755
91
- });
92
- const t = await flyaiSearch({
93
- ...base,
94
- cliBin: trialBin,
95
- timeoutMs: 5000
96
- });
358
+ await writeFile(trialBin, `#!/bin/sh\necho 'search-hotel: MCP HTTP 429: Body: {"jsonrpc":"2.0","id":"1","error":{"code":-32603,"message":"Trial limit reached. Please visit the console at flyai.open.fliggy.com to get a formal API Key"}}' >&2\nexit 1\n`, { mode: 0o755 });
359
+ const t = await flyaiSearch({ ...base, cliBin: trialBin, timeoutMs: 5000 });
97
360
  assert.equal(t.ok, false);
98
361
  assert.equal(t.verdict, 'needs-setup', `429 达限应判 needs-setup,实际 ${t.verdict}`);
99
362
  assert.match(t.setup ?? '', /FLYAI_API_KEY/, 'setup 指引带 FLYAI_API_KEY 配置路径');
@@ -101,11 +364,5 @@ assert.match(t.setup ?? '', /flyai\.open\.fliggy\.com/, 'setup 指引带控制
101
364
  assert.match(t.setup ?? '', /请勿重试|勿重试|不要重试/, 'setup 明示本会话勿重试');
102
365
  assert.match(t.error ?? '', /429|Trial limit/i, 'error 保留上游 429 原话');
103
366
  assert.match(t.evidence, /\[实时API:flyai@error@/, '证据链标注');
104
- await rm(tmp, {
105
- recursive: true,
106
- force: true
107
- });
108
- console.log('FLYAI TESTS: 5/5 OK(离线假 CLI:Sentinel→error / 空 itemList→miss / 命中→hit / exit≠0→error / 429 达限→needs-setup)');
109
-
110
-
111
- //# sourceURL=ts/scripts/flyai-tests.ts
367
+ await rm(tmp, { recursive: true, force: true });
368
+ console.log('FLYAI TESTS: transport/hotel completeness contract OK(离线假 CLI:Sentinel→error / 空 itemList→miss / flight+train+hotel mixed→整体 error 且不落事实 / typed 字段校验 / 完整 flight+train+hotel→hit / exit≠0→error / 429→needs-setup)');
@@ -0,0 +1,131 @@
1
+ /**
2
+ * G5 机械闸自测(issue #348;全离线,零网络,零共享 state 写入)。
3
+ *
4
+ * 覆盖:
5
+ * 1. (17) pattern 命中/不命中双向——真桥接形状(G5_BRIDGE/g5Bridge/
6
+ * capability-travel-mcp/t-system bridge/G5桥)必须命中;承运人代码
7
+ * G5/G52667、persona-bench 案例 G5、base64 噪声、「提到 G5 门」的
8
+ * 门定义句必须不命中(误报会把 tracker 自身打红);
9
+ * 2. (7) 台账解析——GRANT 缺 issue/决策链接 → 拒收(fail closed);
10
+ * code fence 内模板行不计入;glob 语义(** 跨段 / * 单段);覆盖判定;
11
+ * 3. (2) 命名面豁免存在(master-outline 对 + 台账/guard 对);
12
+ * 4. (4) CLI 端到端(临时 git fixture,--root 注入):未授权引用 → exit 1
13
+ * 且输出可操作指引;GRANT 覆盖后 → exit 0;干净仓 → exit 0;
14
+ * 台账缺失 → exit 2(闸自身不可用,而非放行)。
15
+ *
16
+ * 运行:cd ts && npx tsx scripts/g5-guard-tests.ts
17
+ */
18
+ import assert from 'node:assert/strict';
19
+ import { spawnSync } from 'node:child_process';
20
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
21
+ import { tmpdir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { globToRegExp, grantCovers, parseGrants, scanContent, NAMING_SURFACES } from './g5-guard.js';
24
+ let assertions = 0;
25
+ function ok(value, message) {
26
+ assert.ok(value, message);
27
+ assertions += 1;
28
+ }
29
+ // ── 1. pattern 命中/不命中双向 ──────────────────────────────────────────────
30
+ const mustHit = [
31
+ ['env key', 'G5_BRIDGE_ENDPOINT=https://internal.example'],
32
+ ['camelCase identifier', 'const g5Bridge = createBridge()'],
33
+ ['hyphen path', 'see ts/src/adapters/g5-bridge/client.ts'],
34
+ ['internal MCP plugin id', 'registerPlugin("capability-travel-mcp")'],
35
+ ['t-system bridge', 't-system bridge adapter for flights'],
36
+ ['bridge adjacent t-system', 'BridgeTSystem flights'],
37
+ ['chinese adjacency', '启用 G5 桥'],
38
+ ['chinese bridge g5', '桥 G5 工具'],
39
+ ['chinese t-system prefix', '桥接 T 系统 的机票工具'],
40
+ ['chinese t-system suffix', 'T 系统适配层'],
41
+ ];
42
+ for (const [label, text] of mustHit) {
43
+ ok(scanContent('fixture.ts', text).length === 1, `pattern 应命中: ${label} (${text})`);
44
+ }
45
+ const mustNotHit = [
46
+ ['carrier code', '{ "carrier_codes": ["CA", "CZ", "G5", "GS"] }'],
47
+ ['flight number', '"known_flights": ["G52667", "CZ5817"]'],
48
+ ['persona-bench case id', '| G5 | 万xx 指代 | = 甲米 |'],
49
+ ['gate mention without bridge adjacency', 'constrained by G5); ② reference'],
50
+ ['gate row phrase', 'G5 (internal travel bridge approval)'],
51
+ ['base64-ish noise', '"integrity": "sha512-mQ+/0Fo3LTIX+4k3m+P4y9e9IA6/BeNHrWW19U+G5x0Z8BlybHDELZIB=="'],
52
+ ['zh gate row phrase', '内部差旅工具桥(T 系统侧,脱敏)的内部审批'],
53
+ ];
54
+ for (const [label, text] of mustNotHit) {
55
+ ok(scanContent('fixture.ts', text).length === 0, `pattern 不应命中(误报): ${label} (${text})`);
56
+ }
57
+ // ── 2. 台账解析 + glob 语义 ────────────────────────────────────────────────
58
+ const empty = parseGrants('# G5 Authorization Ledger\n\n(empty)\n');
59
+ ok(empty.grants.length === 0 && empty.malformed === 0, '空台账应解析为 0 授权');
60
+ const noLink = parseGrants('- GRANT: src/** — some bridge\n');
61
+ ok(noLink.grants.length === 0 && noLink.malformed === 1, '缺 issue/决策链接的 GRANT 应被拒收(fail closed)');
62
+ const ledger = parseGrants([
63
+ '# ledger',
64
+ '- GRANT: src/adapters/** — masked flight reads, tenant X — issue: #348',
65
+ '- GRANT: docs/notes.md — demo — decision: https://example/decision',
66
+ ].join('\n'));
67
+ ok(ledger.grants.length === 2 && ledger.malformed === 0, '带 issue/决策链接的 GRANT 应成立');
68
+ ok(grantCovers(ledger.grants[0], 'src/adapters/flight/client.ts'), '`**` 应跨段覆盖');
69
+ ok(!grantCovers(ledger.grants[0], 'docs/notes.md'), '不匹配的路径不应被覆盖');
70
+ const withTemplate = parseGrants([
71
+ '# ledger',
72
+ '```',
73
+ '- GRANT: <path-glob> — system/purpose/scope one-liner — issue/decision: #<n> or URL',
74
+ '```',
75
+ '- GRANT: src/** — real entry — issue: #348',
76
+ ].join('\n'));
77
+ ok(withTemplate.grants.length === 1 && withTemplate.malformed === 0, 'code fence 内的模板行不应计入 GRANT/畸形');
78
+ const single = globToRegExp('docs/*.md');
79
+ ok(single.test('docs/a.md') && !single.test('docs/sub/b.md'), '`*` 应限单段');
80
+ // ── 3. 命名面豁免 ──────────────────────────────────────────────────────────
81
+ ok(NAMING_SURFACES.has('docs/gotry-master-outline.md') && NAMING_SURFACES.has('docs/gotry-master-outline.zh-CN.md'), '门定义对必须在命名面');
82
+ ok(NAMING_SURFACES.has('docs/g5-authorization-ledger.md') && NAMING_SURFACES.has('ts/scripts/g5-guard.ts'), '台账与 guard 自身必须在命名面');
83
+ // ── 4. CLI 端到端(临时 git fixture) ───────────────────────────────────────
84
+ const tsRoot = join(import.meta.dirname, '..');
85
+ const guardScript = join(tsRoot, 'scripts', 'g5-guard.ts');
86
+ const tsxBin = join(tsRoot, 'node_modules', '.bin', 'tsx');
87
+ function makeFixture(files) {
88
+ const root = mkdtempSync(join(tmpdir(), 'g5-guard-fixture-'));
89
+ for (const [rel, content] of Object.entries(files)) {
90
+ const abs = join(root, rel);
91
+ mkdirSync(join(abs, '..'), { recursive: true });
92
+ writeFileSync(abs, content);
93
+ }
94
+ spawnSync('git', ['init', '-q'], { cwd: root });
95
+ spawnSync('git', ['add', '-A'], { cwd: root });
96
+ return root;
97
+ }
98
+ function runGuard(root) {
99
+ const result = spawnSync(tsxBin, [guardScript, '--root', root], { encoding: 'utf8' });
100
+ return { status: result.status ?? -1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
101
+ }
102
+ const LEDGER_HEADER = [
103
+ '# G5 Authorization Ledger',
104
+ '',
105
+ '## Entries',
106
+ '',
107
+ '(empty)',
108
+ '',
109
+ ].join('\n');
110
+ // 红:未授权桥接引用 → exit 1 + 可操作指引
111
+ const redRoot = makeFixture({
112
+ 'src/adapter.ts': 'export const endpoint = process.env.G5_BRIDGE_ENDPOINT\n',
113
+ 'docs/README.md': 'docs index\n',
114
+ 'docs/g5-authorization-ledger.md': LEDGER_HEADER,
115
+ });
116
+ const red = runGuard(redRoot);
117
+ ok(red.status === 1 && red.stderr.includes('src/adapter.ts') && red.stderr.includes('issues/348'), `未授权引用应 exit 1 且给出路径与 #348 指引(status=${red.status}, stderr=${red.stderr.slice(0, 200)})`);
118
+ // 绿:创始人侧 GRANT 覆盖后 → exit 0(fixture 内的 GRANT 仅演示解析,非真实授权)
119
+ writeFileSync(join(redRoot, 'docs', 'g5-authorization-ledger.md'), LEDGER_HEADER + '- GRANT: src/** — fixture demo only, not a real grant — issue: #348\n');
120
+ const green = runGuard(redRoot);
121
+ ok(green.status === 0, `GRANT 覆盖后应 exit 0(status=${green.status}, stderr=${green.stderr.slice(0, 200)})`);
122
+ rmSync(redRoot, { recursive: true, force: true });
123
+ // 绿:干净仓(无任何引用)→ exit 0
124
+ const cleanRoot = makeFixture({ 'src/adapter.ts': 'export const endpoint = "https://api.hotelbyte.example"\n', 'docs/g5-authorization-ledger.md': LEDGER_HEADER });
125
+ ok(runGuard(cleanRoot).status === 0, '干净仓应 exit 0');
126
+ rmSync(cleanRoot, { recursive: true, force: true });
127
+ // exit 2:台账缺失 → 闸不可用,而不是放行
128
+ const noLedgerRoot = makeFixture({ 'src/adapter.ts': 'export const x = 1\n' });
129
+ ok(runGuard(noLedgerRoot).status === 2, '台账缺失应 exit 2(闸配置错误),不得放行');
130
+ rmSync(noLedgerRoot, { recursive: true, force: true });
131
+ console.log(`G5 GUARD TESTS: ${assertions}/${assertions} OK(命中/不命中双向 / 台账 fail-closed+glob / 命名面 / CLI 红绿+exit2)`);