@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,175 +1,483 @@
1
+ /**
2
+ * doctor 能力层测试(离线,注入 repoRoot/homeDir/env,零安装零网络写路径):
3
+ * 1. 空 tmp 环境 → agent-reach=missing / 扩展=missing / flyai=degraded(无 key)/ LLM key 恒 ok 且不进 broken
4
+ * 2. 补齐假 .venv 双文件 → agent-reach=ok;只补 python → degraded(半可用态被显式区分)
5
+ * 3. FLYAI_API_KEY 注入 → flyai=ok
6
+ * 4. nodeOk 边界(22.14/22.15/23.0)
7
+ * 5. renderDoctorReportMd:命令类 fix 加反引号、prose 类不加;报告含全部条目
8
+ * 6. MCP 工具面:gotry_doctor 注册可执行,isolated stateRoot 落 doctor-report.md
9
+ *
10
+ * issue #284 自助修复(repair action,7 段):
11
+ * 7. read-only call 不安装:action 缺省/=diagnose 时零调用 installer,verdict 保持旧口形
12
+ * 8. 允许修复 → 只跑 auto-repairable → 复检决定 installed/failed
13
+ * 9. 安装器报 ok 但复检仍坏 → failed + nextAction = doctor --fix
14
+ * 10. 同 agent + 同 scope 复用审批;不同 scope 重问
15
+ * 11. rejected/cancelled + unavailable 通道:零执行 + 结构化 verdict='approval-denied'
16
+ * 12. user-action 项绝不安装;scope 过滤只考虑 items 范围内 id
17
+ * 13. tool-level gotry_doctor 注册 + observable chain(diagnose → repair)
18
+ *
19
+ * 运行: cd ts && npx tsx scripts/doctor-tests.ts
20
+ */
1
21
  import assert from 'node:assert/strict';
2
22
  import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
3
23
  import { tmpdir } from 'node:os';
4
24
  import { dirname, join } from 'node:path';
5
- import { runDoctorChecks, renderDoctorReportMd, nodeOk } from '../capabilities/doctor.js';
25
+ import { runDoctorChecks, renderDoctorReportMd, nodeOk, scopeKeyFor, createRepairApprovalGate, runDoctorRepair } from '../capabilities/doctor.js';
6
26
  import { apply } from '../src/index.js';
7
27
  const tmp = await mkdtemp(join(tmpdir(), 'gotry-doctor-test-'));
8
28
  const emptyRepo = join(tmp, 'repo-empty');
9
29
  const emptyHome = join(tmp, 'home-empty');
10
- await mkdir(emptyRepo, {
11
- recursive: true
12
- });
13
- await mkdir(emptyHome, {
14
- recursive: true
15
- });
16
- const r1 = await runDoctorChecks({
17
- repoRoot: emptyRepo,
18
- homeDir: emptyHome,
19
- env: {}
20
- });
21
- const byId = (id)=>r1.items.find((i)=>i.id === id);
30
+ await mkdir(emptyRepo, { recursive: true });
31
+ await mkdir(emptyHome, { recursive: true });
32
+ // 1. 空 tmp 环境:全部缺省态;LLM key 让渡面不进 broken 计数
33
+ const r1 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: {} });
34
+ const byId = (id) => r1.items.find(i => i.id === id);
22
35
  assert.ok(byId('agent-reach'), 'agent-reach 项存在');
23
36
  assert.equal(byId('agent-reach').status, 'missing', `空仓 agent-reach=missing,实际 ${byId('agent-reach').status}`);
24
- assert.match(byId('agent-reach').fix ?? '', /npx gotry doctor --fix/, 'missing 项带精确补装指引');
37
+ assert.match(byId('agent-reach').fix ?? '', /npx @danceiny\/gotry doctor --fix/, 'missing 项带精确补装指引');
25
38
  assert.equal(byId('extension').status, 'missing', '空 home 扩展=missing');
26
39
  assert.equal(byId('flyai').status, 'degraded', '无 key flyai=degraded(有共享试用,非缺失)');
27
40
  assert.equal(byId('llm-key').status, 'ok', 'LLM key 恒 ok(让渡面)');
28
41
  assert.equal(r1.ok, false, '有 missing 项时报告 ok=false');
29
42
  assert.match(r1.summary, /待处理/, 'summary 指明待处理项');
30
43
  console.log('1. 空 tmp 环境 → missing/degraded 分级正确,LLM key 让渡 OK');
44
+ // 2. 假 .venv:双文件齐 → ok;只 python → degraded
31
45
  const repoPartial = join(tmp, 'repo-partial');
32
- await mkdir(join(repoPartial, '.venv/bin'), {
33
- recursive: true
34
- });
35
- await writeFile(join(repoPartial, '.venv/bin/python'), '#!/bin/sh\n', {
36
- mode: 0o755
37
- });
38
- const r2a = await runDoctorChecks({
39
- repoRoot: repoPartial,
40
- homeDir: emptyHome,
41
- env: {}
42
- });
43
- assert.equal(r2a.items.find((i)=>i.id === 'agent-reach').status, 'degraded', '只 python → degraded(半可用显式区分)');
46
+ await mkdir(join(repoPartial, '.venv/bin'), { recursive: true });
47
+ await writeFile(join(repoPartial, '.venv/bin/python'), '#!/bin/sh\n', { mode: 0o755 });
48
+ const r2a = await runDoctorChecks({ repoRoot: repoPartial, homeDir: emptyHome, env: {} });
49
+ assert.equal(r2a.items.find(i => i.id === 'agent-reach').status, 'degraded', '只 python → degraded(半可用显式区分)');
44
50
  const repoFull = join(tmp, 'repo-full');
45
- await mkdir(join(repoFull, '.venv/bin'), {
46
- recursive: true
47
- });
48
- await writeFile(join(repoFull, '.venv/bin/python'), '#!/bin/sh\n', {
49
- mode: 0o755
50
- });
51
- await writeFile(join(repoFull, '.venv/bin/agent-reach'), '#!/bin/sh\n', {
52
- mode: 0o755
53
- });
54
- const r2b = await runDoctorChecks({
55
- repoRoot: repoFull,
56
- homeDir: emptyHome,
57
- env: {}
58
- });
59
- assert.equal(r2b.items.find((i)=>i.id === 'agent-reach').status, 'ok', 'python+agent-reach 齐 → ok');
51
+ await mkdir(join(repoFull, '.venv/bin'), { recursive: true });
52
+ await writeFile(join(repoFull, '.venv/bin/python'), '#!/bin/sh\n', { mode: 0o755 });
53
+ await writeFile(join(repoFull, '.venv/bin/agent-reach'), '#!/bin/sh\n', { mode: 0o755 });
54
+ const r2b = await runDoctorChecks({ repoRoot: repoFull, homeDir: emptyHome, env: {} });
55
+ assert.equal(r2b.items.find(i => i.id === 'agent-reach').status, 'ok', 'python+agent-reach 齐 → ok');
60
56
  console.log('2. .venv 三态(missing/degraded/ok)分级 OK');
61
- const r3 = await runDoctorChecks({
62
- repoRoot: emptyRepo,
63
- homeDir: emptyHome,
64
- env: {
65
- FLYAI_API_KEY: 'test-key'
66
- }
67
- });
68
- assert.equal(r3.items.find((i)=>i.id === 'flyai').status, 'ok', '有正式 key → flyai=ok');
57
+ // 3. FLYAI_API_KEY 注入 → ok
58
+ const r3 = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: emptyHome, env: { FLYAI_API_KEY: 'test-key' } });
59
+ assert.equal(r3.items.find(i => i.id === 'flyai').status, 'ok', '有正式 key → flyai=ok');
69
60
  console.log('3. FLYAI_API_KEY 注入 → flyai ok OK');
61
+ // 4. nodeOk 边界
70
62
  assert.equal(nodeOk('22.14.0'), false, '22.14 不足');
71
63
  assert.equal(nodeOk('22.15.0'), true, '22.15 达标');
72
64
  assert.equal(nodeOk('23.0.0'), true, '23.x 达标');
73
65
  console.log('4. nodeOk 边界 OK');
66
+ // 5. 报告渲染:命令类反引号、prose 类原样
74
67
  const md = renderDoctorReportMd(r1);
75
68
  assert.match(md, /# GoTry 依赖体检报告/, '报告标题');
76
- assert.match(md, /`npx gotry doctor --fix`/, '命令类 fix 加反引号');
69
+ assert.match(md, /`npx @danceiny\/gotry doctor --fix`/, '命令类 fix 加反引号');
77
70
  assert.match(md, /flyai\.open\.fliggy\.com 控制台申请正式 key/, 'prose 类 fix 原样呈现');
78
71
  assert.ok(!md.includes('`到 flyai'), 'prose 类 fix 不应整体包反引号');
79
72
  assert.match(md, /LLM key/, '让渡面(LLM key)照常入表');
80
73
  console.log('5. 报告渲染 OK');
74
+ // 5b. npm/npx 提升布局(2026-09-08 安装链修复):gotry 的依赖落在包目录外的同级提升位,
75
+ // existsSync 硬编码候选在该布局全 miss——map/ask-user 必须经 createRequire 解析链
76
+ // 命中(此前把真实就位的 ask-user 误报 missing、map-tools 误报随包缺席)。
81
77
  {
82
78
  const hoist = await mkdtemp(join(tmpdir(), 'gotry-doctor-hoist-'));
83
79
  const pkgRoot = join(hoist, 'node_modules', '@danceiny', 'gotry');
84
- await mkdir(pkgRoot, {
85
- recursive: true
86
- });
87
- await writeFile(join(pkgRoot, 'package.json'), JSON.stringify({
88
- name: '@danceiny/gotry',
89
- version: '0.0.1-test'
90
- }), 'utf-8');
91
- const stubPkg = async (dir, main)=>{
92
- await mkdir(join(hoist, 'node_modules', dir, 'lib'), {
93
- recursive: true
94
- });
95
- await writeFile(join(hoist, 'node_modules', dir, 'package.json'), JSON.stringify({
96
- name: dir,
97
- version: '0.0.0-test',
98
- main
99
- }), 'utf-8');
80
+ await mkdir(pkgRoot, { recursive: true });
81
+ await writeFile(join(pkgRoot, 'package.json'), JSON.stringify({ name: '@danceiny/gotry', version: '0.0.1-test' }), 'utf-8');
82
+ // 提升位依赖(与真实 npx 缓存同形):裸名 map-tools + dsh 本体 + ask-user
83
+ const stubPkg = async (dir, main) => {
84
+ await mkdir(join(hoist, 'node_modules', dir, 'lib'), { recursive: true });
85
+ await writeFile(join(hoist, 'node_modules', dir, 'package.json'), JSON.stringify({ name: dir, version: '0.0.0-test', main }), 'utf-8');
100
86
  await writeFile(join(hoist, 'node_modules', dir, main), '// stub\n', 'utf-8');
101
87
  };
102
88
  await stubPkg('dsh-map-tools', 'lib/index.js');
103
89
  await stubPkg('@deepseek-ai/dsh', 'lib/bin.js');
104
90
  await stubPkg('@deepseek-ai/dsh-tool-ask-user', 'lib/index.js');
105
- const rb = await runDoctorChecks({
106
- repoRoot: pkgRoot,
107
- homeDir: join(hoist, 'home-empty'),
108
- env: {}
109
- });
110
- assert.equal(rb.items.find((i)=>i.id === 'map-tools').status, 'ok', '提升布局 map-tools 经包根解析链命中');
111
- assert.equal(rb.items.find((i)=>i.id === 'ask-user').status, 'ok', '提升布局 ask-user dsh 上下文解析链命中(误报修复)');
112
- await rm(hoist, {
113
- recursive: true,
114
- force: true
115
- });
116
- }console.log('5b. npm 提升布局解析链(map-tools/ask-user 误报修复)OK');
91
+ const rb = await runDoctorChecks({ repoRoot: pkgRoot, homeDir: join(hoist, 'home-empty'), env: {} });
92
+ assert.equal(rb.items.find(i => i.id === 'map-tools').status, 'ok', '提升布局 map-tools 经包根解析链命中');
93
+ assert.equal(rb.items.find(i => i.id === 'ask-user').status, 'ok', '提升布局 ask-user 经 dsh 上下文解析链命中(误报修复)');
94
+ await rm(hoist, { recursive: true, force: true });
95
+ }
96
+ console.log('5b. npm 提升布局解析链(map-tools/ask-user 误报修复)OK');
97
+ // 5c. 随包 vendor 布局:tarball ts/dsh-runtime/vendor/dsh-map-tools 直接命中
98
+ // (npm 依赖形态因 peerDependencies ERESOLVE 不可用,vendor 是 npm 布局的分发面)
117
99
  {
118
100
  const vend = await mkdtemp(join(tmpdir(), 'gotry-doctor-vendor-'));
119
101
  const vendorEntry = join(vend, 'ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js');
120
- await mkdir(dirname(vendorEntry), {
121
- recursive: true
122
- });
102
+ await mkdir(dirname(vendorEntry), { recursive: true });
123
103
  await writeFile(vendorEntry, '// stub\n', 'utf-8');
124
- const rc = await runDoctorChecks({
125
- repoRoot: vend,
126
- homeDir: join(vend, 'home-empty'),
127
- env: {}
128
- });
129
- assert.equal(rc.items.find((i)=>i.id === 'map-tools').status, 'ok', 'vendor 副本命中(map-tools tarball 分发)');
130
- await rm(vend, {
131
- recursive: true,
132
- force: true
133
- });
134
- }console.log('5c. 随包 vendor 布局(map-tools tarball 分发面)OK');
104
+ const rc = await runDoctorChecks({ repoRoot: vend, homeDir: join(vend, 'home-empty'), env: {} });
105
+ assert.equal(rc.items.find(i => i.id === 'map-tools').status, 'ok', 'vendor 副本命中(map-tools 随 tarball 分发)');
106
+ await rm(vend, { recursive: true, force: true });
107
+ }
108
+ console.log('5c. 随包 vendor 布局(map-tools tarball 分发面)OK');
109
+ // 6. MCP 工具面:gotry_doctor 注册 + isolated stateRoot 报告落盘
135
110
  const smokeRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-state-'));
136
111
  const registered = [];
137
112
  const ctx = {
138
- tools: {
139
- register: (t)=>registered.push(t)
140
- },
141
- systemPrompt: {
142
- variable: ()=>{}
143
- },
144
- on: ()=>()=>{}
113
+ tools: { register: (t) => registered.push(t) },
114
+ systemPrompt: { variable: () => { } },
115
+ on: () => () => { },
145
116
  };
146
- apply(ctx, {
147
- stateRoot: smokeRoot,
148
- timeoutMs: 30_000,
149
- hbcliBin: 'hbcli-not-on-path',
150
- sessionAccess: 'ask'
151
- });
152
- const doctorTool = registered.find((t)=>t.name === 'gotry_doctor');
117
+ apply(ctx, { stateRoot: smokeRoot, timeoutMs: 30_000, hbcliBin: 'hbcli-not-on-path', sessionAccess: 'ask' });
118
+ const doctorTool = registered.find(t => t.name === 'gotry_doctor');
153
119
  assert.ok(doctorTool, 'gotry_doctor 已注册');
154
- const out = await doctorTool.execute({
155
- query: {}
156
- }, null);
120
+ const out = await doctorTool.execute({ query: {} }, null);
157
121
  assert.equal(out.ok, true, '体检工具 ok=true(体检本身成功,与依赖是否缺失无关)');
158
122
  assert.equal(out.verdict, 'needs-attention', '注入环境外仍能出 verdict(本机真实状态,不判 all-clear)');
159
123
  assert.ok(Array.isArray(out.items) && out.items.length >= 6, 'items 齐全');
160
124
  assert.ok(out.report_path && out.report_path.includes(smokeRoot), `报告落在 isolated stateRoot: ${out.report_path}`);
161
125
  const reportMd = await readFile(out.report_path, 'utf-8');
162
126
  assert.match(reportMd, /# GoTry 依赖体检报告/, '落盘报告可预览');
163
- await rm(tmp, {
164
- recursive: true,
165
- force: true
166
- });
167
- await rm(smokeRoot, {
168
- recursive: true,
169
- force: true
170
- });
127
+ await rm(tmp, { recursive: true, force: true });
128
+ await rm(smokeRoot, { recursive: true, force: true });
171
129
  console.log('6. gotry_doctor 工具面 + 报告落盘 OK');
130
+ // ---------------------------------------------------------------------------
131
+ // issue #284 自助修复测试(7 段),全部注入 fake installer / recheck / approval,
132
+ // 不跑真安装面、不动 ~/.gotry、不写 shared state。
133
+ // ---------------------------------------------------------------------------
134
+ // 合成 doctor items(与 bootstrap-tests §12 onboardingItems 同形态,稳定 id 锚点)。
135
+ const repairItems = [
136
+ { id: 'node', label: 'Node 运行时', status: 'ok', detail: 'Node 22.x' },
137
+ { id: 'extension', label: 'GoTry Session Bridge 扩展', status: 'missing', detail: '未安装', fix: 'https://chromewebstore...' },
138
+ { id: 'agent-reach', label: 'Agent Reach(网页/社媒读取)', status: 'missing', detail: '未装配', fix: 'npx @danceiny/gotry doctor --fix' },
139
+ { id: 'hbcli', label: 'hbcli(酒店实时源)', status: 'missing', detail: '未安装', fix: 'npx @danceiny/gotry doctor --fix' },
140
+ { id: 'flyai', label: 'FlyAI(飞猪官方检索)', status: 'degraded', detail: '未配 FLYAI_API_KEY', fix: '到 flyai 控制台申请 key' },
141
+ { id: 'sidebar', label: 'dsh-better-sidebar(侧栏工作台)', status: 'missing', detail: '未安装', fix: 'npx @danceiny/gotry doctor --fix' },
142
+ { id: 'calendar', label: 'dsh-calendar(日历工作窗口)', status: 'degraded', detail: '已挂载未配置', fix: 'cordis.patch.yml 覆盖 config' },
143
+ { id: 'map-tools', label: 'dsh-map-tools(地图/路线/POI)', status: 'missing', detail: '未随包解析', fix: '重装 @danceiny/gotry' },
144
+ { id: 'ask-user', label: 'dsh-tool-ask-user(结构化澄清卡)', status: 'missing', detail: '未解析', fix: '重装 @danceiny/gotry' },
145
+ { id: 'llm-key', label: 'LLM key', status: 'ok', detail: '由 dsh 宿主管' },
146
+ ];
147
+ // fakeRecheck helper: convert DoctorItem[] → BootstrapItem[] (level: 优先取合并字段,否则 status)
148
+ const toBootstrapItems = (items) => items.map((i) => ({ label: i.label, level: i.level ?? i.status, detail: i.detail, fix: i.fix }));
149
+ // 假分类器桥(用于 §12 内的边界测试,生产路径用真 bootstrap.classifyDoctorGap 在 §7/§8 断言)。
150
+ function fakeBridge(env = {}) {
151
+ return {
152
+ classifyDoctorGap: (i, opts) => {
153
+ const e = opts?.env ?? env;
154
+ if (!i || i.level === 'ok')
155
+ return 'ok';
156
+ const label = i.label ?? '';
157
+ if (label.startsWith('Agent Reach'))
158
+ return e.GOTRY_SETUP_REACH === '0' ? 'unavailable' : 'auto';
159
+ if (label.startsWith('hbcli'))
160
+ return i.level === 'degraded' ? 'user-action' : (e.GOTRY_SETUP_HBCLI === '0' ? 'unavailable' : 'auto');
161
+ if (label.startsWith('dsh-better-sidebar'))
162
+ return e.GOTRY_SETUP_SIDEBAR === '0' ? 'unavailable' : 'auto';
163
+ if (label.startsWith('GoTry Session Bridge'))
164
+ return 'user-action';
165
+ if (label.startsWith('FlyAI'))
166
+ return 'user-action';
167
+ if (label.startsWith('dsh-calendar'))
168
+ return 'user-action';
169
+ if (label.startsWith('dsh-map-tools'))
170
+ return 'unavailable';
171
+ if (label.startsWith('dsh-tool-ask-user'))
172
+ return 'unavailable';
173
+ if (label.startsWith('Node'))
174
+ return 'unavailable';
175
+ return 'unavailable';
176
+ },
177
+ buildOnboardingPlan: (items, _opts) => {
178
+ const auto = [];
179
+ const userAction = [];
180
+ const unavailable = [];
181
+ for (const i of items ?? []) {
182
+ const b = fakeBridge(env).classifyDoctorGap(i, { env });
183
+ if (b === 'ok')
184
+ continue;
185
+ if (b === 'auto')
186
+ auto.push(i);
187
+ else if (b === 'user-action')
188
+ userAction.push(i);
189
+ else
190
+ unavailable.push(i);
191
+ }
192
+ return { promptable: auto.length > 0, auto, userAction, unavailable };
193
+ },
194
+ };
195
+ }
196
+ // 7. scopeKeyFor 纯函数 + runDoctorRepair 走真 bootstrap.classifyDoctorGap/buildOnboardingPlan
197
+ // (regression for #284 blocker #2:直传 DoctorItem 会让 classifyDoctorGap 因 !item.level
198
+ // 把所有项分到 ok,plan 永远空。runDoctorRepair 内部调 toBootstrapItem 修复此点。)
199
+ {
200
+ assert.equal(scopeKeyFor([]), '', '空 scope = 空 key');
201
+ assert.equal(scopeKeyFor(['a']), 'a', '单元素 scope');
202
+ assert.equal(scopeKeyFor(['b', 'a', 'a']), 'a,b', 'scope 排序去重(批准缓存命中)');
203
+ const fakeInstallers = {
204
+ hbcli: async () => ({ ok: true }),
205
+ reach: async () => ({ ok: true }),
206
+ sidebar: async () => ({ ok: true }),
207
+ };
208
+ const fakeRecheck = async () => toBootstrapItems(repairItems.map((i) => ({ ...i, level: i.id === 'agent-reach' || i.id === 'hbcli' || i.id === 'sidebar' ? 'ok' : i.status })));
209
+ const fakeReport = { ok: false, items: repairItems, summary: '合成' };
210
+ const fakeAgent = { id: 'test-agent-7' };
211
+ const gate = createRepairApprovalGate({
212
+ approval: () => ({ request: async () => 'allowed-once' }),
213
+ });
214
+ const r = await runDoctorRepair(fakeReport, (k, _reason) => gate.request(fakeAgent, k, _reason), {
215
+ bridge: fakeBridge(), installers: fakeInstallers, recheck: fakeRecheck,
216
+ });
217
+ // selected: 3 项 auto(agent-reach / hbcli / sidebar)—**这是 status→level 适配生效的证据**
218
+ assert.equal(r.repairs.length, 3, '3 项 auto selected(真 buildOnboardingPlan 正确分桶)');
219
+ assert.deepEqual(r.repairs.map((x) => x.id).sort(), ['agent-reach', 'hbcli', 'sidebar']);
220
+ assert.ok(r.repairs.every((x) => x.status === 'installed'), 'recheck 全 ok → 全 installed');
221
+ assert.equal(r.skipped.length, 5, 'skipped = 3 user-action + 2 unavailable');
222
+ assert.equal(r.verdict, 'repaired');
223
+ }
224
+ console.log('7. scopeKeyFor + status→level 适配(plan 真实非空)OK');
225
+ // 8. **生产路径 regression**(#284 blocker #1):不传 bridge.runOnboardingFix 时,runDoctorRepair
226
+ // 走 bootstrap.runOnboardingFix 真导出。**直接 invoke 真 runOnboardingFix** 注入 fake installers/recheck
227
+ // 证明"production 路径真的会调到 bootstrap.runOnboardingFix"+"实际 installers 被触发 + recheck 决定结果"。
228
+ // 旧的 fake-classifier-only 测试无法发现"production 路径根本没接 installers"这种 bug。
229
+ {
230
+ const url = new URL('../../bin/gotry-bootstrap.js', import.meta.url);
231
+ const realMod = await import(url.href);
232
+ const installerCalls = [];
233
+ const bridgeInstallers = {
234
+ hbcli: async () => { installerCalls.push('hbcli'); return { ok: true }; },
235
+ reach: async () => { installerCalls.push('reach'); return { ok: true }; },
236
+ sidebar: async () => { installerCalls.push('sidebar'); return { ok: true }; },
237
+ };
238
+ const fakeRecheck = async () => toBootstrapItems(repairItems.map((i) => ({ ...i, level: i.id === 'agent-reach' || i.id === 'hbcli' || i.id === 'sidebar' ? 'ok' : i.status })));
239
+ const fakeReport = { ok: false, items: repairItems, summary: '合成' };
240
+ const fakeAgent = { id: 'test-agent-8' };
241
+ const gate = createRepairApprovalGate({
242
+ approval: () => ({ request: async () => 'allowed-once' }),
243
+ });
244
+ const r = await runDoctorRepair(fakeReport, (k, _reason) => gate.request(fakeAgent, k, _reason), {
245
+ runOnboardingFix: realMod.runOnboardingFix,
246
+ installers: bridgeInstallers,
247
+ recheck: fakeRecheck,
248
+ bridge: { classifyDoctorGap: realMod.classifyDoctorGap, buildOnboardingPlan: realMod.buildOnboardingPlan },
249
+ });
250
+ assert.deepEqual(installerCalls.slice().sort(), ['hbcli', 'reach', 'sidebar'], '**production 路径真调到 bootstrap.runOnboardingFix**(三个 auto installer 各被触发一次)');
251
+ assert.equal(r.verdict, 'repaired');
252
+ assert.equal(r.approval, 'granted');
253
+ assert.ok(r.repairs.every((x) => x.status === 'installed'), '3 项全 installed');
254
+ }
255
+ console.log('8. production 路径(bootstrap.runOnboardingFix 真导出 + 注入 installers/recheck → 真实触发 + recheck 决定结果)OK');
256
+ // 9. installer 报 ok 但 recheck 仍坏 → failed + nextAction = doctor --fix(verdict 不冒充 repaired)
257
+ {
258
+ const url = new URL('../../bin/gotry-bootstrap.js', import.meta.url);
259
+ const realMod = await import(url.href);
260
+ const bridgeInstallers = {
261
+ hbcli: async () => ({ ok: true }),
262
+ reach: async () => ({ ok: true }),
263
+ sidebar: async () => ({ ok: true }),
264
+ };
265
+ const fakeRecheck = async () => toBootstrapItems(repairItems.map((i) => ({ ...i, level: i.id === 'hbcli' ? i.status : 'ok' })));
266
+ const fakeReport = { ok: false, items: repairItems, summary: '合成' };
267
+ const fakeAgent = { id: 'test-agent-9' };
268
+ const gate = createRepairApprovalGate({
269
+ approval: () => ({ request: async () => 'allowed-once' }),
270
+ });
271
+ const r = await runDoctorRepair(fakeReport, (k, _r2) => gate.request(fakeAgent, k, _r2), {
272
+ runOnboardingFix: realMod.runOnboardingFix,
273
+ installers: bridgeInstallers, recheck: fakeRecheck,
274
+ bridge: { classifyDoctorGap: realMod.classifyDoctorGap, buildOnboardingPlan: realMod.buildOnboardingPlan },
275
+ });
276
+ assert.equal(r.verdict, 'partial-repair', '一项失败 → partial-repair(诚实,不冒充 repaired)');
277
+ const hbcli = r.repairs.find((x) => x.id === 'hbcli');
278
+ assert.equal(hbcli.status, 'failed');
279
+ assert.match(hbcli.nextAction ?? '', /doctor --fix/, '失败给可执行重试命令');
280
+ assert.equal(r.repairs.find((x) => x.id === 'agent-reach').status, 'installed');
281
+ }
282
+ console.log('9. installer 报 ok 但 recheck 仍坏 → failed + retry 命令,partial-repair 不冒充 OK');
283
+ // 10. 同 agent + 同 scope 复用;不同 scope 重问;rejected/cancelled 在缓存里分开
284
+ {
285
+ const agent = { id: 'test-agent-10' };
286
+ const scopeA = 'agent-reach,sidebar';
287
+ const scopeB = 'hbcli,sidebar';
288
+ let requestCount = 0;
289
+ const gate = createRepairApprovalGate({
290
+ approval: () => ({ request: async () => { requestCount += 1; return 'allowed-once'; } }),
291
+ });
292
+ await gate.request(agent, scopeA, 'A1');
293
+ await gate.request(agent, scopeA, 'A2');
294
+ assert.equal(requestCount, 1, '同 agent + 同 scope 复用(只 1 次请求)');
295
+ await gate.request(agent, scopeB, 'B1');
296
+ assert.equal(requestCount, 2, '不同 scope 重问(共 2 次请求)');
297
+ // rejected/cancelled 在缓存里分开记(has 返回具体 outcome,不合并)
298
+ const cancelGate = createRepairApprovalGate({
299
+ approval: () => ({ request: async () => 'cancelled' }),
300
+ });
301
+ const agent2 = { id: 'agent-cancel' };
302
+ const scopeC = 'hbcli';
303
+ const r1 = await cancelGate.request(agent2, scopeC, 'r1');
304
+ const r2 = await cancelGate.request(agent2, scopeC, 'r2');
305
+ assert.equal(r1, 'cancelled', '首次 cancelled → cancelled');
306
+ assert.equal(r2, 'cancelled', '同 scope 后续请求复用 cancelled,不合并成 rejected');
307
+ const rejGate = createRepairApprovalGate({
308
+ approval: () => ({ request: async () => 'rejected' }),
309
+ });
310
+ assert.equal(await rejGate.request(agent2, 'sidebar', 'r3'), 'rejected');
311
+ }
312
+ console.log('10. 同 agent + 同 scope 复用;不同 scope 重问;rejected/cancelled 分开记 OK');
313
+ // 11. rejected/cancelled + unavailable 通道:零执行 + verdict='approval-denied'
314
+ {
315
+ const fakeReport = { ok: false, items: repairItems, summary: '合成' };
316
+ const fakeAgent = { id: 'test-agent-11' };
317
+ for (const outcome of ['rejected', 'cancelled']) {
318
+ const gate = createRepairApprovalGate({
319
+ approval: () => ({ request: async () => outcome }),
320
+ });
321
+ const r = await runDoctorRepair(fakeReport, (k, _r2) => gate.request(fakeAgent, k, _r2), {
322
+ bridge: fakeBridge(),
323
+ installers: { hbcli: async () => { throw new Error('should not run'); }, reach: async () => { throw new Error('should not run'); }, sidebar: async () => { throw new Error('should not run'); } },
324
+ recheck: async () => toBootstrapItems(repairItems),
325
+ });
326
+ assert.equal(r.verdict, 'approval-denied', `${outcome} → approval-denied`);
327
+ assert.equal(r.ok, false);
328
+ assert.equal(r.approval, outcome);
329
+ assert.equal(r.repairs.length, 0, `${outcome} → 零执行(零 repairs)`);
330
+ assert.deepEqual(r.selected.map((i) => i.id), ['agent-reach', 'hbcli', 'sidebar'], `${outcome} 时仍保留待批准计划`);
331
+ assert.equal(r.scopeKey, 'agent-reach,hbcli,sidebar', `${outcome} 时仍保留审计 scope`);
332
+ assert.ok(r.summary.includes('拒绝') || r.summary.includes('吊销'), 'summary 含拒绝/吊销原因');
333
+ }
334
+ // unavailable:approval seam 缺席
335
+ {
336
+ const gate = createRepairApprovalGate({ approval: () => undefined });
337
+ const r = await runDoctorRepair(fakeReport, (k, _r2) => gate.request(fakeAgent, k, _r2), {
338
+ bridge: fakeBridge(),
339
+ installers: { hbcli: async () => ({ ok: true }), reach: async () => ({ ok: true }), sidebar: async () => ({ ok: true }) },
340
+ recheck: async () => toBootstrapItems(repairItems),
341
+ });
342
+ assert.equal(r.verdict, 'approval-denied');
343
+ assert.equal(r.approval, 'unavailable', 'approval seam 缺席 → unavailable');
344
+ assert.equal(r.repairs.length, 0, 'unavailable → 零执行');
345
+ assert.equal(r.selected.length, 3, 'unavailable 时仍返回选定计划');
346
+ assert.equal(r.scopeKey, 'agent-reach,hbcli,sidebar', 'unavailable 时仍返回 scope');
347
+ assert.ok(r.summary.includes('无可用审批通道'), 'summary 显式说明通道缺席');
348
+ }
349
+ }
350
+ console.log('11. rejected/cancelled + unavailable 通道 → 零执行 + verdict=approval-denied OK');
351
+ // 12. user-action / unavailable 项绝不安装(scope 过滤);全健康早退;未知 id 静默剔除
352
+ {
353
+ const fakeReport = { ok: false, items: repairItems, summary: '合成' };
354
+ const calls = [];
355
+ const fakeInstallers = {
356
+ hbcli: async () => { calls.push('hbcli'); return { ok: true }; },
357
+ reach: async () => { calls.push('reach'); return { ok: true }; },
358
+ sidebar: async () => { calls.push('sidebar'); return { ok: true }; },
359
+ };
360
+ const recheckItems = repairItems.map((i) => i.id === 'agent-reach' || i.id === 'hbcli' || i.id === 'sidebar' ? { ...i, level: 'ok' } : i);
361
+ const fakeAgent = { id: 'test-agent-12' };
362
+ const gate = createRepairApprovalGate({
363
+ approval: () => ({ request: async () => 'allowed-once' }),
364
+ });
365
+ // scope 限制:agent-reach(可装)+ flyai/calendar(用户操作)
366
+ const scopedReport = {
367
+ ...fakeReport,
368
+ items: repairItems.filter((i) => i.id === 'agent-reach' || i.id === 'flyai' || i.id === 'calendar'),
369
+ };
370
+ const r = await runDoctorRepair(scopedReport, (k, _r2) => gate.request(fakeAgent, k, _r2), {
371
+ bridge: fakeBridge(), installers: fakeInstallers, recheck: async () => recheckItems,
372
+ });
373
+ assert.equal(r.repairs.length, 1, 'scope 限制后只 1 项 auto(agent-reach)');
374
+ assert.equal(r.repairs[0].id, 'agent-reach');
375
+ assert.deepEqual(calls, ['reach'], '只调一次 reach installer;user-action 项绝不装');
376
+ assert.ok(r.skipped.some((s) => s.id === 'flyai'), 'flyai 进 skipped');
377
+ assert.ok(r.skipped.some((s) => s.id === 'calendar'), 'calendar 进 skipped');
378
+ // 全 healthy 报告 → plan 空 → verdict='no-repair-needed'(零审批请求,零安装)
379
+ {
380
+ const allOk = repairItems.map((i) => ({ ...i, status: 'ok' }));
381
+ const r2 = await runDoctorRepair({ ok: true, items: allOk, summary: 'all ok' }, (k, _r3) => gate.request(fakeAgent, k, _r3), {
382
+ bridge: fakeBridge(), installers: fakeInstallers, recheck: async () => allOk,
383
+ });
384
+ assert.equal(r2.verdict, 'no-repair-needed');
385
+ assert.equal(r2.approval, 'not-needed');
386
+ assert.equal(r2.ok, true, '全健康所选范围无需修复且结果成功');
387
+ assert.deepEqual(calls, ['reach'], 'all-ok 早退,installer 零额外调用');
388
+ }
389
+ // 未知 id → 静默剔除
390
+ {
391
+ const weirdReport = {
392
+ ...fakeReport,
393
+ items: [...repairItems, { id: 'bogus-id', label: 'Bogus', status: 'missing', detail: '?' }],
394
+ };
395
+ const r3 = await runDoctorRepair(weirdReport, (k, _r4) => gate.request(fakeAgent, k, _r4), {
396
+ bridge: fakeBridge(), installers: fakeInstallers, recheck: async () => repairItems,
397
+ });
398
+ assert.equal(r3.repairs.length, 3, 'bogus-id 不进 selected');
399
+ assert.ok(!r3.repairs.some((x) => x.id === 'bogus-id'));
400
+ }
401
+ }
402
+ console.log('12. user-action 项绝不装(scope 过滤 + 分类器守住 + 未知 id 静默剔除 + 全健康早退)OK');
403
+ // 13. tool-level gotry_doctor 注册 + observable chain(diagnose + repair 两条路径)
404
+ {
405
+ const smokeRoot2 = await mkdtemp(join(tmpdir(), 'gotry-doctor-repair-'));
406
+ const registered = [];
407
+ const ctx2 = {
408
+ tools: { register: (t) => registered.push(t) },
409
+ systemPrompt: { variable: () => { } },
410
+ on: () => () => { },
411
+ };
412
+ apply(ctx2, { stateRoot: smokeRoot2, timeoutMs: 30_000, hbcliBin: 'hbcli-not-on-path', sessionAccess: 'ask' });
413
+ const doctorTool = registered.find((t) => t.name === 'gotry_doctor');
414
+ assert.ok(doctorTool, 'gotry_doctor 已注册');
415
+ // 13a. read-only 默认路径
416
+ const r1 = await doctorTool.execute({ query: {} }, null);
417
+ assert.equal(r1.action, 'diagnose');
418
+ assert.ok(['all-clear', 'needs-attention'].includes(r1.verdict ?? ''));
419
+ assert.equal(r1.repairs, undefined, 'diagnose 路径不返回 repairs');
420
+ await rm(smokeRoot2, { recursive: true, force: true });
421
+ // 13b. 完整工具路径:真实注册/审批闸/bootstrap.runOnboardingFix,安装器与 HOME
422
+ // 复检仅用注入 fixture;安装器退出后由复检状态决定结果,报告写复检态。
423
+ const repairRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-repair-e2e-'));
424
+ let healthy = false;
425
+ let approvalRequests = 0;
426
+ let installerCalls = 0;
427
+ const repairLabel = 'Agent Reach(网页/社媒读取)';
428
+ const check = async () => ({
429
+ ok: healthy,
430
+ items: [{
431
+ id: 'agent-reach', label: repairLabel,
432
+ status: healthy ? 'ok' : 'missing',
433
+ detail: healthy ? 'fixture 复检已就位' : 'fixture 未装配',
434
+ ...(healthy ? {} : { fix: 'npx @danceiny/gotry doctor --fix' }),
435
+ }],
436
+ summary: healthy ? 'fixture 复检通过。' : 'fixture 诊断发现 1 项待处理。',
437
+ });
438
+ const registeredRepair = [];
439
+ const repairCtx = {
440
+ tools: { register: (t) => registeredRepair.push(t) },
441
+ systemPrompt: { variable: () => { } },
442
+ on: () => () => { },
443
+ get: (name) => name === 'approval' ? {
444
+ request: async () => { approvalRequests += 1; return 'allowed-once'; },
445
+ } : undefined,
446
+ };
447
+ apply(repairCtx, { stateRoot: repairRoot, timeoutMs: 30_000, hbcliBin: 'hbcli-not-on-path', sessionAccess: 'ask' }, {
448
+ doctor: {
449
+ check,
450
+ repair: {
451
+ installers: {
452
+ reach: async () => { installerCalls += 1; healthy = true; return { ok: true }; },
453
+ },
454
+ },
455
+ },
456
+ });
457
+ const repairTool = registeredRepair.find((t) => t.name === 'gotry_doctor');
458
+ assert.ok(repairTool, 'gotry_doctor repair fixture 已注册');
459
+ const fakeAgent = { id: 'tool-test-agent' };
460
+ const r2 = await repairTool.execute({ query: { action: 'repair', items: ['agent-reach'] } }, { agent: fakeAgent, callId: 'doctor-repair-e2e' });
461
+ assert.equal(r2.action, 'repair');
462
+ assert.equal(approvalRequests, 1, '真实工具执行只请求一次 scope 审批');
463
+ assert.equal(installerCalls, 1, '真实工具执行进入既有 bootstrap repair bridge');
464
+ assert.equal(r2.verdict, 'repaired');
465
+ assert.equal(r2.repairs?.[0]?.status, 'installed');
466
+ assert.equal(r2.recheck?.ok, true, '工具结果显式返回安装后的实际复检');
467
+ assert.ok(r2.plan, 'plan 字段存在');
468
+ assert.ok(typeof r2.plan?.scope_key === 'string', 'scope_key 字段存在');
469
+ assert.ok(r2.diagnosis, 'observable chain: diagnosis 已落');
470
+ assert.ok((r2.summary ?? '').includes('[repair]'), 'summary 含 [repair] 标记');
471
+ assert.ok((r2.summary ?? '').includes('verdict='), 'summary 含 verdict 字段');
472
+ assert.ok(Array.isArray(r2.plan?.selected), 'selected 数组存在');
473
+ assert.ok(Array.isArray(r2.plan?.needs_user_action), 'needs_user_action 数组存在');
474
+ assert.ok(Array.isArray(r2.plan?.unavailable), 'unavailable 数组存在');
475
+ assert.ok(typeof r2.approval === 'string', 'approval 字段存在');
476
+ assert.ok(Array.isArray(r2.repairs), 'repairs 数组存在');
477
+ assert.ok(Array.isArray(r2.skipped), 'skipped 数组存在');
478
+ assert.ok(r2.report_path?.startsWith(repairRoot), '复检报告落隔离 stateRoot');
479
+ assert.match(await readFile(r2.report_path, 'utf8'), /fixture 复检已就位/, '报告内容是复检态,不是修复前诊断');
480
+ await rm(repairRoot, { recursive: true, force: true });
481
+ }
482
+ console.log('13. tool-level gotry_doctor 注册 + observable chain(diagnose→repair→recheck)OK');
172
483
  console.log('doctor-tests: 全部通过');
173
-
174
-
175
- //# sourceURL=ts/scripts/doctor-tests.ts