@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
@@ -0,0 +1,262 @@
1
+ /**
2
+ * sponsor 插件与同内核端到端复用证明测试(issue #235;全离线 fixture 合成数据,零真实调用):
3
+ * 1. 插件配置 fail-closed(反例先行):缺 sponsorId/travelerPrincipalId/bffPrincipalId、
4
+ * 空路由表/重复路由、缺披露文案或缺排序偏置声明、负/非整数佣金、sponsorId 冒充 traveler
5
+ * principal(三主体混同)——全部构造即拒
6
+ * 2. 运行激活默认关闭:SPONSOR_RUNTIME_ACTIVATION_DEFAULT=false,requireRuntimeActivation(false) 抛错,
7
+ * 显式 true 才放行(本件为结构性证明,非运行时激活,零真实交易路径)
8
+ * 3. 路由授权(越权反例):路由不在 sponsor allowedRoutes 闭集 → route-unauthorized;
9
+ * 授权路由但缺 session/order 绑定 → missing-session-binding(禁止复制 Buyer/credentials 规避)
10
+ * 4. 同内核引用证明(复用面):sponsor 模块源码必须 import 自 src/booking-saga.ts 与 src/wish-pool.ts
11
+ * (同源调用,零拷贝——不重声明边表/解析器);运行时面 ts/src/** 零 sponsor-plugin 引用
12
+ * (C 端默认路径结构性隔离);内核边表/拒绝表十二格从消费侧逐字钉死(内核漂移即本套件红)
13
+ * 5. 排序红线:高佣金候选不满足旅行者条件 → 保持排除,赞助权重不可伪造用户效用;
14
+ * 同分 tie-break 只按申报顺位且必须显式披露偏置;佣金数值从不进入排序键
15
+ * 6. 端到端 B2B(fixture):配置→路由授权→库存/规划(内核评分)→确认闸
16
+ * (write.pending→write.confirmed,披露 digest 进入确认指纹,必携 receipt)→审计/回放
17
+ * (内核 sagaTraceViolations 空违例)
18
+ * 7. 端到端 B2C + 隔离:同一 runner 不带 plugin → b2c 形态;saga 边序列与 B2B 逐格相同
19
+ * (同一内核函数);leak 检查器零发现(sponsor 字段零渗入);空 receipt 两种形态同拒
20
+ * (同一 WriteGate 规则,无豁免)
21
+ * 8. A/B 同业务 id 不串:同 businessId 不同 sponsor → scope 三元组隔离,事件/指纹/approval
22
+ * 互不可见;A 的 approval 绑定在 B 的指纹上校验失败;取消退款路径(propose→confirm→compensate)
23
+ * 在 sponsor 形态同样走内核边表,回放零违例
24
+ *
25
+ * 运行: cd ts && npx tsx scripts/sponsor-reuse-tests.ts
26
+ */
27
+ import assert from 'node:assert/strict';
28
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+ import { SAGA_EDGES, SAGA_REJECTIONS, } from '../src/booking-saga.js';
31
+ import { SPONSOR_PLUGIN_SCHEMA, SPONSOR_RUNTIME_ACTIVATION_DEFAULT, authorizeSponsorRoute, buildConfirmationFingerprint, bindApproval, createSponsorPlugin, rankSponsorCandidates, requireRuntimeActivation, runBookingScenario, scoreCandidateAgainstWish, sponsorLeakFindings, verifyApprovalBinding, } from '../capabilities/sponsor-plugin.js';
32
+ const REPO_TS = new URL('..', import.meta.url).pathname;
33
+ // ── fixture:两个 sponsor 配置 + 三条合成库存候选 ─────────────────────────────
34
+ function sponsorConfigA() {
35
+ return {
36
+ sponsorId: 'sponsor-a-travel',
37
+ travelerPrincipalId: 'traveler-001',
38
+ bffPrincipalId: 'bff-agency-a',
39
+ allowedRoutes: ['route.packaged-erhai-4d', 'route.packaged-yunnan-5d'],
40
+ disclosure: {
41
+ commissionBps: 300,
42
+ statement: '本方案由旅行社 A 提供库存,成交后旅行社获得 3% 佣金。',
43
+ rankingBiasNotice: '同分候选按旅行社申报顺位排序;佣金不影响条件判定。',
44
+ },
45
+ };
46
+ }
47
+ function sponsorConfigB() {
48
+ const c = sponsorConfigA();
49
+ return { ...c, sponsorId: 'sponsor-b-board', bffPrincipalId: 'bff-board-b', disclosure: { ...c.disclosure, commissionBps: 0 } };
50
+ }
51
+ function makePlugin(overrides = {}) {
52
+ return createSponsorPlugin({ ...sponsorConfigA(), ...overrides });
53
+ }
54
+ const WISH = { days: 4, budgetCny: 8000, month: 7 };
55
+ function candidates() {
56
+ return [
57
+ {
58
+ candidateRef: 'cand-alpha',
59
+ route: 'route.packaged-erhai-4d',
60
+ name: '大理洱海 4 日包价',
61
+ priceCny: 6500,
62
+ conditions: { days: 4, budget_cny: 7000, best_months: [7] },
63
+ commissionBps: 300,
64
+ },
65
+ {
66
+ candidateRef: 'cand-beta',
67
+ route: 'route.packaged-yunnan-5d',
68
+ name: '云南 5 日(旅行者只有 4 天)',
69
+ priceCny: 7200,
70
+ conditions: { days: 5, budget_cny: 7000, best_months: [7] },
71
+ commissionBps: 900,
72
+ },
73
+ ];
74
+ }
75
+ // ── 1. 插件配置 fail-closed(反例先行) ───────────────────────────────────────
76
+ for (const [field, broken] of [
77
+ ['sponsorId', { sponsorId: '' }],
78
+ ['travelerPrincipalId', { travelerPrincipalId: ' ' }],
79
+ ['bffPrincipalId', { bffPrincipalId: '' }],
80
+ ['allowedRoutes-empty', { allowedRoutes: [] }],
81
+ ['allowedRoutes-dup', { allowedRoutes: ['route.a', 'route.a'] }],
82
+ ['statement', { disclosure: { commissionBps: 100, statement: '', rankingBiasNotice: 'x' } }],
83
+ ['rankingBiasNotice', { disclosure: { commissionBps: 100, statement: 'x', rankingBiasNotice: '' } }],
84
+ ['commissionBps-negative', { disclosure: { commissionBps: -1, statement: 'x', rankingBiasNotice: 'y' } }],
85
+ ['commissionBps-fraction', { disclosure: { commissionBps: 1.5, statement: 'x', rankingBiasNotice: 'y' } }],
86
+ ]) {
87
+ assert.throws(() => makePlugin(broken), Error, `缺/坏 ${field} → 拒绝建插件(fail-closed)`);
88
+ }
89
+ assert.throws(() => makePlugin({ travelerPrincipalId: 'sponsor-a-travel' }), Error, 'sponsorId 冒充 traveler principal(三主体混同)→ 拒绝');
90
+ const pluginA = makePlugin();
91
+ assert.equal(pluginA.schemaVersion, SPONSOR_PLUGIN_SCHEMA, '插件 schema = sponsor.plugin.v1');
92
+ assert.match(pluginA.disclosureDigestSha256, /^[0-9a-f]{64}$/, '披露口径摘要为 sha256(佣金/文案/偏置声明进 digest)');
93
+ // ── 2. 运行激活默认关闭 ─────────────────────────────────────────────────────
94
+ assert.equal(SPONSOR_RUNTIME_ACTIVATION_DEFAULT, false, '激活常量默认 false(结构性默认关)');
95
+ assert.throws(() => requireRuntimeActivation(SPONSOR_RUNTIME_ACTIVATION_DEFAULT), Error, '默认态调用激活门 → 抛错');
96
+ assert.doesNotThrow(() => requireRuntimeActivation(true), '显式 opt-in 才放行(真实运行时接线属 M6 Entry 后)');
97
+ // ── 3. 路由授权(越权反例) ──────────────────────────────────────────────────
98
+ const unauthorized = authorizeSponsorRoute(pluginA, 'route.not-declared', { sessionRef: 'sess-1' });
99
+ assert.equal(unauthorized.ok, false, '越权路由 → 拒绝');
100
+ assert.equal(unauthorized.ok ? '' : unauthorized.reason, 'route-unauthorized', '拒绝理由 = route-unauthorized');
101
+ const unbound = authorizeSponsorRoute(pluginA, 'route.packaged-erhai-4d', { sessionRef: null });
102
+ assert.equal(unbound.ok ? '' : unbound.reason, 'missing-session-binding', '授权路由但缺 session 绑定 → 拒绝(禁止复制 Buyer/credentials 规避)');
103
+ const okRoute = authorizeSponsorRoute(pluginA, 'route.packaged-erhai-4d', { sessionRef: 'sess-traveler-001', orderRef: 'ord-fix-1' });
104
+ assert.equal(okRoute.ok, true, '闭集内路由 + session/order 绑定 → 授权通过');
105
+ const pluginB = makePlugin(sponsorConfigB());
106
+ const crossRoute = authorizeSponsorRoute(pluginB, 'route.packaged-yunnan-5d', { sessionRef: 'sess-b' });
107
+ assert.equal(crossRoute.ok, true, '每个 sponsor 的授权闭集独立维护');
108
+ // ── 4. 同内核引用证明(复用面 = 零拷贝 + 零内核改动 + C 端隔离) ───────────────
109
+ // 4a. 内核契约从消费侧钉死:12 格 (origin,trigger) 边表/拒绝表与冻结字面量逐字一致
110
+ const EDGE_GOLDEN = [
111
+ 'none:propose>pending:write.pending',
112
+ 'pending:confirm>confirmed:write.confirmed',
113
+ 'pending:compensate>compensated:write.compensated',
114
+ 'confirmed:compensate>compensated:write.compensated',
115
+ ];
116
+ assert.equal(SAGA_EDGES.map((e) => `${e.from}:${e.trigger}>${e.to}:${e.event}`).join('|'), EDGE_GOLDEN.join('|'), '内核 booking_saga_fsm.v1 边表与冻结字面量逐字一致(内核漂移即红)');
117
+ const REJECT_GOLDEN = [
118
+ 'none:confirm', 'none:compensate', 'pending:propose', 'confirmed:propose',
119
+ 'confirmed:confirm', 'compensated:propose', 'compensated:confirm', 'compensated:compensate',
120
+ ];
121
+ assert.equal(SAGA_REJECTIONS.map((e) => `${e.from}:${e.trigger}`).join('|'), REJECT_GOLDEN.join('|'), '内核拒绝闭集八格与冻结字面量逐字一致');
122
+ // 4b. sponsor 模块同源 import(零拷贝:不重声明内核边表/解析器/评分器)
123
+ const sponsorSrc = readFileSync(join(REPO_TS, 'capabilities/sponsor-plugin.ts'), 'utf8');
124
+ assert.match(sponsorSrc, /from '\.\.\/src\/booking-saga\.ts'/, 'sponsor 模块 import 自内核 booking-saga.ts(同一内核函数)');
125
+ assert.match(sponsorSrc, /from '\.\.\/src\/wish-pool\.ts'/, 'sponsor 模块 import 自内核 wish-pool.ts(同一评分函数)');
126
+ assert.doesNotMatch(sponsorSrc, /function resolveSagaTrigger/, '零拷贝:不重声明内核触发解析器');
127
+ assert.doesNotMatch(sponsorSrc, /const SAGA_EDGES/, '零拷贝:不重声明内核边表');
128
+ assert.doesNotMatch(sponsorSrc, /function scoreWishMatch/, '零拷贝:不重声明内核评分器');
129
+ // 4c. 运行时面隔离:ts/src/** 无任何文件引用 sponsor-plugin(C 端默认路径结构性零渗入)
130
+ function walkTs(dir) {
131
+ return readdirSync(dir).flatMap((name) => {
132
+ const p = join(dir, name);
133
+ if (statSync(p).isDirectory())
134
+ return walkTs(p);
135
+ return name.endsWith('.ts') ? [p] : [];
136
+ });
137
+ }
138
+ const runtimeFiles = walkTs(join(REPO_TS, 'src'));
139
+ assert.ok(runtimeFiles.length > 20, `运行时面文件枚举非空(实得 ${runtimeFiles.length})`);
140
+ const leaked = runtimeFiles.filter((f) => readFileSync(f, 'utf8').includes('sponsor-plugin'));
141
+ assert.deepEqual(leaked, [], 'ts/src/** 零 sponsor-plugin 引用(C 端默认路径不接线 sponsor 面)');
142
+ // ── 5. 排序红线(赞助收益不伪造用户效用) ────────────────────────────────────
143
+ const ranking = rankSponsorCandidates(pluginA, candidates(), WISH);
144
+ assert.deepEqual(ranking.excluded.map((e) => `${e.candidateRef}:${e.why}`), ['cand-beta:conditions-not-met'], '高佣金(900bps)但天数不足的候选保持排除——佣金不可赎回条件');
145
+ assert.equal(ranking.ranked.length, 1, '只有条件全命中的候选进入排序');
146
+ assert.equal(ranking.ranked[0].candidate.candidateRef, 'cand-alpha', '命中候选 = cand-alpha');
147
+ assert.equal(ranking.ranked[0].sponsorTieBreakApplied, false, '无同分不发生赞助 tie-break');
148
+ assert.ok(ranking.ranked[0].disclosure.statement.length > 0, '排序结果必携披露文案');
149
+ assert.ok(ranking.biasNotice.length > 0, '排序输出顶层必携偏置声明(渲染面必须可见)');
150
+ // 同分 tie:按申报顺位,不按佣金;发生 tie-break 必须显式披露
151
+ const tieCandidates = [
152
+ { candidateRef: 'tie-low', route: 'route.packaged-erhai-4d', name: '低佣金', priceCny: 6600, conditions: { days: 4, budget_cny: 7000 }, commissionBps: 100 },
153
+ { candidateRef: 'tie-high', route: 'route.packaged-yunnan-5d', name: '高佣金同分', priceCny: 6700, conditions: { days: 4, budget_cny: 7000 }, commissionBps: 900 },
154
+ ];
155
+ const tieRank = rankSponsorCandidates(pluginA, tieCandidates, { days: 4, budgetCny: 8000 });
156
+ assert.deepEqual(tieRank.ranked.map((r) => r.candidate.candidateRef), ['tie-low', 'tie-high'], '同分按申报顺位(申报顺位在前的低佣金居首)');
157
+ assert.equal(tieRank.ranked[0].sponsorTieBreakApplied, false, '申报顺位首位无需 tie-break 标记');
158
+ const tieReversed = rankSponsorCandidates(pluginA, [tieCandidates[1], tieCandidates[0]], { days: 4, budgetCny: 8000 });
159
+ assert.deepEqual(tieReversed.ranked.map((r) => r.candidate.candidateRef), ['tie-high', 'tie-low'], '调换申报顺位则高佣金居首——排序键是申报顺位而非佣金');
160
+ assert.equal(tieReversed.ranked[0].sponsorTieBreakApplied, true, 'tie-break 真实发生时必须标记');
161
+ assert.ok(tieReversed.ranked[0].disclosure.rankingBiasNotice.length > 0, 'tie-break 候选必携偏置声明(偏置必须展示给旅行者)');
162
+ // 内核评分器直证:同一 scoreCandidateAgainstWish 在两形态共用(条件不满足 → null)
163
+ assert.equal(scoreCandidateAgainstWish(candidates()[1], WISH), null, '内核评分:天数不足 → null(不可预订)');
164
+ assert.equal(scoreCandidateAgainstWish(candidates()[0], WISH)?.hits.length, 3, '内核评分:三条件全命中');
165
+ // ── 6. 端到端 B2B(fixture 合成数据,零真实调用) ─────────────────────────────
166
+ const b2bTrace = runBookingScenario({
167
+ businessId: 'BIZ-1',
168
+ travelerPrincipalId: 'traveler-001',
169
+ sessionRef: 'sess-traveler-001',
170
+ candidates: candidates(),
171
+ wish: WISH,
172
+ action: 'confirm',
173
+ receipt: 'receipt-hb-fix-001',
174
+ plugin: pluginA,
175
+ });
176
+ assert.equal(b2bTrace.form, 'b2b-sponsor', 'B2B 形态标记 b2b-sponsor');
177
+ assert.equal(b2bTrace.selectedCandidateRef, 'cand-alpha', '选中条件全命中候选');
178
+ const b2bKinds = b2bTrace.events.map((e) => e.kind);
179
+ assert.deepEqual(b2bKinds, ['route.authorized', 'candidate.ranked', 'write.pending', 'write.confirmed'], 'B2B 事件序列:路由授权→排序→propose→confirm');
180
+ const b2bPending = b2bTrace.events[2];
181
+ const b2bConfirmed = b2bTrace.events[3];
182
+ assert.equal(b2bTrace.sagaStatus, 'confirmed', 'saga 终态 confirmed');
183
+ assert.ok(b2bPending.guard !== null && b2bPending.guard.includes('idem_key UNIQUE'), 'write.pending 携内核边表守卫原文(同一张边表)');
184
+ assert.ok(b2bConfirmed.guard !== null && b2bConfirmed.guard.includes('receipt'), 'write.confirmed 携内核边表守卫原文(L3 具名确认)');
185
+ assert.equal(b2bConfirmed.receipt, 'receipt-hb-fix-001', '确认必携外部回执');
186
+ assert.deepEqual(b2bTrace.sagaViolations, [], '内核 sagaTraceViolations 回放零违例(审计链合法)');
187
+ // 披露 digest 进入确认指纹:同一请求换披露 → 指纹必变
188
+ const fpA = buildConfirmationFingerprint({
189
+ businessId: 'BIZ-1', travelerPrincipalId: 'traveler-001', sponsorId: 'sponsor-a-travel',
190
+ candidateRef: 'cand-alpha', route: 'route.packaged-erhai-4d', priceCny: 6500,
191
+ disclosureDigestSha256: pluginA.disclosureDigestSha256,
192
+ });
193
+ const fpA2 = buildConfirmationFingerprint({
194
+ businessId: 'BIZ-1', travelerPrincipalId: 'traveler-001', sponsorId: 'sponsor-a-travel',
195
+ candidateRef: 'cand-alpha', route: 'route.packaged-erhai-4d', priceCny: 6500,
196
+ disclosureDigestSha256: '0'.repeat(64),
197
+ });
198
+ assert.notEqual(fpA, fpA2, '披露口径变化 → 确认指纹变化(佣金披露进入确认指纹)');
199
+ assert.ok(b2bConfirmed.fingerprintSha256 && b2bConfirmed.fingerprintSha256.length === 64, '确认事件携带 64 位指纹');
200
+ const approval = bindApproval({ fingerprintSha256: fpA, receipt: 'receipt-hb-fix-001', travelerPrincipalId: 'traveler-001', sponsorId: 'sponsor-a-travel' });
201
+ assert.equal(verifyApprovalBinding(fpA, approval), true, 'approval 绑定同指纹校验通过');
202
+ assert.equal(verifyApprovalBinding(fpA2, approval), false, '指纹不匹配 → approval 绑定校验失败');
203
+ // ── 7. 端到端 B2C + 隔离(同一 runner,零 sponsor 渗入) ──────────────────────
204
+ const b2cTrace = runBookingScenario({
205
+ businessId: 'BIZ-1',
206
+ travelerPrincipalId: 'traveler-001',
207
+ candidates: candidates(),
208
+ wish: WISH,
209
+ action: 'confirm',
210
+ receipt: 'receipt-hb-fix-001',
211
+ });
212
+ assert.equal(b2cTrace.form, 'b2c', '无 plugin → C 端默认形态');
213
+ assert.equal(b2cTrace.selectedCandidateRef, 'cand-alpha', 'C 端同一内核评分选中同一候选(复用)');
214
+ assert.deepEqual(b2cTrace.events.map((e) => e.kind), ['candidate.ranked', 'write.pending', 'write.confirmed'], 'C 端无路由授权/披露事件');
215
+ // 同一内核边序列:两形态 saga 段逐格相同(from/to/event/guard)
216
+ const sagaSlice = (t) => t.events
217
+ .filter((e) => e.sagaEvent !== null)
218
+ .map((e) => `${e.kind}:${e.from}>${e.to}:${(e.guard ?? '').slice(0, 12)}`);
219
+ assert.deepEqual(sagaSlice(b2bTrace), sagaSlice(b2cTrace), 'B2B/B2C saga 段逐格相同——同一内核函数在 sponsor 形态下跑通同一状态机');
220
+ assert.deepEqual(sponsorLeakFindings(b2cTrace), [], 'C 端 trace 泄漏检查零发现(sponsor 字段零渗入)');
221
+ assert.ok(sponsorLeakFindings(b2bTrace).length > 0, '对照:B2B trace 泄漏检查有发现(检查器本身有效)');
222
+ // 空 receipt 两种形态同拒(同一 WriteGate/空 receipt 红线,无豁免)
223
+ for (const [form, opts] of [
224
+ ['b2b', { plugin: pluginA, sessionRef: 'sess-traveler-001' }],
225
+ ['b2c', {}],
226
+ ]) {
227
+ assert.throws(() => runBookingScenario({
228
+ businessId: 'BIZ-1', travelerPrincipalId: 'traveler-001',
229
+ candidates: candidates(), wish: WISH, action: 'confirm',
230
+ receipt: ' ', ...opts,
231
+ }), Error, `${form} 空 receipt → 拒绝(空 receipt 红线无 B2B 豁免)`);
232
+ }
233
+ assert.throws(() => runBookingScenario({
234
+ businessId: 'BIZ-1', travelerPrincipalId: 'traveler-001', sessionRef: 'sess-x',
235
+ candidates: [{ ...candidates()[0], route: 'route.not-declared' }], wish: WISH,
236
+ action: 'confirm', receipt: 'r-1', plugin: pluginA,
237
+ }), Error, 'B2B 端到端越权路由 → fail-closed 中止(不静默换路由)');
238
+ // ── 8. A/B 同业务 id 不串(事件/投影/approval/effect) ────────────────────────
239
+ const traceA = b2bTrace;
240
+ const traceB = runBookingScenario({
241
+ businessId: 'BIZ-1',
242
+ travelerPrincipalId: 'traveler-001',
243
+ sessionRef: 'sess-board-b',
244
+ candidates: candidates(),
245
+ wish: WISH,
246
+ action: 'cancel',
247
+ receipt: 'receipt-hb-fix-002',
248
+ plugin: pluginB,
249
+ });
250
+ assert.equal(traceA.scope.sponsorId, 'sponsor-a-travel', 'A 事件 scope 携 sponsor-a');
251
+ assert.equal(traceB.scope.sponsorId, 'sponsor-b-board', 'B 事件 scope 携 sponsor-b');
252
+ assert.ok(traceA.events.every((e) => e.scope.sponsorId === 'sponsor-a-travel'), 'A 的全部事件归属 A(同业务 id 不串事件)');
253
+ assert.ok(traceB.events.every((e) => e.scope.sponsorId === 'sponsor-b-board'), 'B 的全部事件归属 B');
254
+ assert.notEqual(traceA.scope.key, traceB.scope.key, '同 businessId 不同 sponsor → scope 键不同(投影隔离)');
255
+ assert.equal(traceB.sagaStatus, 'compensated', 'B 走取消退款:propose→confirm→compensate');
256
+ assert.deepEqual(traceB.events.map((e) => e.kind), ['route.authorized', 'candidate.ranked', 'write.pending', 'write.confirmed', 'write.compensated'], '取消路径事件序列');
257
+ assert.deepEqual(traceB.sagaViolations, [], 'B 取消退款回放零违例(sponsor 形态同一取消/退款规则)');
258
+ // approval 不可跨 scope 重放:A 的 approval 绑到 B 的指纹 → 校验失败
259
+ const approvalB = bindApproval({ fingerprintSha256: traceB.events[3].fingerprintSha256, receipt: 'receipt-hb-fix-002', travelerPrincipalId: 'traveler-001', sponsorId: 'sponsor-b-board' });
260
+ assert.equal(verifyApprovalBinding(traceA.events[3].fingerprintSha256, approvalB), false, 'A 的指纹不接受 B 的 approval(BFF 重放/跨租户绑定拒绝)');
261
+ assert.deepEqual(traceA.sagaViolations, [], 'A 回放零违例');
262
+ console.log('SPONSOR REUSE TESTS: 8/8 OK(配置 fail-closed / 默认关激活门 / 越权路由拒绝 / 同内核零拷贝+C端隔离 / 佣金不伪造效用 / B2B fixture E2E+披露入指纹 / B2C 同 runner 零渗入+同 saga 边 / A/B 同业务id不串)');
@@ -1,136 +1,261 @@
1
- import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1
+ /**
2
+ * 账本 CLI e2e(run-all §29):子进程走 state-cli 的操作面——
3
+ * migrate(旧文件→账本)/ stats / log / export(视图单向导出)/ forget(物理硬删)/
4
+ * pw-request→pw-confirm→pw-list(saga CLI 面),并锁住严格参数解析与 local-only 边界。
5
+ * 运行(在 ts/ 下):npx tsx scripts/state-cli-tests.ts
6
+ */
7
+ import Database from 'better-sqlite3';
8
+ import { createHash } from 'node:crypto';
9
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
10
  import { tmpdir } from 'node:os';
3
11
  import { join } from 'node:path';
4
12
  import { spawnSync } from 'node:child_process';
13
+ import { parseFlightPackToSpec } from '../src/unified.js';
5
14
  let pass = 0;
6
15
  let fail = 0;
7
16
  function assert(cond, msg) {
8
17
  if (cond) {
9
18
  pass++;
10
19
  console.log(` ok - ${msg}`);
11
- } else {
20
+ }
21
+ else {
12
22
  fail++;
13
23
  console.error(` FAIL - ${msg}`);
14
24
  }
15
25
  }
16
26
  function cli(args) {
17
- return spawnSync('npx', [
18
- 'tsx',
19
- 'scripts/state-cli.ts',
20
- ...args
21
- ], {
22
- encoding: 'utf-8'
23
- });
27
+ return spawnSync('npx', ['tsx', 'scripts/state-cli.ts', ...args], { encoding: 'utf-8' });
28
+ }
29
+ function snapshotPath(path) {
30
+ if (!existsSync(path))
31
+ return '<missing>';
32
+ const h = createHash('sha256');
33
+ const walk = (p, rel) => {
34
+ const st = statSync(p);
35
+ if (st.isDirectory()) {
36
+ h.update(`dir:${rel}\n`);
37
+ for (const name of readdirSync(p).sort())
38
+ walk(join(p, name), rel ? `${rel}/${name}` : name);
39
+ }
40
+ else if (st.isFile()) {
41
+ h.update(`file:${rel}:${st.size}\n`);
42
+ h.update(readFileSync(p));
43
+ h.update('\n');
44
+ }
45
+ else {
46
+ h.update(`other:${rel}:${st.mode}:${st.size}\n`);
47
+ }
48
+ };
49
+ walk(path, '');
50
+ return h.digest('hex');
51
+ }
52
+ function seedTenantFixture(root, tenant) {
53
+ const db = new Database(join(root, 'gotry-state', 'gotry-state.db'));
54
+ const ts = '2026-09-08T00:00:00.000Z';
55
+ db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
56
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(tenant, ts, 'test:tenant-fixture', 'tenant.fixture', 'tenant-subject', '{}', 'tenant-fixture-1', null);
57
+ db.prepare(`INSERT INTO pending_writes (tenant_id, idem_key, seam, payload, status, receipt, created, updated)
58
+ VALUES (?, ?, ?, ?, 'pending', NULL, ?, ?)`).run(tenant, 'tenant-pw-1', 'tenant-seam', '{}', ts, ts);
59
+ db.close();
60
+ }
61
+ function seedPendingWorkflowPair(root, id) {
62
+ const pack = JSON.parse(readFileSync(join('..', 'data', 'flights_2026.json'), 'utf-8'));
63
+ const spec = parseFlightPackToSpec(pack);
64
+ spec.budgetCny = 9000;
65
+ const ticket = {
66
+ id,
67
+ objective: 'CLI tick E2E:本地 pending 工单真实回收',
68
+ requestedAt: '2026-09-08T00:00:00.000Z',
69
+ etaLabel: '秒级',
70
+ };
71
+ const state = {
72
+ calendar: { year: 2026, assertedWeekdays: {} },
73
+ profile: {},
74
+ gates: [],
75
+ wishes: [],
76
+ spec,
77
+ };
78
+ const db = new Database(join(root, 'gotry-state', 'gotry-state.db'));
79
+ const ts = '2026-09-08T00:00:00.000Z';
80
+ const insertRun = db.prepare(`INSERT INTO workflow_runs (tenant_id, id, goal, status, ticket_json, state_json, deliverable, created, updated)
81
+ VALUES (?, ?, ?, 'pending', ?, ?, NULL, ?, ?)`);
82
+ const insertEvent = db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
83
+ VALUES (?, ?, ?, 'async.run_created', ?, ?, ?, ?)`);
84
+ for (const tenant of ['local', 'tenant-b']) {
85
+ insertRun.run(tenant, id, ticket.objective, JSON.stringify(ticket), JSON.stringify(state), ts, ts);
86
+ insertEvent.run(tenant, ts, 'test:pending-tick', id, JSON.stringify({ ticket }), `tick:${tenant}:${id}`, id);
87
+ }
88
+ db.close();
89
+ }
90
+ function workflowSummary(root, id) {
91
+ const db = new Database(join(root, 'gotry-state', 'gotry-state.db'), { readonly: true });
92
+ const runs = db.prepare(`SELECT tenant_id, id, status, deliverable FROM workflow_runs WHERE id = ? ORDER BY tenant_id`).all(id);
93
+ const steps = db.prepare(`SELECT tenant_id, run_id, name, status, result FROM workflow_steps WHERE run_id = ? ORDER BY tenant_id, name`).all(id);
94
+ const events = db.prepare(`SELECT tenant_id, kind, subject_id, idem_key, run_id FROM events WHERE run_id = ? OR subject_id = ? ORDER BY tenant_id, seq`).all(id, id);
95
+ db.close();
96
+ return JSON.stringify({ runs, steps, events });
24
97
  }
98
+ function workflowRows(root, id) {
99
+ const db = new Database(join(root, 'gotry-state', 'gotry-state.db'), { readonly: true });
100
+ const rows = {
101
+ runs: db.prepare(`SELECT tenant_id, status, deliverable FROM workflow_runs WHERE id = ? ORDER BY tenant_id`).all(id),
102
+ steps: db.prepare(`SELECT tenant_id, name, status, result FROM workflow_steps WHERE run_id = ? ORDER BY tenant_id, name`).all(id),
103
+ events: db.prepare(`SELECT tenant_id, kind FROM events WHERE run_id = ? ORDER BY tenant_id, seq`).all(id),
104
+ };
105
+ db.close();
106
+ return rows;
107
+ }
108
+ function rowFor(rows, tenant) {
109
+ return rows.find(r => r.tenant_id === tenant);
110
+ }
111
+ // 迁移面:旧文件 root → migrate → stats/log/export
25
112
  const root = mkdtempSync(join(tmpdir(), 'gotry-cli-'));
26
113
  const dir = join(root, 'gotry-state');
27
- mkdirSync(dir, {
28
- recursive: true
29
- });
30
- writeFileSync(join(dir, 'motivation-profile.json'), JSON.stringify({
31
- weights: {
32
- curiosity: 0.6
33
- },
34
- evidence: [
35
- '原话A'
36
- ],
37
- hard: {}
38
- }));
39
- writeFileSync(join(dir, 'wish-pool.json'), JSON.stringify([
40
- {
41
- wish_id: 'wLEGACY1',
42
- name: '京都',
43
- conditions: {
44
- days: 7
45
- },
46
- added_at: '2026-08-01T00:00:00Z'
47
- }
48
- ]));
49
- const mig = cli([
50
- 'migrate',
51
- root
52
- ]);
114
+ mkdirSync(dir, { recursive: true });
115
+ writeFileSync(join(dir, 'motivation-profile.json'), JSON.stringify({ weights: { curiosity: 0.6 }, evidence: ['原话A'], hard: {} }));
116
+ writeFileSync(join(dir, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wLEGACY1', name: '京都', conditions: { days: 7 }, added_at: '2026-08-01T00:00:00Z' }]));
117
+ const mig = cli(['migrate', root]);
53
118
  assert(mig.status === 0 && /账本就绪/.test(mig.stdout ?? ''), `migrate exit 0 且报告就绪(实际 ${mig.status}:${(mig.stderr ?? '').slice(0, 200)})`);
54
119
  assert(existsSync(join(dir, 'gotry-state.db')), 'migrate 后账本文件存在');
55
120
  assert(existsSync(join(dir, 'pre-ledger-backup', 'wish-pool.json')), 'migrate 前自动快照(pre-ledger-backup/)');
56
- const stats = cli([
57
- 'stats',
58
- root
59
- ]);
121
+ const stats = cli(['stats', root]);
60
122
  assert(stats.status === 0 && /wishes=1/.test(stats.stdout ?? '') && /events=\d+/.test(stats.stdout ?? ''), `stats 计数正确(${(stats.stdout ?? '').trim()})`);
61
- const log = cli([
62
- 'log',
63
- root,
64
- '--limit',
65
- '3'
66
- ]);
123
+ const log = cli(['log', root, '--limit', '3']);
67
124
  assert(log.status === 0 && /\bwish\.imported\b|\bmotivation\.imported\b/.test(log.stdout ?? ''), `log 呈现账本事件(append-only 审计面)`);
125
+ // export:视图单向导出(DB→文件)
68
126
  rmSync(join(dir, 'wish-pool.json'));
69
- const exp = cli([
70
- 'export',
71
- root
72
- ]);
127
+ const exp = cli(['export', root]);
73
128
  assert(exp.status === 0 && existsSync(join(dir, 'wish-pool.json')), 'export 重建旧文件名视图(红线 6:可见可导出)');
74
129
  const exported = JSON.parse(readFileSync(join(dir, 'wish-pool.json'), 'utf-8'));
75
130
  assert(exported[0]?.wish_id === 'wLEGACY1', 'export 视图内容与账本一致');
76
- const fg = cli([
77
- 'forget',
78
- root,
79
- 'wish',
80
- 'wLEGACY1'
81
- ]);
131
+ const tickLocal = cli(['tick', root]);
132
+ assert(tickLocal.status === 0 && /无 pending 工单/.test(tickLocal.stdout ?? ''), 'tick local 无 pending 时不求解且正常返回');
133
+ const forkDest = join(root, 'forks', 'whatif.db');
134
+ const whatifLocal = cli(['whatif', root, forkDest]);
135
+ assert(whatifLocal.status === 0 && existsSync(forkDest), 'whatif local 生成整库 snapshot 副本');
136
+ // forget:物理硬删 + 审计一行
137
+ const fg = cli(['forget', root, 'wish', 'wLEGACY1']);
82
138
  assert(fg.status === 0 && /物理硬删/.test(fg.stdout ?? ''), 'forget 执行成功');
83
- const stats2 = cli([
84
- 'stats',
85
- root
86
- ]);
139
+ const stats2 = cli(['stats', root]);
87
140
  assert(/wishes=0/.test(stats2.stdout ?? ''), 'forget 后愿望池归零(红线 6「可删除」,D5 物理硬删)');
88
- const log2 = cli([
89
- 'log',
90
- root,
91
- '--limit',
92
- '3'
93
- ]);
141
+ const log2 = cli(['log', root, '--limit', '3']);
94
142
  assert(/forget\.executed/.test(log2.stdout ?? ''), 'forget 留审计一行(删除本身可溯源)');
143
+ // saga CLI 面:fresh root → pw-request → pw-confirm → pw-list
95
144
  const root2 = mkdtempSync(join(tmpdir(), 'gotry-cli2-'));
96
- const req = cli([
97
- 'pw-request',
98
- root2,
99
- 'booking:e2e-1',
100
- 'flight-order-confirm',
101
- '{"flight":"MU123"}'
102
- ]);
145
+ const req = cli(['pw-request', root2, 'booking:e2e-1', 'flight-order-confirm', '{"flight":"MU123"}']);
103
146
  assert(req.status === 0 && /"created":true/.test(req.stdout ?? ''), 'pw-request 登记成功(L2:只登记不执行)');
104
- const dup = cli([
105
- 'pw-request',
106
- root2,
107
- 'booking:e2e-1',
108
- 'flight-order-confirm',
109
- '{"flight":"MU123"}'
110
- ]);
147
+ const dup = cli(['pw-request', root2, 'booking:e2e-1', 'flight-order-confirm', '{"flight":"MU123"}']);
111
148
  assert(/"created":false/.test(dup.stdout ?? ''), 'pw-request 幂等键去重');
112
- const conf = cli([
113
- 'pw-confirm',
114
- root2,
115
- 'booking:e2e-1',
116
- 'PNR-XYZ'
117
- ]);
149
+ const conf = cli(['pw-confirm', root2, 'booking:e2e-1', 'PNR-XYZ']);
118
150
  assert(conf.status === 0 && /"ok":true/.test(conf.stdout ?? ''), 'pw-confirm 携 receipt 确认(L3)');
119
- const list = cli([
120
- 'pw-list',
121
- root2
122
- ]);
151
+ const list = cli(['pw-list', root2]);
123
152
  assert(/confirmed/.test(list.stdout ?? '') && /PNR-XYZ/.test(list.stdout ?? ''), 'pw-list 呈现终态与 receipt');
124
- rmSync(root, {
125
- recursive: true,
126
- force: true
127
- });
128
- rmSync(root2, {
129
- recursive: true,
130
- force: true
131
- });
153
+ // 严格解析:未知/重复/缺值/非法 numeric 在打开账本前 fail-closed,且不改 root 内容。
154
+ const parseRoot = mkdtempSync(join(tmpdir(), 'gotry-cli-parse-'));
155
+ const parseMig = cli(['migrate', parseRoot]);
156
+ assert(parseMig.status === 0, 'strict 解析夹具账本可创建');
157
+ const strictCases = [
158
+ { name: '未知选项', args: ['stats', parseRoot, '--bogus', 'x'], pattern: /未知选项 --bogus/ },
159
+ { name: '重复选项', args: ['stats', parseRoot, '--tenant', 'a', '--tenant', 'b'], pattern: /重复选项 --tenant/ },
160
+ { name: '重复 state-root', args: ['stats', '--state-root', parseRoot, '--state-root', parseRoot], pattern: /重复选项 --state-root/ },
161
+ { name: '缺少选项值', args: ['stats', parseRoot, '--tenant', '--limit', '1'], pattern: /--tenant 缺少值/ },
162
+ { name: '非法 limit=0', args: ['log', parseRoot, '--limit', '0'], pattern: /--limit 必须是正整数/ },
163
+ { name: '非 log 的 limit', args: ['stats', parseRoot, '--limit', '3'], pattern: /stats 不支持 --limit/ },
164
+ { name: '单横杠未知选项', args: ['stats', parseRoot, '-x'], pattern: /未知选项 -x/ },
165
+ { name: 'rebuild 小数 seq', args: ['rebuild', '1.5'], pattern: /toSeq 必须是非负整数:1\.5/ },
166
+ { name: 'rebuild 省略整数位小数 seq', args: ['rebuild', '.5'], pattern: /toSeq 必须是非负整数:\.5/ },
167
+ { name: 'rebuild 正号省略整数位小数 seq', args: ['rebuild', '+.5'], pattern: /toSeq 必须是非负整数:\+\.5/ },
168
+ { name: 'rebuild 负数 seq', args: ['rebuild', '-1'], pattern: /toSeq 必须是非负整数:-1/ },
169
+ { name: '非法 seq', args: ['rewind', parseRoot, '1.5'], pattern: /seq 必须是非负整数/ },
170
+ { name: '负数 seq', args: ['rewind', parseRoot, '-3'], pattern: /seq 必须是非负整数/ },
171
+ { name: '未知命令', args: ['bogus-cmd', parseRoot], pattern: /未知命令/ },
172
+ ];
173
+ for (const c of strictCases) {
174
+ const before = snapshotPath(parseRoot);
175
+ const r = cli(c.args);
176
+ const after = snapshotPath(parseRoot);
177
+ assert(r.status !== 0 && c.pattern.test(r.stderr ?? '') && before === after, `strict parse ${c.name} 零账本/文件变化`);
178
+ }
179
+ const numericParent = mkdtempSync(join(tmpdir(), 'gotry-cli-numeric-parent-'));
180
+ const numericRoot = join(numericParent, '1.5');
181
+ const dotNumericRoot = join(numericParent, '.5');
182
+ const numericMig = cli(['migrate', '--state-root', numericRoot]);
183
+ const numericRebuild = cli(['rebuild', '--state-root', numericRoot, '1']);
184
+ const dotNumericMig = cli(['migrate', '--state-root', dotNumericRoot]);
185
+ const dotNumericRebuild = cli(['rebuild', '--state-root', dotNumericRoot, '1']);
186
+ assert(numericMig.status === 0 && numericRebuild.status === 0 && dotNumericMig.status === 0 && dotNumericRebuild.status === 0, '数字/小数样式 root 通过 --state-root 明示仍可用');
187
+ // local-only:非 local 的 tick/export/whatif 必须在 mkdir/openDb/solve/写文件前拒绝。
188
+ const guardRoot = mkdtempSync(join(tmpdir(), 'gotry-cli-guard-'));
189
+ const guardDir = join(guardRoot, 'gotry-state');
190
+ mkdirSync(guardDir, { recursive: true });
191
+ writeFileSync(join(guardDir, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wGUARD', name: '台北', conditions: { days: 4 }, added_at: '2026-09-01T00:00:00Z' }]));
192
+ const guardMig = cli(['migrate', guardRoot]);
193
+ assert(guardMig.status === 0, 'local-only 夹具账本可创建');
194
+ const badWhatifDest = join(guardRoot, 'forks', 'tenant.db');
195
+ const localOnlyCases = [
196
+ { name: 'export', args: ['export', '--tenant', 'tenant-a', '--state-root', guardRoot] },
197
+ { name: 'tick', args: ['tick', guardRoot, '--tenant', 'tenant-a'] },
198
+ { name: 'whatif', args: ['whatif', guardRoot, badWhatifDest, '--tenant', 'tenant-a'] },
199
+ ];
200
+ for (const c of localOnlyCases) {
201
+ const before = snapshotPath(guardRoot);
202
+ const r = cli(c.args);
203
+ const after = snapshotPath(guardRoot);
204
+ assert(r.status !== 0 && /仅支持 --tenant local/.test(r.stderr ?? '') && before === after, `${c.name} 非 local fail-closed 且 root hash 不变`);
205
+ }
206
+ assert(!existsSync(badWhatifDest), '非 local whatif 未创建 snapshot 文件');
207
+ const missingBase = mkdtempSync(join(tmpdir(), 'gotry-cli-missing-'));
208
+ const missingRoot = join(missingBase, 'missing-root');
209
+ const missingReject = cli(['export', '--tenant', 'tenant-a', '--state-root', missingRoot]);
210
+ assert(missingReject.status !== 0 && !existsSync(missingRoot), '非 local export 在无 DB root 下零目录创建');
211
+ // tick 正/负 E2E:local 与 tenant-b 同 id pending。非 local 拒绝必须保持双方 workflow/step/audit 不变;
212
+ // local tick 必须用真实 state-cli 子进程 + solveUnified 回收本地 pending,且不碰 tenant-b。
213
+ const tickRoot = mkdtempSync(join(tmpdir(), 'gotry-cli-tick-'));
214
+ const tickId = 'dp-cli-tick-e2e';
215
+ const tickMig = cli(['migrate', tickRoot]);
216
+ assert(tickMig.status === 0, 'tick pending 夹具账本可创建');
217
+ seedPendingWorkflowPair(tickRoot, tickId);
218
+ const tickRejectBefore = workflowSummary(tickRoot, tickId);
219
+ const tickReject = cli(['tick', tickRoot, '--tenant', 'tenant-b']);
220
+ const tickRejectAfter = workflowSummary(tickRoot, tickId);
221
+ assert(tickReject.status !== 0 && /仅支持 --tenant local/.test(tickReject.stderr ?? '') && tickRejectBefore === tickRejectAfter, '非 local tick 拒绝后 local/B workflow/step/audit 均不变');
222
+ const tickRun = cli(['tick', tickRoot]);
223
+ const tickRows = workflowRows(tickRoot, tickId);
224
+ const localRun = rowFor(tickRows.runs, 'local');
225
+ const tenantRun = rowFor(tickRows.runs, 'tenant-b');
226
+ const localStep = rowFor(tickRows.steps, 'local');
227
+ const tenantStep = rowFor(tickRows.steps, 'tenant-b');
228
+ assert(tickRun.status === 0 && /工单 dp-cli-tick-e2e 已回收/.test(tickRun.stdout ?? ''), 'local tick 真实回收 pending 工单');
229
+ assert(localRun?.status === 'settled' && tenantRun?.status === 'pending', 'local tick 只结算 local run,tenant-b 同 id 保持 pending');
230
+ assert(localStep?.name === 'solve' && localStep.status === 'done' && !tenantStep, 'local tick 只写 local solve step');
231
+ assert(tickRows.events.some(e => e.tenant_id === 'local' && e.kind === 'async.settled') && !tickRows.events.some(e => e.tenant_id === 'tenant-b' && (e.kind === 'async.settled' || e.kind === 'async.failed')), 'local tick 只写 local 终态审计事件');
232
+ assert(existsSync(join(tickRoot, 'gotry-state', 'async', `${tickId}.deliverable.md`)), 'local tick 写入隔离 root 的 deliverable 视图');
233
+ // tenant 读范围与合法顺序等价:直接 SQL 种 tenant fixture,不依赖并行 #224 的写路径修复。
234
+ const tenantRoot = mkdtempSync(join(tmpdir(), 'gotry-cli-tenant-'));
235
+ const tenantMig = cli(['migrate', tenantRoot]);
236
+ assert(tenantMig.status === 0, 'tenant scope 夹具账本可创建');
237
+ seedTenantFixture(tenantRoot, 'tenant-a');
238
+ const statsTenantA = cli(['stats', tenantRoot, '--tenant', 'tenant-a']);
239
+ const statsTenantB = cli(['--tenant', 'tenant-a', '--state-root', tenantRoot, 'stats']);
240
+ assert(statsTenantA.status === 0 && statsTenantA.stdout === statsTenantB.stdout && /events=1/.test(statsTenantA.stdout ?? '') && /pendingWrites=1/.test(statsTenantA.stdout ?? ''), 'stats tenant scope 参数顺序等价且只读 tenant-a');
241
+ const pwTenant = cli(['pw-list', '--state-root', tenantRoot, '--tenant', 'tenant-a']);
242
+ const pwLocal = cli(['pw-list', '--state-root', tenantRoot]);
243
+ assert(/tenant-pw-1/.test(pwTenant.stdout ?? '') && !/tenant-pw-1/.test(pwLocal.stdout ?? ''), 'pw-list tenant scope 不串到默认 local');
244
+ // flag 值不得成为业务参数:同一合法调用多种顺序登记到指定 tenant,idemKey 不被 tenant/root 值串位。
245
+ const orderRoot = mkdtempSync(join(tmpdir(), 'gotry-cli-order-'));
246
+ const orderReqA = cli(['pw-request', '--tenant', 'tenant-order', '--state-root', orderRoot, 'order-key-a', 'flight-order-confirm', '{"flight":"MU456"}']);
247
+ const orderReqB = cli(['--state-root', orderRoot, 'pw-request', 'order-key-b', '--tenant', 'tenant-order', 'flight-order-confirm', '{"flight":"MU789"}']);
248
+ const orderList = cli(['pw-list', '--state-root', orderRoot, '--tenant', 'tenant-order']);
249
+ assert(orderReqA.status === 0 && orderReqB.status === 0 && /order-key-a/.test(orderList.stdout ?? '') && /order-key-b/.test(orderList.stdout ?? '') && !/tenant-order\s+seam=/.test(orderList.stdout ?? ''), 'pw-request 合法参数前后顺序等价且 flag 值不串业务参数');
250
+ rmSync(root, { recursive: true, force: true });
251
+ rmSync(root2, { recursive: true, force: true });
252
+ rmSync(parseRoot, { recursive: true, force: true });
253
+ rmSync(numericParent, { recursive: true, force: true });
254
+ rmSync(guardRoot, { recursive: true, force: true });
255
+ rmSync(missingBase, { recursive: true, force: true });
256
+ rmSync(tickRoot, { recursive: true, force: true });
257
+ rmSync(tenantRoot, { recursive: true, force: true });
258
+ rmSync(orderRoot, { recursive: true, force: true });
132
259
  console.log(`\nSTATE-CLI TESTS: ${pass} ok, ${fail} fail`);
133
- if (fail > 0) process.exit(1);
134
-
135
-
136
- //# sourceURL=ts/scripts/state-cli-tests.ts
260
+ if (fail > 0)
261
+ process.exit(1);