@danceiny/gotry 0.0.1-rc.8 → 0.2.0-rc.27
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 +172 -217
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1744 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -49
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +135 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +546 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +968 -0
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/session/action-cache.js +126 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +141 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +54 -0
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +50 -0
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +107 -0
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +109 -0
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +686 -0
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +72 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-tests.js +116 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +68 -10
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- 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 +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1479 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/companion-tests.js +67 -0
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +564 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- 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-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- 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/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- 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 +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- 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 +41 -0
- 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 +569 -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/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -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/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -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 +480 -0
- package/dist/scripts/ledger-workflow-crash.js +32 -0
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +49 -0
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +29 -31
- package/dist/scripts/memory-value-report.js +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +161 -0
- package/dist/scripts/nightly-evidence.js +286 -0
- package/dist/scripts/nudge-digest.js +43 -42
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +31 -0
- package/dist/scripts/session-attach-poc.js +62 -0
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +60 -0
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +29 -0
- package/dist/scripts/session-tests.js +862 -0
- package/dist/scripts/session-zones-tests.js +358 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +703 -219
- package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +261 -0
- package/dist/scripts/state-cli.js +585 -0
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +59 -0
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- 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 +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- 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 +41 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +86 -0
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2305 -731
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +358 -162
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +47 -0
- 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 +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zones.js +507 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +767 -0
- package/dist/src/state-repair.js +299 -0
- 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 +59 -0
- package/dist/src/turn-deadline.js +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +538 -360
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +255 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +296 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +568 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +2107 -224
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +278 -40
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +912 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +360 -89
- package/ts/src/wish-pool.ts +79 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flyai 能力层测试(离线,临时假 CLI 脚本经 cliBin 注入,零网络):
|
|
3
|
+
* 1. Sentinel 限流形状 {"message":"SentinelBlockException..."}——合法 JSON 但无 data.itemList,
|
|
4
|
+
* 曾被 `data?.itemList ?? []` 吞成 0/0 静默 miss(issue #24)→ 应判 error 且保留 sentinel 字样
|
|
5
|
+
* 2. 业务空形状 {"data":{"itemList":[]}} → verdict=miss(0/0,evidence 标注)
|
|
6
|
+
* 3. 非空 flight/train itemList 只要有 malformed sibling → registered effect error,不落负事实
|
|
7
|
+
* (flight/train 各一条有效+畸形 fixture;无 partial-completeness hit)
|
|
8
|
+
* 4. 非空 hotel itemList 有 malformed sibling → registered effect error,不落酒店事实
|
|
9
|
+
* 5. transport/hotel 字段类型与空白校验不接受 truthy 数字/对象,并覆盖 official Alibaba shape
|
|
10
|
+
* 6. 完整合法 flight/train/hotel → verdict=hit;有效空列表仍 miss
|
|
11
|
+
* 7. exit≠0 → verdict=error
|
|
12
|
+
* 8. 试用额度达限(exit 1 + MCP HTTP 429 "Trial limit reached",2026-09-02 迪拜
|
|
13
|
+
* session 实况)→ verdict=needs-setup + setup 指引(不再当通用 error 盲重试)
|
|
14
|
+
*
|
|
15
|
+
* 运行: cd ts && npx tsx scripts/flyai-tests.ts
|
|
16
|
+
*/
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { readdirSync } from 'node:fs';
|
|
19
|
+
import { mkdir, readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { flyaiSearch } from '../capabilities/flyai.js';
|
|
23
|
+
import { makeProductionInterpreter } from '../capabilities/effect.js';
|
|
24
|
+
import { loadFactRegistry } from '../capabilities/fact-log.js';
|
|
25
|
+
import { apply } from '../src/index.js';
|
|
26
|
+
import { factsFromFlyai, factsFromHotel } from '../src/bookable-facts.js';
|
|
27
|
+
const tmp = await mkdtemp(join(tmpdir(), 'flyai-test-'));
|
|
28
|
+
function shellQuote(value) {
|
|
29
|
+
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
30
|
+
}
|
|
31
|
+
async function fakeCli(name, code, payload) {
|
|
32
|
+
const p = join(tmp, name);
|
|
33
|
+
await writeFile(p, `#!/bin/sh\nprintf '%s\\n' ${shellQuote(payload)}\nexit ${code}\n`, { mode: 0o755 });
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
async function fakeCliStreams(name, code, stdout, stderr) {
|
|
37
|
+
const p = join(tmp, name);
|
|
38
|
+
await writeFile(p, `#!/bin/sh\nprintf '%s\\n' ${shellQuote(stdout)}\nprintf '%s\\n' ${shellQuote(stderr)} >&2\nexit ${code}\n`, { mode: 0o755 });
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
const base = { kind: 'flight', origin: '上海', destination: '丽江', depDate: '2026-10-01' };
|
|
42
|
+
const trainBase = { kind: 'train', origin: '上海', destination: '大理', depDate: '2026-10-01' };
|
|
43
|
+
const hotelBase = { kind: 'hotel', destName: '大理', checkInDate: '2026-10-01', checkOutDate: '2026-10-03' };
|
|
44
|
+
async function registeredSearch(q) {
|
|
45
|
+
const registeredFlyai = makeProductionInterpreter({ breakers: new Map(), sleep: async () => { } });
|
|
46
|
+
const observation = await registeredFlyai({ effect: 'FLYAI_SEARCH', params: q });
|
|
47
|
+
assert.ok(observation.result, 'registered FlyAI effect 应返回结构化 observation');
|
|
48
|
+
return observation.result;
|
|
49
|
+
}
|
|
50
|
+
const registeredStateRoot = await mkdtemp(join(tmpdir(), 'flyai-registered-e2e-'));
|
|
51
|
+
let registeredCliBin = '';
|
|
52
|
+
const registeredTools = [];
|
|
53
|
+
const registeredBreakers = new Map();
|
|
54
|
+
const registeredContext = {
|
|
55
|
+
tools: { register: (tool) => registeredTools.push(tool) },
|
|
56
|
+
systemPrompt: { variable: () => { } },
|
|
57
|
+
on: () => () => { },
|
|
58
|
+
get: () => undefined,
|
|
59
|
+
};
|
|
60
|
+
const isolatedConfig = {
|
|
61
|
+
stateRoot: registeredStateRoot,
|
|
62
|
+
timeoutMs: 5000,
|
|
63
|
+
hbcliBin: 'hbcli-not-on-path',
|
|
64
|
+
sessionAccess: 'off',
|
|
65
|
+
};
|
|
66
|
+
const registeredProductionEffect = makeProductionInterpreter({
|
|
67
|
+
breakers: registeredBreakers,
|
|
68
|
+
sleep: async () => { },
|
|
69
|
+
handlers: {
|
|
70
|
+
FLYAI_SEARCH: async (params) => flyaiSearch({ ...params, cliBin: registeredCliBin, timeoutMs: 5000 }),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
apply(registeredContext, isolatedConfig, { effect: registeredProductionEffect });
|
|
74
|
+
const registeredFlyaiTool = registeredTools.find(tool => tool.name === 'gotry_flyai_search');
|
|
75
|
+
assert.ok(registeredFlyaiTool, 'apply 应注册 gotry_flyai_search');
|
|
76
|
+
async function registeredToolSearch(name, args, itemList) {
|
|
77
|
+
registeredCliBin = await fakeCli(`registered-${name}`, 0, JSON.stringify({ data: { itemList } }));
|
|
78
|
+
return await registeredFlyaiTool.execute(args, null);
|
|
79
|
+
}
|
|
80
|
+
async function assertRegisteredFactLogEmpty(label) {
|
|
81
|
+
const facts = await loadFactRegistry(registeredStateRoot);
|
|
82
|
+
assert.equal(facts.length, 0, `${label} 不得落 positive/negative fact`);
|
|
83
|
+
registeredBreakers.clear();
|
|
84
|
+
}
|
|
85
|
+
// 1. 真正 registered tool E2E:apply → gotry_flyai_search → production effect/parser → fact sidecar
|
|
86
|
+
const registeredMalformedCases = [
|
|
87
|
+
{ name: 'all-malformed-flight', args: { kind: 'flight', from: '北京', to: '上海', date: '2027-04-01' }, itemList: [{}] },
|
|
88
|
+
{ name: 'all-malformed-train', args: { kind: 'train', from: '北京', to: '上海', date: '2027-04-02' }, itemList: [null] },
|
|
89
|
+
{ name: 'all-malformed-hotel', args: { kind: 'hotel', to: '大理', checkIn: '2027-04-03', checkOut: '2027-04-05' }, itemList: [{ name: 42 }] },
|
|
90
|
+
];
|
|
91
|
+
for (const testCase of registeredMalformedCases) {
|
|
92
|
+
const result = await registeredToolSearch(testCase.name, testCase.args, testCase.itemList);
|
|
93
|
+
assert.equal(result.ok, false, `${testCase.name} 应返回 structured error(ok=false)`);
|
|
94
|
+
assert.equal(result.verdict, 'error', `${testCase.name} 应经 registered tool 返回 error`);
|
|
95
|
+
assert.match(String(result.evidence ?? ''), /flyai@error|malformed/i, `${testCase.name} 应保留结构化 error evidence`);
|
|
96
|
+
await assertRegisteredFactLogEmpty(testCase.name);
|
|
97
|
+
}
|
|
98
|
+
const registeredMixedCases = [
|
|
99
|
+
{
|
|
100
|
+
name: 'mixed-flight',
|
|
101
|
+
args: { kind: 'flight', from: '北京', to: '上海', date: '2027-04-06' },
|
|
102
|
+
itemList: [{ adultPrice: '¥400.0', journeys: [{ segments: [{ marketingTransportNo: 'CA1883', marketingTransportName: '国航', depDateTime: '2027-04-06 21:00:00', arrDateTime: '2027-04-06 23:20:00', depStationName: '首都国际机场', arrStationName: '浦东国际机场', duration: '140分钟' }] }] }, {}],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'mixed-train',
|
|
106
|
+
args: { kind: 'train', from: '北京', to: '上海', date: '2027-04-07' },
|
|
107
|
+
itemList: [{ adultPrice: '¥553.0', journeys: [{ segments: [{ marketingTransportNo: 'G11', depDateTime: '2027-04-07 08:00:00', arrDateTime: '2027-04-07 12:28:00', depStationName: '北京南', arrStationName: '上海虹桥', duration: '268分钟' }] }] }, null],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'mixed-hotel',
|
|
111
|
+
args: { kind: 'hotel', to: '大理', checkIn: '2027-04-08', checkOut: '2027-04-10' },
|
|
112
|
+
itemList: [{ name: '大理A 酒店', star: '高档型', price: '¥7xx', shId: 'hotel-a', detailUrl: 'https://example.test/hotel-a' }, { name: null }],
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
for (const testCase of registeredMixedCases) {
|
|
116
|
+
const result = await registeredToolSearch(testCase.name, testCase.args, testCase.itemList);
|
|
117
|
+
assert.equal(result.ok, false, `${testCase.name} 应返回 structured error(ok=false)`);
|
|
118
|
+
assert.equal(result.verdict, 'error', `${testCase.name} 应经 registered tool 返回整体 error`);
|
|
119
|
+
assert.match(String(result.evidence ?? ''), /flyai@error|malformed/i, `${testCase.name} 应保留结构化 error evidence`);
|
|
120
|
+
await assertRegisteredFactLogEmpty(testCase.name);
|
|
121
|
+
}
|
|
122
|
+
const registeredMiss = await registeredToolSearch('exact-empty-flight', { kind: 'flight', from: '深圳', to: '普吉', date: '2027-04-11' }, []);
|
|
123
|
+
assert.equal(registeredMiss.verdict, 'miss', `registered exact empty list 应返回 miss: ${JSON.stringify(registeredMiss)}`);
|
|
124
|
+
const factsAfterMiss = await loadFactRegistry(registeredStateRoot);
|
|
125
|
+
assert.equal(factsAfterMiss.length, 1, 'registered miss 只应产生一条 exact-date negative fact');
|
|
126
|
+
assert.equal(factsAfterMiss[0]?.kind, 'flight');
|
|
127
|
+
assert.equal(factsAfterMiss[0]?.bookability, 'unavailable_exact_date');
|
|
128
|
+
const registeredHit = await registeredToolSearch('official-train-hit', { kind: 'train', from: '北京', to: '上海', date: '2027-04-12' }, [{ adultPrice: '¥553.0', journeys: [{ journeyType: '直达', segments: [{ depStationName: '北京南', depDateTime: '2027-04-12 08:00:00', arrStationName: '上海虹桥', arrDateTime: '2027-04-12 12:28:00', duration: '268分钟', marketingTransportNo: 'G11', seatClassName: '二等座' }] }], jumpUrl: 'https://example.test/train' }]);
|
|
129
|
+
assert.equal(registeredHit.verdict, 'hit', 'registered official train batch 应返回 hit');
|
|
130
|
+
const factsAfterHit = await loadFactRegistry(registeredStateRoot);
|
|
131
|
+
const typedPositive = factsAfterHit.find((f) => f.kind === 'train' && f.query_id.endsWith(':2027-04-12'));
|
|
132
|
+
assert.ok(typedPositive, 'registered hit 应产生 typed positive fact');
|
|
133
|
+
assert.equal(typedPositive?.bookability, 'bookable_exact_date');
|
|
134
|
+
assert.equal(typedPositive?.flight_no, 'G11');
|
|
135
|
+
await rm(registeredStateRoot, { recursive: true, force: true });
|
|
136
|
+
console.log('1. registered E2E → mixed/all-malformed zero facts / empty miss negative / official train hit positiveOK');
|
|
137
|
+
// #517: registered tool + actual CLI. Persistent exit23/HTTP500 is retried
|
|
138
|
+
// once, then remains an error and writes no inventory fact.
|
|
139
|
+
const registeredTransientState = await mkdtemp(join(tmpdir(), 'flyai-517-registered-state-'));
|
|
140
|
+
const registeredTransientTools = [];
|
|
141
|
+
const registeredTransientContext = {
|
|
142
|
+
tools: { register: (tool) => registeredTransientTools.push(tool) },
|
|
143
|
+
systemPrompt: { variable: () => { } }, on: () => () => { }, get: () => undefined,
|
|
144
|
+
};
|
|
145
|
+
let registeredTransientCli = '';
|
|
146
|
+
const registeredTransientEffect = makeProductionInterpreter({
|
|
147
|
+
breakers: new Map(), sleep: async () => { },
|
|
148
|
+
handlers: { FLYAI_SEARCH: async (params) => flyaiSearch({ ...params, cliBin: registeredTransientCli, timeoutMs: 5_000 }) },
|
|
149
|
+
});
|
|
150
|
+
apply(registeredTransientContext, { ...isolatedConfig, stateRoot: registeredTransientState }, { effect: registeredTransientEffect });
|
|
151
|
+
const registeredTransientTool = registeredTransientTools.find(tool => tool.name === 'gotry_flyai_search');
|
|
152
|
+
assert.ok(registeredTransientTool, '#517 应沿 registered tool 执行');
|
|
153
|
+
const registeredTransientCount = join(tmp, 'flyai-517-http500.count');
|
|
154
|
+
registeredTransientCli = join(tmp, 'flyai-517-http500-exit23');
|
|
155
|
+
await writeFile(registeredTransientCli, [
|
|
156
|
+
'#!/bin/sh',
|
|
157
|
+
'n=0',
|
|
158
|
+
`[ -f '${registeredTransientCount}' ] && n=$(cat '${registeredTransientCount}')`,
|
|
159
|
+
`printf '%s' "$((n + 1))" > '${registeredTransientCount}'`,
|
|
160
|
+
"printf '%s\\n' 'MCP HTTP 500 upstream temporary failure' >&2",
|
|
161
|
+
'exit 23',
|
|
162
|
+
'',
|
|
163
|
+
].join('\n'), { mode: 0o755 });
|
|
164
|
+
const registeredTransientResult = await registeredTransientTool.execute({ kind: 'flight', from: '上海', to: '丽江', date: '2027-04-20' }, null);
|
|
165
|
+
assert.equal(registeredTransientResult.verdict, 'error', 'exit23 + HTTP500 耗尽后应为最终 error');
|
|
166
|
+
assert.equal(await readFile(registeredTransientCount, 'utf8'), '2', 'registered HTTP500 应实际尝试两次');
|
|
167
|
+
assert.equal((await loadFactRegistry(registeredTransientState)).length, 0, '最终 error 不得写入库存 facts');
|
|
168
|
+
await rm(registeredTransientState, { recursive: true, force: true });
|
|
169
|
+
console.log('1b. registered CLI exit23+HTTP500 → attempts=2 / final error / zero facts OK');
|
|
170
|
+
// Remaining #517 boundaries use the same production effect and actual child
|
|
171
|
+
// process adapter, while checking exact attempt counts in the effect trace.
|
|
172
|
+
async function processEffectCase(cliBin, extra = {}) {
|
|
173
|
+
const interpreter = makeProductionInterpreter({ breakers: new Map(), sleep: async () => { } });
|
|
174
|
+
return await interpreter({ effect: 'FLYAI_SEARCH', params: { ...base, cliBin, timeoutMs: 5_000, ...extra } });
|
|
175
|
+
}
|
|
176
|
+
const noTransientCli = await fakeCliStreams('flyai-517-exit23-no-transient', 23, '', 'provider failed');
|
|
177
|
+
const noTransientOutcome = await processEffectCase(noTransientCli);
|
|
178
|
+
assert.equal(noTransientOutcome.trace.attempts, 1, '无瞬时文本的 exit23 不得重试');
|
|
179
|
+
assert.equal(noTransientOutcome.result?.retryable, false);
|
|
180
|
+
const deadlineCli = join(tmp, 'flyai-517-local-deadline');
|
|
181
|
+
await writeFile(deadlineCli, '#!/bin/sh\nsleep 1\n', { mode: 0o755 });
|
|
182
|
+
const deadlineOutcome = await processEffectCase(deadlineCli, { timeoutMs: 50 });
|
|
183
|
+
assert.equal(deadlineOutcome.trace.attempts, 1, '本地 deadline 只能执行一次');
|
|
184
|
+
assert.equal(deadlineOutcome.result?.verdict, 'timeout');
|
|
185
|
+
assert.equal(deadlineOutcome.result?.localTermination, 'deadline');
|
|
186
|
+
assert.equal(deadlineOutcome.result?.retryable, false, '本地 deadline 不得当上游 timeout 重试');
|
|
187
|
+
const spawnOutcome = await processEffectCase(join(tmp, 'flyai-517-does-not-exist'));
|
|
188
|
+
assert.equal(spawnOutcome.trace.attempts, 1, 'spawn 失败只能执行一次');
|
|
189
|
+
assert.equal(spawnOutcome.result?.localTermination, 'spawn');
|
|
190
|
+
assert.equal(spawnOutcome.result?.retryable, false);
|
|
191
|
+
const emptyExitCli = join(tmp, 'flyai-517-empty-exit');
|
|
192
|
+
await writeFile(emptyExitCli, '#!/bin/sh\nkill -TERM $$\n', { mode: 0o755 });
|
|
193
|
+
const emptyExitOutcome = await processEffectCase(emptyExitCli);
|
|
194
|
+
assert.equal(emptyExitOutcome.trace.attempts, 1, '空退出只能执行一次');
|
|
195
|
+
assert.equal(emptyExitOutcome.result?.localTermination, 'empty-exit');
|
|
196
|
+
assert.equal(emptyExitOutcome.result?.retryable, false);
|
|
197
|
+
const signalController = new AbortController();
|
|
198
|
+
const signalCli = join(tmp, 'flyai-517-signal');
|
|
199
|
+
await writeFile(signalCli, '#!/bin/sh\nsleep 1\n', { mode: 0o755 });
|
|
200
|
+
const signalPromise = processEffectCase(signalCli, { signal: signalController.signal });
|
|
201
|
+
setTimeout(() => signalController.abort(), 30);
|
|
202
|
+
const signalOutcome = await signalPromise;
|
|
203
|
+
assert.equal(signalOutcome.trace.attempts, 1, 'signal 终止只能执行一次');
|
|
204
|
+
assert.equal(signalOutcome.trace.declined, 'aborted', 'signal 终止应沿 effect 取消面返回');
|
|
205
|
+
// #514: 本机终止携带陈旧 429 文本(终止前打印的额度错误)不得被判为
|
|
206
|
+
// needs-setup/可重试——终止分类必须先于 HTTP 文本扫描。
|
|
207
|
+
const staleQuotaCli = join(tmp, 'flyai-514-stale-429-signal');
|
|
208
|
+
await writeFile(staleQuotaCli, [
|
|
209
|
+
'#!/bin/sh',
|
|
210
|
+
"printf '%s\\n' 'MCP HTTP 429 Trial limit reached' >&2",
|
|
211
|
+
'kill -TERM $$',
|
|
212
|
+
'',
|
|
213
|
+
].join('\n'), { mode: 0o755 });
|
|
214
|
+
const staleQuotaOutcome = await processEffectCase(staleQuotaCli);
|
|
215
|
+
assert.equal(staleQuotaOutcome.trace.attempts, 1, '带陈旧 429 文本的终止只能执行一次');
|
|
216
|
+
assert.equal(staleQuotaOutcome.result?.verdict, 'error', '终止不得沿用 429 额度分类');
|
|
217
|
+
assert.equal(staleQuotaOutcome.result?.localTermination, 'empty-exit');
|
|
218
|
+
assert.equal(staleQuotaOutcome.result?.retryable, false);
|
|
219
|
+
assert.equal(staleQuotaOutcome.result?.setup, undefined, '终止不得生成额度补配指引');
|
|
220
|
+
console.log('1c. exit23/no-text、deadline、signal、spawn、empty-exit、stale-429-termination 均 fail-closed 单次 OK');
|
|
221
|
+
// #516: 主进程正常退出后,持有 stderr 管道且不退出的后代不得让调用无界挂起。
|
|
222
|
+
// 进程组边界统一复用 spawn-bounded(与 hbcli/anything 同策略):主进程 exit 后
|
|
223
|
+
// 最多等 drainMs,到点杀组、drain-timeout fail-closed——主进程退出原因(code 0)
|
|
224
|
+
// 与 drain 超时区分开,不完整收集绝不写成 hit/miss 库存事实。
|
|
225
|
+
const drainPayload = JSON.stringify({
|
|
226
|
+
data: {
|
|
227
|
+
itemList: [{
|
|
228
|
+
journeys: [{ segments: [{
|
|
229
|
+
marketingTransportNo: '9C6617', marketingTransportName: '吉祥航空',
|
|
230
|
+
depDateTime: '2026-10-01 07:55', arrDateTime: '2026-10-01 11:20',
|
|
231
|
+
depStationName: '浦东T2', arrStationName: '丽江三义', duration: 205,
|
|
232
|
+
}] }],
|
|
233
|
+
ticketPrice: '580',
|
|
234
|
+
jumpUrl: 'https://www.fliggy.com/demo',
|
|
235
|
+
}],
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
function drainHolderCli(name, holderSpec, stdout, stderrExtra) {
|
|
239
|
+
return [
|
|
240
|
+
'#!/bin/sh',
|
|
241
|
+
`${holderSpec} &`,
|
|
242
|
+
`echo $! > '${join(tmp, `${name}.holder.pid`)}'`,
|
|
243
|
+
"printf '%s\\n' 'descendant still holds stderr' >&2",
|
|
244
|
+
...(stderrExtra ? ["printf '%s\\n' " + shellQuote(stderrExtra) + " >&2"] : []),
|
|
245
|
+
`printf '%s\\n' ${shellQuote(stdout)}`,
|
|
246
|
+
'exit 0',
|
|
247
|
+
'',
|
|
248
|
+
].join('\n');
|
|
249
|
+
}
|
|
250
|
+
async function holderPid(name) {
|
|
251
|
+
const text = await readFile(join(tmp, `${name}.holder.pid`), 'utf8');
|
|
252
|
+
return Number(text.trim());
|
|
253
|
+
}
|
|
254
|
+
async function waitForPidGone(pid, boundMs) {
|
|
255
|
+
const deadline = Date.now() + boundMs;
|
|
256
|
+
for (;;) {
|
|
257
|
+
try {
|
|
258
|
+
process.kill(pid, 0);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (Date.now() >= deadline)
|
|
264
|
+
return false;
|
|
265
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const tmpdirBeforeDrain = readdirSync(tmpdir()).filter(f => f.startsWith('gotry-flyai-')).length;
|
|
269
|
+
// 1d-1: drain 超时(真实注册工具入口)——有效 hit 载荷 + 后代持管道不退 →
|
|
270
|
+
// 有界返回 drain-timeout error、零库存事实、后代被杀(读回证明)。
|
|
271
|
+
const drainStateRoot = await mkdtemp(join(tmpdir(), 'flyai-516-registered-state-'));
|
|
272
|
+
const drainTools = [];
|
|
273
|
+
const drainContext = {
|
|
274
|
+
tools: { register: (tool) => drainTools.push(tool) },
|
|
275
|
+
systemPrompt: { variable: () => { } }, on: () => () => { }, get: () => undefined,
|
|
276
|
+
};
|
|
277
|
+
let drainCli = '';
|
|
278
|
+
const drainBreakers = new Map();
|
|
279
|
+
const drainEffect = makeProductionInterpreter({
|
|
280
|
+
breakers: drainBreakers, sleep: async () => { },
|
|
281
|
+
handlers: { FLYAI_SEARCH: async (params) => flyaiSearch({ ...params, cliBin: drainCli, timeoutMs: 10_000, drainMs: 600 }) },
|
|
282
|
+
});
|
|
283
|
+
apply(drainContext, { ...isolatedConfig, stateRoot: drainStateRoot, timeoutMs: 10_000 }, { effect: drainEffect });
|
|
284
|
+
const drainTool = drainTools.find(tool => tool.name === 'gotry_flyai_search');
|
|
285
|
+
assert.ok(drainTool, '#516 应沿 registered tool 执行');
|
|
286
|
+
drainCli = join(tmp, 'flyai-516-drain-timeout');
|
|
287
|
+
await writeFile(drainCli, drainHolderCli('flyai-516-drain-timeout', 'sleep 30', drainPayload, ''), { mode: 0o755 });
|
|
288
|
+
const drainStarted = Date.now();
|
|
289
|
+
const drainResult = await drainTool.execute({ kind: 'flight', from: '上海', to: '丽江', date: '2027-05-01' }, null);
|
|
290
|
+
const drainElapsed = Date.now() - drainStarted;
|
|
291
|
+
assert.equal(drainResult.ok, false, 'drain 超时不得报告成功');
|
|
292
|
+
assert.equal(drainResult.verdict, 'error', `drain 超时应判 error,实际 ${String(drainResult.verdict)}`);
|
|
293
|
+
assert.equal(drainResult.localTermination, 'drain-timeout', 'drain 超时须与主进程退出原因区分');
|
|
294
|
+
assert.equal(drainResult.retryable, false, 'drain 超时不可重试');
|
|
295
|
+
assert.equal(drainResult.options, undefined, '不完整收集不得暴露 options');
|
|
296
|
+
assert.match(String(drainResult.evidence ?? ''), /drain-timeout/, 'evidence 标注 drain-timeout');
|
|
297
|
+
assert.ok(drainElapsed >= 600 && drainElapsed < 3_500, `drain 超时应有界(600ms 窗口),实际 ${drainElapsed}ms`);
|
|
298
|
+
assert.equal((await loadFactRegistry(drainStateRoot)).length, 0, 'drain 超时不得写入库存 facts');
|
|
299
|
+
assert.ok(await waitForPidGone(await holderPid('flyai-516-drain-timeout'), 2_000), '整组 SIGKILL 后持有者后代须已死(读回)');
|
|
300
|
+
drainBreakers.clear();
|
|
301
|
+
// 1d-2: 终止分类先于 HTTP 文本——drain 超时携带陈旧 429 文本不得判 needs-setup。
|
|
302
|
+
drainCli = join(tmp, 'flyai-516-drain-stale-429');
|
|
303
|
+
await writeFile(drainCli, drainHolderCli('flyai-516-drain-stale-429', 'sleep 30', drainPayload, 'MCP HTTP 429 Trial limit reached'), { mode: 0o755 });
|
|
304
|
+
const drainStale = await processEffectCase(drainCli, { drainMs: 600, timeoutMs: 10_000 });
|
|
305
|
+
assert.equal(drainStale.trace.attempts, 1, 'drain 超时只能执行一次');
|
|
306
|
+
assert.equal(drainStale.result?.verdict, 'error', 'drain 超时不得沿用 429 额度分类');
|
|
307
|
+
assert.equal(drainStale.result?.localTermination, 'drain-timeout');
|
|
308
|
+
assert.equal(drainStale.result?.setup, undefined, 'drain 超时不得生成额度补配指引');
|
|
309
|
+
// 1d-3: drain 窗口内释放(后代短暂持管道后自行退出)→ 正常 exit 0 语义保持 hit。
|
|
310
|
+
drainCli = join(tmp, 'flyai-516-drain-within-window');
|
|
311
|
+
await writeFile(drainCli, drainHolderCli('flyai-516-drain-within-window', 'sleep 0.2', drainPayload, ''), { mode: 0o755 });
|
|
312
|
+
const withinStarted = Date.now();
|
|
313
|
+
const withinResult = await drainTool.execute({ kind: 'flight', from: '上海', to: '丽江', date: '2027-05-02' }, null);
|
|
314
|
+
const withinElapsed = Date.now() - withinStarted;
|
|
315
|
+
assert.equal(withinResult.ok, true, '窗口内释放应保持正常成功');
|
|
316
|
+
assert.equal(withinResult.verdict, 'hit', `窗口内释放应判 hit,实际 ${String(withinResult.verdict)}`);
|
|
317
|
+
assert.ok(withinElapsed < 5_000, `正常 exit + 短暂 drain 应有界,实际 ${withinElapsed}ms`);
|
|
318
|
+
assert.equal((await loadFactRegistry(drainStateRoot)).length, 1, 'hit 照常落 typed positive fact');
|
|
319
|
+
await rm(drainStateRoot, { recursive: true, force: true });
|
|
320
|
+
const tmpdirAfterDrain = readdirSync(tmpdir()).filter(f => f.startsWith('gotry-flyai-')).length;
|
|
321
|
+
assert.equal(tmpdirAfterDrain, tmpdirBeforeDrain, 'drain 路径不得泄漏 stdout 临时文件');
|
|
322
|
+
console.log('1d. 持管道后代:drain 超时有界 fail-closed(零事实+读回杀净)/陈旧 429 不改判/窗口内释放保持 hit OK');
|
|
323
|
+
// 1. Sentinel 限流:合法 JSON 的非业务形状 → error(不是静默 miss)
|
|
324
|
+
const sentinelBin = await fakeCli('flyai-sentinel', 0, '{"message":"SentinelBlockException: flow control"}');
|
|
325
|
+
const s = await flyaiSearch({ ...base, cliBin: sentinelBin, timeoutMs: 5000 });
|
|
326
|
+
assert.equal(s.ok, false, 'Sentinel 形状应 ok=false');
|
|
327
|
+
assert.equal(s.verdict, 'error', `Sentinel 形状应判 error,实际 ${s.verdict}`);
|
|
328
|
+
assert.equal(s.retryable, false, 'Sentinel 不得标记 retryable');
|
|
329
|
+
assert.match(s.error ?? '', /sentinel/i, `error 应保留 sentinel 字样(供上层限流识别),实际 ${s.error}`);
|
|
330
|
+
assert.match(s.evidence, /\[实时API:flyai@error@/, 'error 证据链标注');
|
|
331
|
+
console.log('1. Sentinel 非业务形状 → error(非静默 miss)OK');
|
|
332
|
+
// 2. 业务空形状 → miss(上游正常,确无航班)
|
|
333
|
+
const missBin = await fakeCli('flyai-miss', 0, '{"data":{"itemList":[]}}');
|
|
334
|
+
const m = await flyaiSearch({ ...base, cliBin: missBin, timeoutMs: 5000 });
|
|
335
|
+
assert.equal(m.ok, true, '业务空形状 ok=true');
|
|
336
|
+
assert.equal(m.verdict, 'miss', `空 itemList 应判 miss,实际 ${m.verdict}`);
|
|
337
|
+
assert.match(m.evidence, /0\/0 flight options/, 'miss 证据链 0/0');
|
|
338
|
+
console.log('2. 业务空形状 → miss(0/0)OK');
|
|
339
|
+
// 3. 非空但全 malformed transport item → registered effect error,不落负事实
|
|
340
|
+
const malformedBin = await fakeCli('flyai-malformed', 0, '{"data":{"itemList":[{}]}}');
|
|
341
|
+
const malformed = await registeredSearch({ ...base, cliBin: malformedBin, timeoutMs: 5000 });
|
|
342
|
+
assert.equal(malformed.ok, false, '全 malformed transport item 不应报告成功');
|
|
343
|
+
assert.equal(malformed.verdict, 'error', '非空全 malformed transport item 应判 error');
|
|
344
|
+
assert.equal(malformed.retryable, false, 'malformed 不得标记 retryable');
|
|
345
|
+
assert.match(malformed.error ?? '', /malformed|valid typed transport/i, 'error 应保留 transport shape 原因');
|
|
346
|
+
assert.match(malformed.evidence, /flyai@error@.*flight itemList/i, 'evidence 应保留结构化 flight itemList 错误');
|
|
347
|
+
assert.deepEqual(factsFromFlyai({ kind: base.kind, origin: base.origin, destination: base.destination, date: base.depDate }, malformed, new Date().toISOString()), [], 'registered path 的 malformed error 不得生成负库存事实');
|
|
348
|
+
console.log('3. 非空全 malformed transport item → registered error,不落负事实OK');
|
|
349
|
+
// 4. flight 有效+malformed sibling → 整体 error,不落负事实
|
|
350
|
+
const hitPayload = JSON.stringify({
|
|
351
|
+
data: {
|
|
352
|
+
itemList: [{
|
|
353
|
+
journeys: [{
|
|
354
|
+
segments: [{
|
|
355
|
+
marketingTransportNo: '9C6617', marketingTransportName: '吉祥航空',
|
|
356
|
+
depDateTime: '2026-10-01 07:55', arrDateTime: '2026-10-01 11:20',
|
|
357
|
+
depStationName: '浦东T2', arrStationName: '丽江三义', duration: 205,
|
|
358
|
+
}],
|
|
359
|
+
}],
|
|
360
|
+
ticketPrice: '580',
|
|
361
|
+
jumpUrl: 'https://www.fliggy.com/demo',
|
|
362
|
+
}],
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
const mixedBin = await fakeCli('flyai-mixed', 0, JSON.stringify({
|
|
366
|
+
data: {
|
|
367
|
+
itemList: [{}, {
|
|
368
|
+
journeys: [{ segments: [{
|
|
369
|
+
marketingTransportNo: 'G201', marketingTransportName: '高铁',
|
|
370
|
+
depDateTime: '2026-10-01 09:00', arrDateTime: '2026-10-01 12:00',
|
|
371
|
+
depStationName: '上海虹桥', arrStationName: '丽江站', duration: 180,
|
|
372
|
+
}] }],
|
|
373
|
+
ticketPrice: '480',
|
|
374
|
+
}],
|
|
375
|
+
},
|
|
376
|
+
}));
|
|
377
|
+
const flightMixed = await registeredSearch({ ...base, cliBin: mixedBin, timeoutMs: 5000 });
|
|
378
|
+
assert.equal(flightMixed.ok, false, 'flight 混合响应不应报告成功');
|
|
379
|
+
assert.equal(flightMixed.verdict, 'error', 'flight 有效+malformed sibling 应整体 error');
|
|
380
|
+
assert.equal(flightMixed.options, undefined, 'flight malformed error 不应暴露 options');
|
|
381
|
+
assert.match(flightMixed.evidence, /flight itemList malformed.*1\/2/i, 'flight error 应暴露 malformed/总条目比例');
|
|
382
|
+
assert.deepEqual(factsFromFlyai({ kind: base.kind, origin: base.origin, destination: base.destination, date: base.depDate }, flightMixed, new Date().toISOString()), [], 'flight mixed error 不得生成负库存事实');
|
|
383
|
+
console.log('4. flight 有效+malformed sibling → 整体 error,不落负事实OK');
|
|
384
|
+
// 5. train 有效+malformed sibling → 整体 error,不落负事实
|
|
385
|
+
const trainMixedBin = await fakeCli('flyai-train-mixed', 0, JSON.stringify({
|
|
386
|
+
data: {
|
|
387
|
+
itemList: [{
|
|
388
|
+
journeys: [{ segments: [{
|
|
389
|
+
marketingTransportNo: 'G201', marketingTransportName: '高铁',
|
|
390
|
+
depDateTime: '2026-10-01 09:00', arrDateTime: '2026-10-01 12:00',
|
|
391
|
+
depStationName: '上海虹桥', arrStationName: '大理站', duration: '180',
|
|
392
|
+
}] }],
|
|
393
|
+
ticketPrice: '480',
|
|
394
|
+
}, null],
|
|
395
|
+
},
|
|
396
|
+
}));
|
|
397
|
+
const trainMixed = await registeredSearch({ ...trainBase, cliBin: trainMixedBin, timeoutMs: 5000 });
|
|
398
|
+
assert.equal(trainMixed.ok, false, 'train 混合响应不应报告成功');
|
|
399
|
+
assert.equal(trainMixed.verdict, 'error', 'train 有效+malformed sibling 应整体 error');
|
|
400
|
+
assert.equal(trainMixed.options, undefined, 'train malformed error 不应暴露 options');
|
|
401
|
+
assert.match(trainMixed.evidence, /train itemList malformed.*1\/2/i, 'train error 应暴露 malformed/总条目比例');
|
|
402
|
+
assert.deepEqual(factsFromFlyai({ kind: trainBase.kind, origin: trainBase.origin, destination: trainBase.destination, date: trainBase.depDate }, trainMixed, new Date().toISOString()), [], 'train mixed error 不得生成负库存事实');
|
|
403
|
+
console.log('5. train 有效+malformed sibling → 整体 error,不落负事实OK');
|
|
404
|
+
// 6. hotel 有效+malformed sibling → 整体 error,不落酒店事实
|
|
405
|
+
const hotelMixedBin = await fakeCli('flyai-hotel-mixed', 0, JSON.stringify({
|
|
406
|
+
data: {
|
|
407
|
+
itemList: [{ name: '大理A 酒店', star: '高档型', price: '¥7xx', shId: 'hotel-a' }, { name: null }],
|
|
408
|
+
},
|
|
409
|
+
}));
|
|
410
|
+
const hotelMixed = await registeredSearch({ ...hotelBase, cliBin: hotelMixedBin, timeoutMs: 5000 });
|
|
411
|
+
assert.equal(hotelMixed.ok, false, 'hotel 混合响应不应报告成功');
|
|
412
|
+
assert.equal(hotelMixed.verdict, 'error', 'hotel 有效+malformed sibling 应整体 error');
|
|
413
|
+
assert.equal(hotelMixed.hotels, undefined, 'hotel malformed error 不应暴露 hotels');
|
|
414
|
+
assert.match(hotelMixed.evidence, /hotel itemList malformed.*1\/2/i, 'hotel error 应暴露 malformed/总条目比例');
|
|
415
|
+
assert.deepEqual(factsFromHotel({ source: 'flyai-hotel', destination: hotelBase.destName, checkIn: hotelBase.checkInDate, checkOut: hotelBase.checkOutDate, verdict: hotelMixed.verdict, options: 0, evidence: hotelMixed.evidence, fetchedAt: new Date().toISOString() }), [], 'hotel mixed error 不得生成酒店事实');
|
|
416
|
+
console.log('6. hotel 有效+malformed sibling → 整体 error,不落酒店事实OK');
|
|
417
|
+
// 7. typed transport/hotel 字段拒绝 truthy 数字与空白
|
|
418
|
+
const typedTransportMalformedBin = await fakeCli('flyai-typed-transport-malformed', 0, JSON.stringify({
|
|
419
|
+
data: { itemList: [{
|
|
420
|
+
journeys: [{ segments: [{
|
|
421
|
+
marketingTransportNo: 12345, marketingTransportName: '吉祥航空',
|
|
422
|
+
depDateTime: ' ', arrDateTime: '2026-10-01 11:20',
|
|
423
|
+
depStationName: '浦东T2', arrStationName: '丽江三义', duration: 205,
|
|
424
|
+
}] }],
|
|
425
|
+
ticketPrice: '580',
|
|
426
|
+
}] },
|
|
427
|
+
}));
|
|
428
|
+
const typedTransportMalformed = await registeredSearch({ ...base, cliBin: typedTransportMalformedBin, timeoutMs: 5000 });
|
|
429
|
+
assert.equal(typedTransportMalformed.verdict, 'error', '数字 transportNo/空白 depDateTime 应判 error');
|
|
430
|
+
assert.equal(typedTransportMalformed.options, undefined, '字段类型 malformed 不应暴露 options');
|
|
431
|
+
const typedHotelMalformedBin = await fakeCli('flyai-typed-hotel-malformed', 0, JSON.stringify({ data: { itemList: [{ name: { value: '酒店' }, price: '¥7xx' }] } }));
|
|
432
|
+
const typedHotelMalformed = await registeredSearch({ ...hotelBase, cliBin: typedHotelMalformedBin, timeoutMs: 5000 });
|
|
433
|
+
assert.equal(typedHotelMalformed.verdict, 'error', '对象 hotel name 应判 error');
|
|
434
|
+
assert.equal(typedHotelMalformed.hotels, undefined, '字段类型 malformed 不应暴露 hotels');
|
|
435
|
+
console.log('7. typed transport/hotel 字段拒绝 truthy 数字、对象与空白OK');
|
|
436
|
+
// 8. official Alibaba shape → adultPrice、带“分钟”时长与 train 缺名称兼容
|
|
437
|
+
const officialFlightBin = await fakeCli('flyai-official-flight', 0, JSON.stringify({
|
|
438
|
+
data: { itemList: [{
|
|
439
|
+
adultPrice: '¥400.0',
|
|
440
|
+
journeys: [{ journeyType: '直达', segments: [{
|
|
441
|
+
depStationName: '首都国际机场', depDateTime: '2026-03-28 21:00:00',
|
|
442
|
+
arrStationName: '浦东国际机场', arrDateTime: '2026-03-28 23:20:00',
|
|
443
|
+
duration: '140分钟', marketingTransportName: '国航', marketingTransportNo: 'CA1883', seatClassName: '经济舱',
|
|
444
|
+
}] }],
|
|
445
|
+
jumpUrl: 'https://example.test/flight',
|
|
446
|
+
}] },
|
|
447
|
+
}));
|
|
448
|
+
const officialFlight = await registeredSearch({ ...base, cliBin: officialFlightBin, timeoutMs: 5000 });
|
|
449
|
+
assert.equal(officialFlight.verdict, 'hit', 'official flight shape 应保持 hit');
|
|
450
|
+
assert.equal(officialFlight.options?.[0]?.price, 400, 'official adultPrice 应解析为 400');
|
|
451
|
+
assert.equal(officialFlight.options?.[0]?.durationMin, 140, 'official 中文分钟时长应解析为 140');
|
|
452
|
+
const officialTrainBin = await fakeCli('flyai-official-train', 0, JSON.stringify({
|
|
453
|
+
data: { itemList: [{
|
|
454
|
+
adultPrice: '¥553.0',
|
|
455
|
+
journeys: [{ journeyType: '直达', segments: [{
|
|
456
|
+
depStationName: '北京南', depDateTime: '2026-03-15 08:00:00',
|
|
457
|
+
arrStationName: '上海虹桥', arrDateTime: '2026-03-15 12:28:00',
|
|
458
|
+
duration: '268分钟', marketingTransportNo: 'G11', seatClassName: '二等座',
|
|
459
|
+
}] }],
|
|
460
|
+
jumpUrl: 'https://example.test/train',
|
|
461
|
+
}] },
|
|
462
|
+
}));
|
|
463
|
+
const officialTrain = await registeredSearch({ ...trainBase, cliBin: officialTrainBin, timeoutMs: 5000 });
|
|
464
|
+
assert.equal(officialTrain.verdict, 'hit', 'official train shape 应保持 hit');
|
|
465
|
+
assert.equal(officialTrain.options?.[0]?.price, 553, 'official train adultPrice 应解析为 553');
|
|
466
|
+
assert.equal(officialTrain.options?.[0]?.durationMin, 268, 'official train 中文分钟时长应解析为 268');
|
|
467
|
+
assert.equal(officialTrain.options?.[0]?.name, '', '缺 marketingTransportName 不猜名称且保留空字符串');
|
|
468
|
+
console.log('8. official flight/train shape → adultPrice/分钟时长/缺名称兼容OK');
|
|
469
|
+
// 9. official typed fields:invalid price/duration/name type → structured error
|
|
470
|
+
const invalidPriceBin = await fakeCli('flyai-invalid-price', 0, JSON.stringify({
|
|
471
|
+
data: { itemList: [{
|
|
472
|
+
adultPrice: 'not-a-price',
|
|
473
|
+
journeys: [{ segments: [{
|
|
474
|
+
marketingTransportNo: 'CA1883', marketingTransportName: '国航',
|
|
475
|
+
depDateTime: '2026-03-28 21:00:00', arrDateTime: '2026-03-28 23:20:00',
|
|
476
|
+
depStationName: '首都国际机场', arrStationName: '浦东国际机场', duration: '140分钟',
|
|
477
|
+
}] }],
|
|
478
|
+
}] },
|
|
479
|
+
}));
|
|
480
|
+
const invalidPrice = await registeredSearch({ ...base, cliBin: invalidPriceBin, timeoutMs: 5000 });
|
|
481
|
+
assert.equal(invalidPrice.verdict, 'error', '非法文本价格应判 error');
|
|
482
|
+
const invalidDurationBin = await fakeCli('flyai-invalid-duration', 0, JSON.stringify({
|
|
483
|
+
data: { itemList: [{
|
|
484
|
+
adultPrice: '¥400.0',
|
|
485
|
+
journeys: [{ segments: [{
|
|
486
|
+
marketingTransportNo: 'CA1883', marketingTransportName: '国航',
|
|
487
|
+
depDateTime: '2026-03-28 21:00:00', arrDateTime: '2026-03-28 23:20:00',
|
|
488
|
+
depStationName: '首都国际机场', arrStationName: '浦东国际机场', duration: { value: 140 },
|
|
489
|
+
}] }],
|
|
490
|
+
}] },
|
|
491
|
+
}));
|
|
492
|
+
const invalidDuration = await registeredSearch({ ...base, cliBin: invalidDurationBin, timeoutMs: 5000 });
|
|
493
|
+
assert.equal(invalidDuration.verdict, 'error', '对象时长应判 error');
|
|
494
|
+
const invalidNameBin = await fakeCli('flyai-invalid-name', 0, JSON.stringify({
|
|
495
|
+
data: { itemList: [{
|
|
496
|
+
adultPrice: '¥553.0',
|
|
497
|
+
journeys: [{ segments: [{
|
|
498
|
+
depStationName: '北京南', depDateTime: '2026-03-15 08:00:00',
|
|
499
|
+
arrStationName: '上海虹桥', arrDateTime: '2026-03-15 12:28:00',
|
|
500
|
+
duration: '268分钟', marketingTransportNo: 'G11', marketingTransportName: 123,
|
|
501
|
+
}] }],
|
|
502
|
+
}] },
|
|
503
|
+
}));
|
|
504
|
+
const invalidName = await registeredSearch({ ...trainBase, cliBin: invalidNameBin, timeoutMs: 5000 });
|
|
505
|
+
assert.equal(invalidName.verdict, 'error', '数字 marketingTransportName 应判 error');
|
|
506
|
+
console.log('9. official typed 字段非法 price/duration/name → errorOK');
|
|
507
|
+
// 10. 完整合法 flight → hit,字段解析
|
|
508
|
+
const hitBin = await fakeCli('flyai-hit', 0, hitPayload);
|
|
509
|
+
const h = await flyaiSearch({ ...base, cliBin: hitBin, timeoutMs: 5000 });
|
|
510
|
+
assert.equal(h.ok, true, '完整合法 flight ok=true');
|
|
511
|
+
assert.equal(h.verdict, 'hit', `完整合法 flight 应判 hit,实际 ${h.verdict}`);
|
|
512
|
+
assert.equal(h.options?.length, 1, '1 个选项');
|
|
513
|
+
assert.equal(h.options[0].no, '9C6617');
|
|
514
|
+
assert.equal(h.options[0].price, 580, '价格数值解析');
|
|
515
|
+
assert.equal(h.options[0].depStation, '浦东T2');
|
|
516
|
+
assert.match(h.evidence, /1\/1 flight options/, 'hit 证据链 1/1');
|
|
517
|
+
console.log('10. 完整合法 flight → hit(9C6617 ¥580)OK');
|
|
518
|
+
// 11. 完整合法 train → hit,共享 transport typed 合同
|
|
519
|
+
const trainHitBin = await fakeCli('flyai-train-hit', 0, JSON.stringify({
|
|
520
|
+
data: {
|
|
521
|
+
itemList: [{
|
|
522
|
+
journeys: [{ segments: [{
|
|
523
|
+
marketingTransportNo: 'G201', marketingTransportName: '高铁',
|
|
524
|
+
depDateTime: '2026-10-01 09:00', arrDateTime: '2026-10-01 12:00',
|
|
525
|
+
depStationName: '上海虹桥', arrStationName: '大理站', duration: '180', seatClassName: '二等座',
|
|
526
|
+
}] }],
|
|
527
|
+
ticketPrice: '480',
|
|
528
|
+
}],
|
|
529
|
+
},
|
|
530
|
+
}));
|
|
531
|
+
const trainHit = await flyaiSearch({ ...trainBase, cliBin: trainHitBin, timeoutMs: 5000 });
|
|
532
|
+
assert.equal(trainHit.ok, true, '完整合法 train ok=true');
|
|
533
|
+
assert.equal(trainHit.verdict, 'hit', '完整合法 train 应判 hit');
|
|
534
|
+
assert.equal(trainHit.options?.length, 1, '完整合法 train 产出 1 个选项');
|
|
535
|
+
assert.equal(trainHit.options?.[0]?.no, 'G201');
|
|
536
|
+
console.log('11. 完整合法 train → hit(G201)OK');
|
|
537
|
+
// 12. 完整合法 hotel → hit,保持酒店语义
|
|
538
|
+
const hotelHitBin = await fakeCli('flyai-hotel-hit', 0, JSON.stringify({ data: { itemList: [{ name: '大理A 酒店', star: '高档型', price: '¥7xx', rate: null, address: 'addr', interestsPoi: '近洱海', shId: 'hotel-a', detailUrl: 'https://example.test/hotel-a' }] } }));
|
|
539
|
+
const hotelHit = await flyaiSearch({ ...hotelBase, cliBin: hotelHitBin, timeoutMs: 5000 });
|
|
540
|
+
assert.equal(hotelHit.ok, true, '完整合法 hotel ok=true');
|
|
541
|
+
assert.equal(hotelHit.verdict, 'hit', '完整合法 hotel 应判 hit');
|
|
542
|
+
assert.equal(hotelHit.hotels?.length, 1, '1 个酒店选项');
|
|
543
|
+
assert.equal(hotelHit.hotels?.[0]?.name, '大理A 酒店');
|
|
544
|
+
assert.equal(hotelHit.hotels?.[0]?.priceRaw, '¥7xx', '酒店打码价原值保留');
|
|
545
|
+
console.log('12. 完整合法 hotel → hit(打码价保真)OK');
|
|
546
|
+
// 13. exit≠0 → error
|
|
547
|
+
const failBin = await fakeCli('flyai-fail', 1, '');
|
|
548
|
+
const f = await flyaiSearch({ ...base, cliBin: failBin, timeoutMs: 5000 });
|
|
549
|
+
assert.equal(f.ok, false);
|
|
550
|
+
assert.equal(f.verdict, 'error', '非零退出应判 error');
|
|
551
|
+
console.log('13. 非零退出 → error OK');
|
|
552
|
+
// 14. 试用额度达限(2026-09-02 迪拜 session 实况:exit 1 + MCP HTTP 429 Trial limit
|
|
553
|
+
// reached)→ needs-setup 而非通用 error——阻断 LLM 拿同一把 429 跨轮盲重试
|
|
554
|
+
const trialBin = join(tmp, 'flyai-trial');
|
|
555
|
+
await writeFile(trialBin, `#!/bin/sh\necho 'search-hotel: MCP HTTP 429: Body: {"jsonrpc":"2.0","id":"1","error":{"code":-32603,"message":"Trial limit reached. Please visit the console at flyai.open.fliggy.com to get a formal API Key"}}' >&2\nexit 1\n`, { mode: 0o755 });
|
|
556
|
+
const t = await flyaiSearch({ ...base, cliBin: trialBin, timeoutMs: 5000 });
|
|
557
|
+
assert.equal(t.ok, false);
|
|
558
|
+
assert.equal(t.verdict, 'needs-setup', `429 达限应判 needs-setup,实际 ${t.verdict}`);
|
|
559
|
+
assert.match(t.setup ?? '', /gotry setup flyai|FLYAI_API_KEY/, 'setup 指引带本机配置路径');
|
|
560
|
+
assert.match(t.setup ?? '', /flyai\.open\.fliggy\.com/, 'setup 指引带控制台入口');
|
|
561
|
+
assert.match(t.setup ?? '', /请勿.*重试|勿.*重试|不要重试/, 'setup 明示本会话勿重试');
|
|
562
|
+
assert.match(t.error ?? '', /429|Trial limit/i, 'error 保留上游 429 原话');
|
|
563
|
+
assert.match(t.evidence, /\[实时API:flyai@error@/, '证据链标注');
|
|
564
|
+
assert.equal(t.retryable, false, 'trial 429 不得标记 retryable');
|
|
565
|
+
const ordinaryRateBin = await fakeCliStreams('flyai-ordinary-rate', 1, '', 'MCP HTTP 429: rate limited; retry later');
|
|
566
|
+
const ordinaryRate = await flyaiSearch({ ...base, cliBin: ordinaryRateBin, timeoutMs: 5000 });
|
|
567
|
+
assert.equal(ordinaryRate.verdict, 'rate-limited', '普通 429 应保持 rate-limited');
|
|
568
|
+
assert.equal(ordinaryRate.retryable, true, '普通 429 应标记 retryable');
|
|
569
|
+
const networkBin = await fakeCliStreams('flyai-network-transient', 1, '', 'fetch failed: ECONNRESET');
|
|
570
|
+
const networkTransient = await flyaiSearch({ ...base, cliBin: networkBin, timeoutMs: 5000 });
|
|
571
|
+
assert.equal(networkTransient.verdict, 'error', '网络错误保持结构化 error');
|
|
572
|
+
assert.equal(networkTransient.retryable, true, '普通网络错误应标记 retryable');
|
|
573
|
+
// 15. 真实 CLI 进程边界:config/env key 与 DEBUG endpoint 敏感段不能进入 error/raw/AI 输出。
|
|
574
|
+
// 这里仍用本地 fixture CLI,但走 adapter 的真实 HOME/env 解析路径,不触碰用户配置。
|
|
575
|
+
const previousFlyaiEnv = {
|
|
576
|
+
HOME: process.env.HOME,
|
|
577
|
+
FLYAI_API_KEY: process.env.FLYAI_API_KEY,
|
|
578
|
+
DEBUG_FLYAI_API_KEY: process.env.DEBUG_FLYAI_API_KEY,
|
|
579
|
+
DEBUG_FLYAI_MCP_URL: process.env.DEBUG_FLYAI_MCP_URL,
|
|
580
|
+
};
|
|
581
|
+
const sensitiveHome = await mkdtemp(join(tmpdir(), 'flyai-sensitive-home-'));
|
|
582
|
+
const configKey = 'config-secret-key-521';
|
|
583
|
+
const envKey = 'env-secret-key-521';
|
|
584
|
+
const debugEndpoint = 'https://alice:password@example.test/mcp?token=debug-query&x=1';
|
|
585
|
+
const restoreFlyaiEnv = () => {
|
|
586
|
+
for (const [name, value] of Object.entries(previousFlyaiEnv)) {
|
|
587
|
+
if (value === undefined)
|
|
588
|
+
delete process.env[name];
|
|
589
|
+
else
|
|
590
|
+
process.env[name] = value;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
try {
|
|
594
|
+
await mkdir(join(sensitiveHome, '.flyai'), { recursive: true });
|
|
595
|
+
await writeFile(join(sensitiveHome, '.flyai', 'config.json'), JSON.stringify({ FLYAI_API_KEY: configKey }));
|
|
596
|
+
process.env.HOME = sensitiveHome;
|
|
597
|
+
delete process.env.FLYAI_API_KEY;
|
|
598
|
+
delete process.env.DEBUG_FLYAI_API_KEY;
|
|
599
|
+
process.env.DEBUG_FLYAI_MCP_URL = debugEndpoint;
|
|
600
|
+
const configLeakBin = await fakeCliStreams('flyai-config-leak', 1, `stdout key=${configKey} endpoint=${debugEndpoint}`, `stderr key=${configKey} endpoint=${debugEndpoint} HTTP 500`);
|
|
601
|
+
const configLeak = await flyaiSearch({ ...base, cliBin: configLeakBin, timeoutMs: 5000 });
|
|
602
|
+
const configLeakText = JSON.stringify(configLeak);
|
|
603
|
+
assert.equal(configLeak.keySource, 'config', 'config key 应成为实际来源标记');
|
|
604
|
+
assert.equal(configLeak.verdict, 'error');
|
|
605
|
+
assert.equal(configLeak.retryable, true, 'HTTP 500 应标记 retryable,供 effect 层消费');
|
|
606
|
+
assert.ok(!configLeakText.includes(configKey), 'config key 不得进入 error/raw 输出');
|
|
607
|
+
assert.ok(!configLeakText.includes('alice:password'), 'DEBUG endpoint userinfo 不得进入 error/raw 输出');
|
|
608
|
+
assert.ok(!configLeakText.includes('token=debug-query'), 'DEBUG endpoint query 不得进入 error/raw 输出');
|
|
609
|
+
assert.match(configLeak.error ?? '', /已隐藏 endpoint 敏感参数/, '错误应保留 endpoint 已脱敏提示');
|
|
610
|
+
process.env.FLYAI_API_KEY = envKey;
|
|
611
|
+
const envLeakBin = await fakeCliStreams('flyai-env-leak', 1, `stdout key=${envKey} endpoint=${debugEndpoint}`, `stderr key=${envKey} endpoint=${debugEndpoint} HTTP 401 Invalid API key`);
|
|
612
|
+
const envLeak = await flyaiSearch({ ...base, cliBin: envLeakBin, timeoutMs: 5000 });
|
|
613
|
+
const envLeakText = JSON.stringify(envLeak);
|
|
614
|
+
assert.equal(envLeak.keySource, 'env', 'env key 应覆盖 config 来源');
|
|
615
|
+
assert.equal(envLeak.verdict, 'auth-error', 'HTTP 401 应保持 auth-error');
|
|
616
|
+
assert.equal(envLeak.retryable, false, 'HTTP 401 不得标记 retryable');
|
|
617
|
+
assert.ok(!envLeakText.includes(envKey), 'env key 不得进入 error/raw 输出');
|
|
618
|
+
assert.ok(!envLeakText.includes('alice:password'), 'env error 不得包含 endpoint userinfo');
|
|
619
|
+
assert.ok(!envLeakText.includes('token=debug-query'), 'env error 不得包含 endpoint query');
|
|
620
|
+
const forbiddenBin = await fakeCliStreams('flyai-forbidden', 1, '', `endpoint=${debugEndpoint} HTTP 403 Forbidden`);
|
|
621
|
+
const forbidden = await flyaiSearch({ ...base, cliBin: forbiddenBin, timeoutMs: 5000 });
|
|
622
|
+
assert.equal(forbidden.verdict, 'forbidden', 'HTTP 403 应保持 forbidden');
|
|
623
|
+
assert.equal(forbidden.retryable, false, 'HTTP 403 不得标记 retryable');
|
|
624
|
+
const aiLeakBin = await fakeCli('flyai-ai-leak', 0, JSON.stringify({
|
|
625
|
+
data: { answer: envKey, endpoint: debugEndpoint },
|
|
626
|
+
systemMessage: `模型提示 key=${envKey} endpoint=${debugEndpoint}`,
|
|
627
|
+
}));
|
|
628
|
+
const aiLeak = await flyaiSearch({ kind: 'ai', query: '敏感输出回归', cliBin: aiLeakBin, timeoutMs: 5000 });
|
|
629
|
+
const aiLeakText = JSON.stringify(aiLeak);
|
|
630
|
+
assert.equal(aiLeak.verdict, 'hit');
|
|
631
|
+
assert.ok(!aiLeakText.includes(envKey), 'AI data/systemMessage 不得包含 env key');
|
|
632
|
+
assert.ok(!aiLeakText.includes('alice:password'), 'AI data/systemMessage 不得包含 endpoint userinfo');
|
|
633
|
+
assert.ok(!aiLeakText.includes('token=debug-query'), 'AI data/systemMessage 不得包含 endpoint query');
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
restoreFlyaiEnv();
|
|
637
|
+
await rm(sensitiveHome, { recursive: true, force: true });
|
|
638
|
+
}
|
|
639
|
+
console.log('15. config/env key + DEBUG endpoint userinfo/query 脱敏(error/raw/AI 输出)与 HTTP 5xx retryable 标记OK');
|
|
640
|
+
// 16. Official CLI 1.0.16 captured-shape replay (sanitized, offline). These
|
|
641
|
+
// are the smallest representatives of the seven successful captures; the
|
|
642
|
+
// fields stay strict so a changed shape remains a visible malformed result.
|
|
643
|
+
async function replayOfficialShape(name, query, item) {
|
|
644
|
+
const cli = await fakeCli(`official-shape-${name}`, 0, JSON.stringify({ data: { itemList: [item] }, status: 0, message: 'success' }));
|
|
645
|
+
return await flyaiSearch({ ...query, cliBin: cli, timeoutMs: 5000 });
|
|
646
|
+
}
|
|
647
|
+
const replayFlight = await replayOfficialShape('flight', base, {
|
|
648
|
+
journeys: [{ segments: [{ depDateTime: '2026-10-04 08:15:00', arrDateTime: '2026-10-04 10:25:00', depStationName: '浦东国际机场', arrStationName: '大兴国际机场', duration: '130', marketingTransportName: '中联航', marketingTransportNo: 'KN5978', seatClassName: '经济舱' }] }], ticketPrice: '400.00', jumpUrl: 'https://router.feizhu.com/multi/webview?url=https%3A%2F%2Frouter.feizhu.com%2Fws%2F1hfppp',
|
|
649
|
+
});
|
|
650
|
+
assert.equal(replayFlight.verdict, 'hit');
|
|
651
|
+
const replayTransfer = await replayOfficialShape('flight-transfer', base, {
|
|
652
|
+
journeys: [{ journeyType: '中转', totalDuration: '400', segments: [
|
|
653
|
+
{ depDateTime: '2026-10-04 08:15:00', arrDateTime: '2026-10-04 10:25:00', depStationName: '浦东国际机场', arrStationName: '首都国际机场', duration: '130', marketingTransportName: '中联航', marketingTransportNo: 'KN5978', seatClassName: '经济舱' },
|
|
654
|
+
{ depDateTime: '2026-10-04 12:00:00', arrDateTime: '2026-10-04 14:45:00', depStationName: '首都国际机场', arrStationName: '大兴国际机场', duration: '165', marketingTransportName: '国航', marketingTransportNo: 'CA1234', seatClassName: '经济舱' },
|
|
655
|
+
] }], ticketPrice: '800.00', jumpUrl: 'https://router.feizhu.com/multi/webview?url=ws-transfer',
|
|
656
|
+
});
|
|
657
|
+
assert.equal(replayTransfer.verdict, 'hit');
|
|
658
|
+
assert.equal(replayTransfer.options?.[0]?.nonstop, false, '官方中转 journey 必须显式 nonstop=false');
|
|
659
|
+
assert.equal(replayTransfer.options?.[0]?.depStation, '浦东国际机场');
|
|
660
|
+
assert.equal(replayTransfer.options?.[0]?.arrStation, '大兴国际机场');
|
|
661
|
+
assert.equal(replayTransfer.options?.[0]?.durationMin, 400, '优先采用官方 totalDuration');
|
|
662
|
+
assert.equal(replayTransfer.options?.[0]?.segments?.length, 2, '保留完整分段结构');
|
|
663
|
+
const replayTrain = await replayOfficialShape('train', trainBase, {
|
|
664
|
+
journeys: [{ segments: [{ depDateTime: '2026-09-27 06:13:00', arrDateTime: '2026-09-27 07:07:00', depStationName: '上海虹桥站', arrStationName: '杭州东站', duration: '54', marketingTransportName: '高铁', marketingTransportNo: 'G7331', seatClassName: '二等座' }] }], price: '5x', jumpUrl: 'https://router.feizhu.com/multi/webview?url=https%3A%2F%2Frouter.feizhu.com%2Fws%2F4KUGGZ',
|
|
665
|
+
});
|
|
666
|
+
assert.equal(replayTrain.verdict, 'hit');
|
|
667
|
+
assert.equal(replayTrain.options?.[0]?.priceRaw, '5x', '官方打码 train price 原样保留');
|
|
668
|
+
const replayHotel = await replayOfficialShape('hotel', hotelBase, {
|
|
669
|
+
name: '杭州滨江万豪万枫酒店', shid: '81643030', nearbyPoi: '近浙江中医药大学(滨文校区)', price: '¥360起/晚', star: '高档型', address: '浙江省杭州市滨江区浦沿路773号', mainPic: 'https://img.example.test/hotel.jpg', detailUrl: 'https://router.feizhu.com/multi/webview?url=ws',
|
|
670
|
+
});
|
|
671
|
+
assert.equal(replayHotel.verdict, 'hit');
|
|
672
|
+
assert.equal(replayHotel.hotels?.[0]?.hotelId, '81643030');
|
|
673
|
+
assert.equal(replayHotel.hotels?.[0]?.poi, '近浙江中医药大学(滨文校区)');
|
|
674
|
+
assert.equal(replayHotel.hotels?.[0]?.priceRaw, '¥360起/晚', '官方万豪酒店价格单位原样保留');
|
|
675
|
+
const replayPoi = await replayOfficialShape('poi', { kind: 'poi', cityName: '杭州' }, { id: '1887', name: '飞来峰', mainPic: 'https://img.example.test/poi.jpg', jumpUrl: 'https://router.feizhu.com/multi/webview?url=ws', address: '浙江省杭州市西湖区', freePoiStatus: 'FREE', ticketInfo: { price: '¥6x', priceDate: null, ticketName: '大门票(随买随用) 成人票' } });
|
|
676
|
+
assert.equal(replayPoi.verdict, 'hit');
|
|
677
|
+
const replayKeyword = await replayOfficialShape('keyword', { kind: 'keyword', query: '西湖' }, { info: { title: '西湖风景名胜区', jumpUrl: 'https://router.feizhu.com/multi/webview?url=ws', picUrl: 'https://img.example.test/keyword.jpg', price: null, scoreDesc: null, star: null, tags: null } });
|
|
678
|
+
assert.equal(replayKeyword.verdict, 'hit');
|
|
679
|
+
const replayMarriottHotel = await replayOfficialShape('marriott-hotel', { kind: 'marriott-hotel', destName: '杭州' }, { name: '杭州滨江万豪万枫酒店', shid: '81643030', nearbyPoi: '近浙江中医药大学(滨文校区)', price: '¥360起/晚', star: '高档型', address: '浙江省杭州市滨江区浦沿路773号', mainPic: 'https://img.example.test/marriott.jpg', detailUrl: 'https://router.feizhu.com/multi/webview?url=ws' });
|
|
680
|
+
assert.equal(replayMarriottHotel.verdict, 'hit');
|
|
681
|
+
const replayPackage = await replayOfficialShape('marriott-package', { kind: 'marriott-package', keyword: '杭州' }, { itemId: '1079279344842', title: '杭州临安青山湖科技城万丽酒店2晚连住+双早', picUrl: 'https://img.example.test/package.jpg', sellPoint: '酒店位于狮山公园翠色环抱之中', price: '¥1199起/2晚', detailUrl: 'https://router.feizhu.com/multi/webview?url=ws' });
|
|
682
|
+
assert.equal(replayPackage.verdict, 'hit');
|
|
683
|
+
assert.equal(replayPackage.packages?.[0]?.price, '¥1199起/2晚');
|
|
684
|
+
assert.equal(replayPackage.packages?.[0]?.itemId, '1079279344842');
|
|
685
|
+
const replayAiCli = await fakeCli('official-shape-ai', 0, JSON.stringify({ data: '杭州西湖景区精选推荐', message: 'success', status: 0, systemMessage: '*当前为体验模式*' }));
|
|
686
|
+
const replayAi = await flyaiSearch({ kind: 'ai', query: '杭州周末两日旅行建议', cliBin: replayAiCli, timeoutMs: 5000 });
|
|
687
|
+
assert.equal(replayAi.verdict, 'hit');
|
|
688
|
+
assert.equal(replayAi.aiData, '杭州西湖景区精选推荐');
|
|
689
|
+
assert.match(replayAi.systemMessage ?? '', /体验模式/);
|
|
690
|
+
console.log('16. Official CLI 1.0.16 captured-shape replay → 8 类(含 AI bounded data string)OK');
|
|
691
|
+
await rm(tmp, { recursive: true, force: true });
|
|
692
|
+
console.log('FLYAI TESTS: transport/hotel completeness + error contract OK(离线假 CLI:Sentinel→error / 空 itemList→miss / flight+train+hotel mixed→整体 error 且不落事实 / typed 字段校验 / 完整 flight+train+hotel→hit / exit≠0→error / 429→needs-setup / transient retryable / 敏感信息脱敏)');
|