@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.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- 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
|
-
|
|
12
|
-
|
|
13
|
-
await
|
|
14
|
-
|
|
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
|
-
|
|
34
|
-
});
|
|
35
|
-
|
|
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
|
-
|
|
47
|
-
});
|
|
48
|
-
await
|
|
49
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
version: '0.0.
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|