@danceiny/gotry 0.0.1-rc.9 → 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 -219
- 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 -56
- 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 +947 -110
- 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 +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
- 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 +35 -54
- 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 +31 -21
- 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 +75 -24
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +97 -83
- 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 +635 -44
- 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 +24 -59
- 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 +50 -11
- 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 +25 -70
- 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 +382 -246
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- 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 +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 +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 +39 -28
- 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 +20 -20
- package/dist/scripts/session-attach-poc.js +34 -37
- 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 +14 -35
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +27 -46
- package/dist/scripts/session-tests.js +796 -96
- 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 +690 -246
- 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 +226 -101
- package/dist/scripts/state-cli.js +550 -199
- 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 +29 -93
- 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 +48 -74
- 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 +2227 -882
- 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 +319 -160
- 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 +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 +435 -516
- 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 +44 -63
- 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 +535 -359
- 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 +293 -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 +1948 -198
- 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 +223 -25
- 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 +354 -88
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扩展传输车道合同测试(§38;RFC §2.2 通道 C,2026-08-29 PRIMARY)。
|
|
3
|
+
*
|
|
4
|
+
* 全离线确定性:临时端口桥 + 进程内假扩展客户端(直 fetch 桥端点),不开浏览器、
|
|
5
|
+
* 不碰登录态、不写共享状态(审计走 mkdtemp 隔离)。唯一慢例 = needs-extension 全链
|
|
6
|
+
* (等扩展连接宽限 ~6s——这正是「零花费 no-spend」语义本身)。
|
|
7
|
+
*
|
|
8
|
+
* 三类断言:
|
|
9
|
+
* ① 合同面:manifest(MV3/key→固定 ID 派生/host_permissions 精确面/双 world content_scripts)
|
|
10
|
+
* 与 Node 常量防漂移(BRIDGE_PORTS/LOGIN_COOKIE_NAMES/NETWORK_HINTS/flights.ctrip.com);
|
|
11
|
+
* ② 桥行为:origin 白名单(邪恶源 403)/ 长轮询取活幂等 / 心跳判定 / 提交-回包闭环 /
|
|
12
|
+
* 超时与扩展未连接(有界等待 no-spend)/ close 结算;
|
|
13
|
+
* ③ 车道语义:classifyBridgeFailure(只有 extension-not-connected 是用户门)/
|
|
14
|
+
* resolveTransportMode(扩展默认,cdp 显式 opt-in)/ 审计 JSONL /
|
|
15
|
+
* needs-extension 全链 + waiting_extension 双源合同(no_spend_waiting_user)。
|
|
16
|
+
*/
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { fork } from 'node:child_process';
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { runInNewContext } from 'node:vm';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { BRIDGE_PORTS, EXTENSION_ID, EXTENSION_ID_STORE, EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE, EXTENSION_ORIGINS, EXTENSION_STORE_URL, createSessionBridge, localExtensionInstalled, needsExtensionSummary, __resetSessionBridgeForTest, __setSessionBridgeForTest, } from '../capabilities/session/extension-bridge.js';
|
|
26
|
+
import { classifyBridgeFailure, extensionCookieNames, extensionOpenLogin, extensionSearchJob, } from '../capabilities/session/extension-channel.js';
|
|
27
|
+
import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
|
|
28
|
+
import { sessionLogin } from '../capabilities/session-login.js';
|
|
29
|
+
import { LOGIN_COOKIE_NAMES, NETWORK_HINTS, SITE_DOMAIN } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
30
|
+
import { HOTEL_NETWORK_HINTS, HOTEL_SITE_HOST } from '../capabilities/session/adapters/ctrip-hotel.js';
|
|
31
|
+
import { TRAIN_NETWORK_HINTS, TRAIN_SITE_HOST } from '../capabilities/session/adapters/rail-12306.js';
|
|
32
|
+
import { DIDA_NETWORK_HINTS, DIDA_LOGIN_COOKIE_NAMES, DIDA_SITE_HOST } from '../capabilities/session/adapters/dida-portal.js';
|
|
33
|
+
import { evaluateDoubleSource } from '../capabilities/session/benchmark.js';
|
|
34
|
+
import { BRIDGE_EVENTS_PATH } from '../src/backend/modules/session-search.js';
|
|
35
|
+
import { factsFromSessionTrain } from '../src/bookable-facts.js';
|
|
36
|
+
const EXT_DIR = fileURLToPath(new URL('../../extension/', import.meta.url));
|
|
37
|
+
const UNREF_CHILD = fileURLToPath(new URL('./fixtures/extension-bridge-unref-child.mjs', import.meta.url));
|
|
38
|
+
const read = (f) => readFileSync(join(EXT_DIR, f), 'utf8');
|
|
39
|
+
async function contentMainRelativeResponseUrlProof(source) {
|
|
40
|
+
const events = [];
|
|
41
|
+
const window = {
|
|
42
|
+
__gotrySniffInstalled: false,
|
|
43
|
+
fetch: async function (_input, _init) {
|
|
44
|
+
return {
|
|
45
|
+
url: 'https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2026-12-01&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM',
|
|
46
|
+
headers: { get: () => null },
|
|
47
|
+
clone: () => ({ text: async () => '{"data":{"result":[]}}' }),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
dispatchEvent: (event) => {
|
|
51
|
+
events.push({ url: String(event.detail?.url ?? ''), body: String(event.detail?.body ?? '') });
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
class FixtureXhr {
|
|
55
|
+
responseType = '';
|
|
56
|
+
responseText = '{"data":{"result":[]}}';
|
|
57
|
+
response = this.responseText;
|
|
58
|
+
responseURL = 'https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2026-12-01&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM';
|
|
59
|
+
listeners = [];
|
|
60
|
+
addEventListener(event, handler) {
|
|
61
|
+
if (event === 'load')
|
|
62
|
+
this.listeners.push(handler);
|
|
63
|
+
}
|
|
64
|
+
open(_method, _url) { }
|
|
65
|
+
send(_body) {
|
|
66
|
+
for (const handler of this.listeners)
|
|
67
|
+
handler();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
class FixtureCustomEvent {
|
|
71
|
+
detail;
|
|
72
|
+
constructor(_type, init) { this.detail = init.detail; }
|
|
73
|
+
}
|
|
74
|
+
;
|
|
75
|
+
window.addEventListener = () => { };
|
|
76
|
+
runInNewContext(source, { window, location: { hostname: 'kyfw.12306.cn' }, XMLHttpRequest: FixtureXhr, CustomEvent: FixtureCustomEvent });
|
|
77
|
+
await window.fetch('/otn/leftTicket/queryG', {});
|
|
78
|
+
const xhr = new FixtureXhr();
|
|
79
|
+
xhr.open('GET', '/otn/leftTicket/queryG');
|
|
80
|
+
xhr.send();
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
82
|
+
return events;
|
|
83
|
+
}
|
|
84
|
+
let passed = 0;
|
|
85
|
+
async function check(label, assertion) {
|
|
86
|
+
await assertion();
|
|
87
|
+
passed += 1;
|
|
88
|
+
console.log(` ok - ${label}`);
|
|
89
|
+
}
|
|
90
|
+
const mustBridge = async (ports) => {
|
|
91
|
+
// 缺省 EXTENSION_ORIGINS(unpacked + 商店版双通道)——行为测试直接覆盖生产默认白名单
|
|
92
|
+
const created = await createSessionBridge({ ports });
|
|
93
|
+
if (!created.ok)
|
|
94
|
+
throw new Error(created.summary);
|
|
95
|
+
return created.bridge;
|
|
96
|
+
};
|
|
97
|
+
/** 假扩展客户端:一次长轮询取活 + 回包(Origin=固定扩展源,过桥白名单) */
|
|
98
|
+
async function claimOnce(port, respond, signal, capabilities, origin = EXTENSION_ORIGIN, clientId) {
|
|
99
|
+
const r = await fetch(`http://127.0.0.1:${port}/jobs`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: { 'content-type': 'application/json', origin },
|
|
102
|
+
body: JSON.stringify({ extensionVersion: 'test', ...(capabilities ? { capabilities } : {}), ...(clientId ? { clientId } : {}) }),
|
|
103
|
+
signal,
|
|
104
|
+
});
|
|
105
|
+
const data = (await r.json());
|
|
106
|
+
if (data.job && respond) {
|
|
107
|
+
const result = respond(data.job);
|
|
108
|
+
if (result) {
|
|
109
|
+
await fetch(`http://127.0.0.1:${port}/results/${encodeURIComponent(data.job.jobId)}`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'content-type': 'application/json', origin, ...(clientId ? { 'x-gotry-client-id': clientId } : {}) },
|
|
112
|
+
body: JSON.stringify(result),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
async function heartbeat(port) {
|
|
119
|
+
// 带扩展 Origin(与浏览器扩展 SW fetch 同形;无 Origin 的 /health 只是诊断,不记心跳)
|
|
120
|
+
await fetch(`http://127.0.0.1:${port}/health`, { headers: { origin: EXTENSION_ORIGIN } });
|
|
121
|
+
}
|
|
122
|
+
async function waitForParkedCount(port, expected, timeoutMs = 1_000) {
|
|
123
|
+
const deadline = Date.now() + timeoutMs;
|
|
124
|
+
while (Date.now() < deadline) {
|
|
125
|
+
const r = await fetch(`http://127.0.0.1:${port}/status`);
|
|
126
|
+
const body = (await r.json());
|
|
127
|
+
if (body.parked === expected)
|
|
128
|
+
return;
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`/status 未在 ${timeoutMs}ms 内报告 parked=${expected}`);
|
|
132
|
+
}
|
|
133
|
+
async function assertParkedClientDoesNotPinHost() {
|
|
134
|
+
const child = fork(UNREF_CHILD, [], {
|
|
135
|
+
execArgv: process.execArgv,
|
|
136
|
+
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
137
|
+
});
|
|
138
|
+
let stderr = '';
|
|
139
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
140
|
+
let parkedRequest = null;
|
|
141
|
+
try {
|
|
142
|
+
const port = await new Promise((resolve, reject) => {
|
|
143
|
+
const timer = setTimeout(() => reject(new Error('子进程扩展桥未在 2s 内监听')), 2_000);
|
|
144
|
+
child.once('message', (message) => {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
const data = message;
|
|
147
|
+
if (data.kind === 'listening' && typeof data.port === 'number')
|
|
148
|
+
resolve(data.port);
|
|
149
|
+
else
|
|
150
|
+
reject(new Error(data.summary ?? `子进程消息异常:${JSON.stringify(data)}`));
|
|
151
|
+
});
|
|
152
|
+
child.once('exit', (code, signal) => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
reject(new Error(`子进程过早退出 code=${code} signal=${signal} ${stderr}`));
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
parkedRequest = claimOnce(port, null).catch(() => ({ job: null }));
|
|
158
|
+
await waitForParkedCount(port, 1);
|
|
159
|
+
const exitP = new Promise((resolve, reject) => {
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
child.kill();
|
|
162
|
+
reject(new Error('默认桥被 parked /jobs 长轮询钉住超过 1500ms'));
|
|
163
|
+
}, 1_500);
|
|
164
|
+
child.once('exit', (code, signal) => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
resolve({ code, signal });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
child.send({ kind: 'release' });
|
|
170
|
+
const exited = await exitP;
|
|
171
|
+
assert.equal(exited.code, 0, `子进程应自然退出:${stderr}`);
|
|
172
|
+
assert.equal(exited.signal, null);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
176
|
+
child.kill();
|
|
177
|
+
if (parkedRequest)
|
|
178
|
+
await parkedRequest;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const stripRe = (s) => s.replace(/\\/g, '');
|
|
182
|
+
async function main() {
|
|
183
|
+
console.log('§38 扩展传输车道合同(extension bridge,全离线)');
|
|
184
|
+
/* ---------- ① manifest 合同与防漂移 ---------- */
|
|
185
|
+
const manifest = JSON.parse(read('manifest.json'));
|
|
186
|
+
const derivedId = createHash('sha256')
|
|
187
|
+
.update(Buffer.from(manifest.key, 'base64'))
|
|
188
|
+
.digest()
|
|
189
|
+
.subarray(0, 16)
|
|
190
|
+
.toString('hex')
|
|
191
|
+
.replace(/[0-9a-f]/g, (c) => 'abcdefghijklmnop'[Number.parseInt(c, 16)]);
|
|
192
|
+
await check('manifest key 派生扩展 ID = Node 侧固定 EXTENSION_ID(unpacked 通道 origin 白名单锚点)', () => {
|
|
193
|
+
assert.equal(derivedId, EXTENSION_ID);
|
|
194
|
+
assert.equal(EXTENSION_ORIGIN, `chrome-extension://${EXTENSION_ID}`);
|
|
195
|
+
});
|
|
196
|
+
await check('双通道白名单:商店版 ID(CWS 重签,不认 manifest key)与 unpacked 固定 ID 同信,商店 URL 锚定 item ID', () => {
|
|
197
|
+
assert.equal(EXTENSION_ORIGIN_STORE, `chrome-extension://${EXTENSION_ID_STORE}`);
|
|
198
|
+
assert.ok(EXTENSION_ORIGINS.includes(EXTENSION_ORIGIN));
|
|
199
|
+
assert.ok(EXTENSION_ORIGINS.includes(EXTENSION_ORIGIN_STORE));
|
|
200
|
+
assert.equal(EXTENSION_ORIGINS.length, 2);
|
|
201
|
+
assert.ok(EXTENSION_STORE_URL.includes(EXTENSION_ID_STORE));
|
|
202
|
+
});
|
|
203
|
+
await check('manifest 合同:MV3 + 最小权限(cookies/alarms;无 storage/options_page/tabs;founder 2026-09-11 不向员工暴露任何配置面)', () => {
|
|
204
|
+
assert.equal(manifest.manifest_version, 3);
|
|
205
|
+
assert.deepEqual([...manifest.permissions].sort(), ['alarms', 'cookies']);
|
|
206
|
+
assert.ok(!manifest.permissions.includes('storage'), '不得用 storage 持久化配置(配置由 portal 静默派发)');
|
|
207
|
+
assert.equal(manifest.options_page, undefined, '不得挂 options 页(employee-facing UI 全禁)');
|
|
208
|
+
assert.equal(manifest.optional_host_permissions, undefined, '门户只签发同源桥路径,未调用运行时授权 API,不得保留全网可选权限');
|
|
209
|
+
});
|
|
210
|
+
const manifestPorts = manifest.host_permissions
|
|
211
|
+
.map((p) => /127\.0\.0\.1:(\d+)/.exec(p)?.[1])
|
|
212
|
+
.filter((v) => v != null)
|
|
213
|
+
.map(Number)
|
|
214
|
+
.sort((a, b) => a - b);
|
|
215
|
+
await check('防漂移:桥端口池(Node BRIDGE_PORTS)= manifest host_permissions 回环面(+ ctrip/dida 星域;2026-09-11 保留站点域:founder 不接受单凭 chrome.cookies API 推断,显式授权链可审计)', () => {
|
|
216
|
+
assert.deepEqual(manifestPorts, [...BRIDGE_PORTS].sort((a, b) => a - b));
|
|
217
|
+
assert.ok(manifest.host_permissions.includes('https://*.ctrip.com/*'));
|
|
218
|
+
assert.ok(manifest.host_permissions.includes('https://*.dida.com/*'));
|
|
219
|
+
assert.ok(manifest.host_permissions.includes('https://dida.com/*'));
|
|
220
|
+
});
|
|
221
|
+
await check('防漂移:content_scripts 双 world 挂 ctrip 双站+12306+dida(MAIN 嗅探 + ISOLATED 桥;2026-09-03 酒/火实装,2026-09-09 dida 实装)', () => {
|
|
222
|
+
assert.equal(manifest.content_scripts.length, 2);
|
|
223
|
+
// 2026-09-21 hotel-fe#3713:join 交付(portal 页注入 window.__gotryJoinTicket)要求
|
|
224
|
+
// content-bridge 也跑在 hotelbyte portal 页上;content-main(供应商页 MAIN 嗅探)维持站点面不变。
|
|
225
|
+
const portalOrigins = ['https://portal-test.hotelbyte.com/*', 'https://portal.hotelbyte.com/*'];
|
|
226
|
+
const main = manifest.content_scripts.find((cs) => cs.js[0] === 'content-main.js');
|
|
227
|
+
const bridge = manifest.content_scripts.find((cs) => cs.js[0] === 'content-bridge.js');
|
|
228
|
+
assert.ok(main && bridge, '双 world 脚本齐备');
|
|
229
|
+
const siteMatches = ['https://flights.ctrip.com/*', `https://${HOTEL_SITE_HOST}/*`, `https://${TRAIN_SITE_HOST}/*`, 'https://www.12306.cn/*', `https://${DIDA_SITE_HOST}/*`];
|
|
230
|
+
assert.deepEqual(main.matches, siteMatches);
|
|
231
|
+
assert.deepEqual(bridge.matches, [...siteMatches, ...portalOrigins]);
|
|
232
|
+
for (const cs of manifest.content_scripts)
|
|
233
|
+
assert.equal(cs.run_at, 'document_start');
|
|
234
|
+
const worlds = manifest.content_scripts.map((cs) => cs.world ?? 'ISOLATED').sort();
|
|
235
|
+
assert.deepEqual(worlds, ['ISOLATED', 'MAIN']);
|
|
236
|
+
assert.deepEqual(manifest.content_scripts.map((cs) => cs.js[0]).sort(), ['content-bridge.js', 'content-main.js']);
|
|
237
|
+
});
|
|
238
|
+
const backgroundJs = read('background.js');
|
|
239
|
+
const contentMainJs = read('content-main.js');
|
|
240
|
+
const contentBridgeJs = read('content-bridge.js');
|
|
241
|
+
await check('配置交付(2026-09-11 hotelbyte 口径:员工零配置):portal 派发 join ticket → SW 静默接收;无 storage/options UI', () => {
|
|
242
|
+
assert.ok(!backgroundJs.includes('chrome.storage'), '背景 SW 不得读 chrome.storage(配置由 portal 派发,非持久化)');
|
|
243
|
+
assert.ok(!backgroundJs.includes('chrome.storage.local.get'), '背景 SW 不得列读 storage.local.gotryRemoteBridge');
|
|
244
|
+
assert.ok(!backgroundJs.includes('chrome.storage.onChanged'), '背景 SW 不得订阅 storage 变更');
|
|
245
|
+
assert.ok(backgroundJs.includes('chrome.runtime.onMessage'), '背景 SW 应订阅 runtime.onMessage 接收 portal 派发');
|
|
246
|
+
assert.ok(backgroundJs.includes('\'gotry-join\''), '背景 SW 应识别 gotry-join 消息(portal content-bridge 派发)');
|
|
247
|
+
assert.ok(backgroundJs.includes('joinTicket'), '背景 SW 应有 joinTicket 模块变量缓存 ticket');
|
|
248
|
+
assert.ok(backgroundJs.includes('isJoinFresh'), '背景 SW 应有 join ticket 有效性校验(过期/token 缺失即弃)');
|
|
249
|
+
assert.ok(contentBridgeJs.includes('__gotryJoinTicket'), 'content-bridge 应在页面里读 window.__gotryJoinTicket');
|
|
250
|
+
assert.ok(contentBridgeJs.includes('type: \'gotry-join\''), 'content-bridge 应把 ticket 转 gotry-join 消息派给 SW');
|
|
251
|
+
assert.ok(contentBridgeJs.includes('chrome.runtime.sendMessage'), 'content-bridge 应用 chrome.runtime.sendMessage 派发');
|
|
252
|
+
assert.ok(!backgroundJs.includes('options.html'), '扩展不得引用 options.html/options.js(employee 零配置)');
|
|
253
|
+
});
|
|
254
|
+
await check('安装检测契约(2026-09-24 hotel-fe#3802):content-bridge 在 portal 页给 <body> 打 data-portal-helper-installed 标记,横幅据此隐藏', () => {
|
|
255
|
+
assert.ok(contentBridgeJs.includes('data-portal-helper-installed'), 'content-bridge 应设置 portal 安装检测属性(hotel-fe 横幅的唯一检测面)');
|
|
256
|
+
assert.ok(contentBridgeJs.includes('markInstalled'), '应有 markInstalled 封装(document_start 下 body 常为 null,需兜底)');
|
|
257
|
+
assert.ok(/DOMContentLoaded[^\n]*markInstalled/.test(contentBridgeJs), 'body 未就绪时需 DOMContentLoaded 兜底打标记');
|
|
258
|
+
});
|
|
259
|
+
await check('代填登录(2026-09-21 hotel-fe#3713 形态 A 免密进入门户):portal 投递载荷 → SW 开登录页 → content-bridge 按站点配方填表,账密零持久面', () => {
|
|
260
|
+
assert.ok(backgroundJs.includes("'gotry-portal-login'"), 'SW 应识别 portal 投递的一次性凭据载荷');
|
|
261
|
+
assert.ok(backgroundJs.includes('openLoginAndFill'), 'SW 应有「开登录页 + 派发填表」实现');
|
|
262
|
+
assert.ok(backgroundJs.includes('SITE_SEARCH_PREFIXES[site]'), '登录 URL 必须按站点白名单前缀守域(拒绝任意载荷)');
|
|
263
|
+
assert.ok(backgroundJs.includes("'gotry-fill-login'"), 'SW 应向登录页 content-bridge 派发填表指令');
|
|
264
|
+
assert.ok(contentBridgeJs.includes("'gotry-fill-login'"), 'content-bridge 应响应填表指令');
|
|
265
|
+
assert.ok(contentBridgeJs.includes('setNativeValue'), 'React 受控 input 必须走原型 setter + input/change 事件');
|
|
266
|
+
assert.ok(contentBridgeJs.includes('requestSubmit'), '无提交按钮时应回退 form.requestSubmit');
|
|
267
|
+
assert.ok(contentBridgeJs.includes('__gotryPortalLogin'), 'content-bridge 应读 portal 注入的凭据载荷');
|
|
268
|
+
assert.ok(!backgroundJs.includes('chrome.storage'), '账密不得落持久存储(与 2026-09-11 零配置裁定一致)');
|
|
269
|
+
});
|
|
270
|
+
await check('dida 目的地搜索驱动(2026-09-21):portal 页配方(目的地组首项 + button[name=search])→ 落地 URL 用我们的日期重写', () => {
|
|
271
|
+
assert.ok(contentBridgeJs.includes('runDidaSearch'), 'content-bridge 应有 dida 目的地搜索配方');
|
|
272
|
+
assert.ok(contentBridgeJs.includes('nd-suggestion-result__group'), '配方应取门户联想结果分组(目的地组优先)');
|
|
273
|
+
assert.ok(contentBridgeJs.includes('nd-suggestion-search__label'), '配方应识别分组标题以区分目的地/酒店');
|
|
274
|
+
assert.ok(contentBridgeJs.includes('button[name="search"]'), '配方应点门户查询按钮');
|
|
275
|
+
assert.ok(contentBridgeJs.includes('ant-modal-wrap'), '配方应先关 cookie 授权弹窗(否则点击被遮罩吞掉)');
|
|
276
|
+
assert.ok(backgroundJs.includes("'gotry-dida-search'"), 'SW 应向 content-bridge 下发配方指令');
|
|
277
|
+
assert.ok(backgroundJs.includes("searchParams.set('checkInDate'"), 'SW 应用我们的日期重写落地 URL(不驱动门户日期选择器)');
|
|
278
|
+
assert.ok(backgroundJs.includes('landingUrl'), 'SW 应回传落地 URL(供解析器与诊断)');
|
|
279
|
+
});
|
|
280
|
+
await check('dida 查询态(2026-09-21):searchCache 桶 + 只认 searchCache 的等待器 + 页面级挑战标记', () => {
|
|
281
|
+
assert.ok(backgroundJs.includes("if (/HotelPriceAPI\\/SearchCache/i.test(url)) return 'searchCache'"), 'SW 应把 SearchCache 分到 searchCache 桶');
|
|
282
|
+
assert.ok(backgroundJs.includes('waitSniffSearchCache'), '查询态应有只认 searchCache 的等待器(推荐流不得参与结算)');
|
|
283
|
+
assert.ok(backgroundJs.includes('challenge'), 'SW 应把页面级 challenge 标记随结果回传');
|
|
284
|
+
assert.ok(contentBridgeJs.includes('challengeHint'), 'content-bridge 应按 DOM 判定挑战标记');
|
|
285
|
+
assert.ok(backgroundJs.includes('landingUrl'), '驱动后应回传落地 URL');
|
|
286
|
+
});
|
|
287
|
+
await check('dida multiCollect(2026-09-11,推荐流双接口):背景分类函数 + waitSniffMulti 分桶结算', () => {
|
|
288
|
+
assert.ok(backgroundJs.includes('classifyDidaSniff'), '背景 SW 应抽 dida sniff 分类函数');
|
|
289
|
+
assert.ok(backgroundJs.includes('SearchHomepageRecommendHotels'), '多回包分类应含 hotels 接口');
|
|
290
|
+
assert.ok(backgroundJs.includes('SearchHomepageRecommendPrices'), '多回包分类应含 recommendPrices 接口');
|
|
291
|
+
assert.ok(backgroundJs.includes('waitSniffMulti'), '应有多回包分桶结算版本(双齐即返)');
|
|
292
|
+
assert.ok(backgroundJs.includes('job.multiCollect'), '背景 SW 应按 job.multiCollect 分派单首包/多回包');
|
|
293
|
+
});
|
|
294
|
+
await check('防漂移(workspace 事件上行,批次 B 2026-10-01):content-bridge 监听页面事件名 = SW 消息面 = SW 上行端点 = Node 路由;内存 buffer 零 storage,失败静默丢弃', () => {
|
|
295
|
+
assert.ok(contentBridgeJs.includes('gotry-workspace-event-available'), 'content-bridge 应监听页面派发的 gotry-workspace-event-available 自定义事件');
|
|
296
|
+
assert.ok(contentBridgeJs.includes("type: 'gotry-workspace-event'"), 'content-bridge 应把事件 detail 经 gotry-workspace-event 消息转发 SW');
|
|
297
|
+
assert.ok(backgroundJs.includes("'gotry-workspace-event'"), '背景 SW 应接收 workspace 事件消息并进内存 buffer');
|
|
298
|
+
assert.ok(backgroundJs.includes(BRIDGE_EVENTS_PATH), `背景 SW 上行端点应与 Node 路由逐字一致(${BRIDGE_EVENTS_PATH})`);
|
|
299
|
+
assert.ok(backgroundJs.includes('workspaceEventBuffer'), '背景 SW 应有内存 buffer 模块变量(SW 重启即弃,零 storage)');
|
|
300
|
+
assert.ok(backgroundJs.includes('flushWorkspaceEvents'), '背景 SW 应伴随 /jobs 长轮询批量上行(flushWorkspaceEvents)');
|
|
301
|
+
assert.ok(/await flushWorkspaceEvents\(\)/.test(backgroundJs), 'flush 应挂在 /jobs 长轮询循环里(伴随每次轮询)');
|
|
302
|
+
assert.ok(!backgroundJs.includes('chrome.storage'), '事件 buffer 不得落 chrome.storage(红线:扩展零 storage)');
|
|
303
|
+
});
|
|
304
|
+
await check('版本跟随主版本:manifest version_name = package.json version;version = 四段投影(0.0.1-rc.N → 0.0.1.N,founder 2026-09-03 拍板)', () => {
|
|
305
|
+
const pkgVersion = JSON.parse(readFileSync(join(EXT_DIR, '..', 'package.json'), 'utf8')).version;
|
|
306
|
+
assert.equal(manifest.version_name, pkgVersion, 'version_name 应与 gotry 主版本逐字一致');
|
|
307
|
+
const m = /^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(pkgVersion);
|
|
308
|
+
assert.ok(m, `主版本形态应为 x.y.z-rc.N,实际 ${pkgVersion}`);
|
|
309
|
+
assert.equal(manifest.version, `${m[1]}.${m[2]}.${m[3]}.${m[4]}`, 'version 应为主版本的四段投影(Chrome manifest 只收点分整数)');
|
|
310
|
+
assert.equal(manifest.content_scripts.length, 2);
|
|
311
|
+
});
|
|
312
|
+
await check('防漂移:票据 cookie 名单(Node LOGIN_COOKIE_NAMES)= 扩展 SITES.ticketNames', () => {
|
|
313
|
+
for (const name of LOGIN_COOKIE_NAMES) {
|
|
314
|
+
assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺票据名 ${name}`);
|
|
315
|
+
}
|
|
316
|
+
assert.ok(backgroundJs.includes("domain: 'ctrip.com'"), 'background.js 站点域应与 LOGIN_TARGETS.domain 对账');
|
|
317
|
+
});
|
|
318
|
+
await check('防漂移:NETWORK_HINTS + HOTEL_NETWORK_HINTS(Node)= content-main 嗅探面(MAIN-world)', () => {
|
|
319
|
+
for (const hint of NETWORK_HINTS) {
|
|
320
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 hint ${hint.source}`);
|
|
321
|
+
}
|
|
322
|
+
for (const hint of HOTEL_NETWORK_HINTS) {
|
|
323
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺酒店 hint ${hint.source}`);
|
|
324
|
+
}
|
|
325
|
+
for (const hint of TRAIN_NETWORK_HINTS) {
|
|
326
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺火车 hint ${hint.source}`);
|
|
327
|
+
}
|
|
328
|
+
assert.ok(contentMainJs.includes('12306'), 'content-main.js 应感知 12306 页域(火车嗅探作用域)');
|
|
329
|
+
assert.ok(contentMainJs.includes('hotels\\.ctrip\\.com'), 'content-main.js 应感知酒店页域(形状嗅探兜底的作用域)');
|
|
330
|
+
});
|
|
331
|
+
await check('防漂移:酒店形状签名(Node looksLikeHotelListBody 同款)= content-main HOTEL_BODY_SIG_RE', () => {
|
|
332
|
+
assert.ok(stripRe(contentMainJs).includes('"hotelList"|"hotelMatchInfos"|"hotelName"'), '形状嗅探签名两侧必须逐字一致');
|
|
333
|
+
});
|
|
334
|
+
await check('防漂移:检索/登录 URL 面(background per-site 白名单 flights+hotels;ISOLATED 桥转发两类事件)', () => {
|
|
335
|
+
assert.ok(backgroundJs.includes('https://flights.ctrip.com/'), 'background 应含机票检索白名单');
|
|
336
|
+
assert.ok(backgroundJs.includes(`https://${HOTEL_SITE_HOST}/`), 'background 应含酒店检索白名单(2026-09-03 实装)');
|
|
337
|
+
assert.ok(backgroundJs.includes('https://kyfw.12306.cn/'), 'background 应含火车检索白名单(2026-09-03 实装)');
|
|
338
|
+
assert.ok(backgroundJs.includes("'ctrip-hotel'"), 'background SITES 应注册 ctrip-hotel');
|
|
339
|
+
assert.ok(backgroundJs.includes("'train-12306'"), 'background SITES 应注册 train-12306');
|
|
340
|
+
assert.ok(backgroundJs.includes("'dida-portal'"), 'background SITES 应注册 dida-portal(2026-09-09 实装)');
|
|
341
|
+
assert.ok(backgroundJs.includes('https://portal.dida.com/'), 'background 应含 dida 检索白名单');
|
|
342
|
+
assert.ok(contentMainJs.includes('gotry-ctrip-sniff'));
|
|
343
|
+
assert.ok(contentBridgeJs.includes('gotry-ctrip-sniff'));
|
|
344
|
+
assert.ok(contentBridgeJs.includes('url: d.url'), 'content bridge 应转发嗅探响应 URL');
|
|
345
|
+
assert.ok(backgroundJs.includes('url: String(msg.url ??'), 'background 应保留嗅探响应 URL');
|
|
346
|
+
assert.ok(contentMainJs.includes('typeof res.url === \'string\''), 'content-main fetch 应读取浏览器 response.url');
|
|
347
|
+
assert.ok(contentMainJs.includes('typeof xhr.responseURL === \'string\''), 'content-main XHR 应读取浏览器 responseURL');
|
|
348
|
+
assert.ok(contentBridgeJs.includes('gotry-page'));
|
|
349
|
+
});
|
|
350
|
+
await check('相对 12306 输入 → fetch/XHR 均转发同一响应的绝对 URL 与 body', async () => {
|
|
351
|
+
const events = await contentMainRelativeResponseUrlProof(contentMainJs);
|
|
352
|
+
const expectedUrl = 'https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2026-12-01&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM';
|
|
353
|
+
assert.deepEqual(events, [
|
|
354
|
+
{ url: expectedUrl, body: '{"data":{"result":[]}}' },
|
|
355
|
+
{ url: expectedUrl, body: '{"data":{"result":[]}}' },
|
|
356
|
+
]);
|
|
357
|
+
});
|
|
358
|
+
await check('防漂移(Dida):DIDA_NETWORK_HINTS(Node)= content-main 嗅探面;票据名= background SITES;manifest 覆盖 portal.dida.com', () => {
|
|
359
|
+
for (const hint of DIDA_NETWORK_HINTS) {
|
|
360
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 dida hint ${hint.source}`);
|
|
361
|
+
}
|
|
362
|
+
assert.ok(contentMainJs.includes('portal\\.dida\\.com'), 'content-main.js 应感知 dida 页域');
|
|
363
|
+
for (const name of DIDA_LOGIN_COOKIE_NAMES) {
|
|
364
|
+
assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺 dida 票据名 ${name}`);
|
|
365
|
+
}
|
|
366
|
+
assert.ok(backgroundJs.includes("domain: 'dida.com'"), 'background.js dida 站点域应与 LOGIN_TARGETS.domain 对账');
|
|
367
|
+
const manifestAny = JSON.parse(readFileSync(join(EXT_DIR, 'manifest.json'), 'utf8'));
|
|
368
|
+
assert.ok(manifestAny.host_permissions.includes('https://*.dida.com/*'), 'manifest host_permissions 应含 dida 域');
|
|
369
|
+
assert.ok(manifestAny.content_scripts.every((b) => b.matches.includes(`https://${DIDA_SITE_HOST}/*`)), 'manifest 两组 content_scripts 均应注入 portal.dida.com');
|
|
370
|
+
assert.ok(stripRe(contentMainJs).includes('"HotelPriceList"|"RatePlanList"'), 'dida 形状签名两侧必须逐字一致');
|
|
371
|
+
});
|
|
372
|
+
await check('扩展 fetch 只指向桥(loopback / 门户同源桥 URL / bridgeBase() / joinTicket.bridgeUrl 含 .replace 派生 / 变量别名 url);不用 chrome.debugger;站点域一律 zero fetch', () => {
|
|
373
|
+
for (const [name, src] of [['background', backgroundJs], ['content-main', contentMainJs], ['content-bridge', contentBridgeJs]]) {
|
|
374
|
+
// joinTicket.bridgeUrl 分支用 [^}]* 而非 [^)]*:允许同一变量的 .replace(...) 等纯派生表达式(如去尾斜杠),
|
|
375
|
+
// 派生链一旦写出嵌套 `${}` 或新目标即不再命中——只放宽对同一变量的匹配,不放宽目标集合(fail-closed)。
|
|
376
|
+
const bridgeFetches = src.match(/fetch\(`?(?:http:\/\/127\.0\.0\.1|\$\{bridgeBase\(\)\}|\$\{remoteBridge\.baseUrl\}|\$\{joinTicket\.bridgeUrl[^}]*\})|\bfetch\(\s*url\b/g) ?? [];
|
|
377
|
+
const allFetches = src.match(/fetch\(/g) ?? [];
|
|
378
|
+
assert.equal(bridgeFetches.length, allFetches.length, `${name}: fetch 必须只指向桥;门户另有登录代填和搜索控件操作`);
|
|
379
|
+
}
|
|
380
|
+
assert.ok(!backgroundJs.includes('chrome.debugger'), '扩展不得使用 chrome.debugger(警告条/调试面)');
|
|
381
|
+
});
|
|
382
|
+
/* ---------- ② 桥行为(临时端口 + 假扩展客户端) ---------- */
|
|
383
|
+
const b = await mustBridge([0]);
|
|
384
|
+
const port = b.port;
|
|
385
|
+
await check('桥只绑回环临时端口;/health 无 Origin 可诊断', async () => {
|
|
386
|
+
assert.ok(port > 0);
|
|
387
|
+
const r = await fetch(`http://127.0.0.1:${port}/health`);
|
|
388
|
+
const body = (await r.json());
|
|
389
|
+
assert.equal(r.status, 200);
|
|
390
|
+
assert.equal(body.ok, true);
|
|
391
|
+
assert.equal(body.service, 'gotry-session-bridge');
|
|
392
|
+
});
|
|
393
|
+
await check('默认桥:parked 扩展长轮询不得钉住宿主 CLI 进程', assertParkedClientDoesNotPinHost);
|
|
394
|
+
await check('origin 白名单:邪恶源(网页跨域必带)POST /jobs 与 /results 一律 403', async () => {
|
|
395
|
+
for (const path of ['/jobs', '/results/whatever']) {
|
|
396
|
+
const r = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: { 'content-type': 'application/json', origin: 'https://evil.example' },
|
|
399
|
+
body: '{}',
|
|
400
|
+
});
|
|
401
|
+
assert.equal(r.status, 403, path);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
await check('origin 白名单:商店版扩展源(CWS 重签 ID,2026-09-02 上架)POST 放行且计入心跳', async () => {
|
|
405
|
+
const storeLane = await mustBridge([0]);
|
|
406
|
+
try {
|
|
407
|
+
assert.equal(storeLane.extensionConnected(), false);
|
|
408
|
+
const r = await fetch(`http://127.0.0.1:${storeLane.port}/results/whatever`, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN_STORE },
|
|
411
|
+
body: '{}',
|
|
412
|
+
});
|
|
413
|
+
// 批次 A(2026-10-01)起未知 jobId 回包 = 404 unknown-job(不再 200 静默丢弃);
|
|
414
|
+
// 本断言守住的原不变量是「商店版源不得吃 403」——403 会整条商店通道全断。
|
|
415
|
+
assert.notEqual(r.status, 403, '商店版扩展源不得吃 403(否则商店通道全断)');
|
|
416
|
+
assert.equal(r.status, 404, '未知 jobId 回包按批次 A 合同回 404 unknown-job');
|
|
417
|
+
assert.equal((await r.json()).error, 'unknown-job');
|
|
418
|
+
assert.equal(storeLane.extensionConnected(), true, '白名单源的任何请求都刷新心跳');
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
await storeLane.close();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
await check('已领取作业的回包只接受领取它的扩展 Origin', async () => {
|
|
425
|
+
const lane = await mustBridge([0]);
|
|
426
|
+
try {
|
|
427
|
+
const pending = lane.submit({ kind: 'search', site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01' });
|
|
428
|
+
const claimed = await claimOnce(lane.port, null);
|
|
429
|
+
assert.equal(claimed.job?.kind, 'search');
|
|
430
|
+
const target = `http://127.0.0.1:${lane.port}/results/${encodeURIComponent(claimed.job.jobId)}`;
|
|
431
|
+
const forged = await fetch(target, {
|
|
432
|
+
method: 'POST',
|
|
433
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN_STORE },
|
|
434
|
+
body: JSON.stringify({ ok: false, kind: 'search', error: 'other-origin-result' }),
|
|
435
|
+
});
|
|
436
|
+
assert.equal(forged.status, 403);
|
|
437
|
+
const valid = await fetch(target, {
|
|
438
|
+
method: 'POST',
|
|
439
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN },
|
|
440
|
+
body: JSON.stringify({ ok: true, kind: 'search', body: '{"data":{"flightItineraryList":[]}}', title: '机票列表' }),
|
|
441
|
+
});
|
|
442
|
+
assert.equal(valid.status, 200);
|
|
443
|
+
const outcome = await pending;
|
|
444
|
+
assert.equal(outcome.ok, true);
|
|
445
|
+
if (outcome.ok)
|
|
446
|
+
assert.equal(outcome.origin, EXTENSION_ORIGIN);
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
await lane.close();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
await check('心跳判定:初始未连接;合法请求(health/长轮询)后视为在线', async () => {
|
|
453
|
+
assert.equal(b.extensionConnected(), false);
|
|
454
|
+
await heartbeat(port);
|
|
455
|
+
assert.equal(b.extensionConnected(), true);
|
|
456
|
+
});
|
|
457
|
+
await check('提交-回包闭环:cookie-names job 由假扩展取活并回包,submit 原样收果', async () => {
|
|
458
|
+
const submitP = b.submit({ kind: 'cookie-names', site: 'ctrip-flight', timeoutMs: 5_000 }, { timeoutMs: 5_000 });
|
|
459
|
+
const claimed = await claimOnce(port, (job) => {
|
|
460
|
+
assert.equal(job.kind, 'cookie-names');
|
|
461
|
+
assert.equal(job.site, 'ctrip-flight');
|
|
462
|
+
return { ok: true, kind: 'cookie-names', names: ['cticket', 'uid'] };
|
|
463
|
+
});
|
|
464
|
+
assert.ok(claimed.job);
|
|
465
|
+
const outcome = await submitP;
|
|
466
|
+
assert.ok(outcome.ok);
|
|
467
|
+
assert.deepEqual(outcome.ok ? outcome.result.names : [], ['cticket', 'uid']);
|
|
468
|
+
});
|
|
469
|
+
await check('扩展作业失败必须保留 job-error,不能伪装成未登录或空搜索响应', async () => {
|
|
470
|
+
const cases = [
|
|
471
|
+
{ kind: 'cookie-names', invoke: () => extensionCookieNames({ site: 'ctrip-flight', domain: 'ctrip.com', ticketNames: ['cticket'] }, b) },
|
|
472
|
+
{ kind: 'search', invoke: () => extensionSearchJob({ site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-10-01' }, b) },
|
|
473
|
+
{ kind: 'open-login', invoke: () => extensionOpenLogin({ site: 'ctrip-flight', url: 'https://passport.ctrip.com/' }, b) },
|
|
474
|
+
];
|
|
475
|
+
for (const item of cases) {
|
|
476
|
+
const pending = item.invoke();
|
|
477
|
+
const claimed = await claimOnce(port, (job) => {
|
|
478
|
+
assert.equal(job.kind, item.kind);
|
|
479
|
+
return { ok: false, kind: item.kind, error: 'browser tab creation failed' };
|
|
480
|
+
});
|
|
481
|
+
assert.ok(claimed.job);
|
|
482
|
+
const result = await pending;
|
|
483
|
+
assert.equal(result.ok, false, `${item.kind} must expose the extension failure`);
|
|
484
|
+
if (!result.ok) {
|
|
485
|
+
assert.equal(result.kind, 'job-error');
|
|
486
|
+
assert.match(result.summary, /browser tab creation failed/);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
await check('检索嗅探到点仍保留页面标题,供挑战红线判定', async () => {
|
|
491
|
+
const pending = extensionSearchJob({ site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-10-01' }, b);
|
|
492
|
+
const claimed = await claimOnce(port, (job) => {
|
|
493
|
+
assert.equal(job.kind, 'search');
|
|
494
|
+
return { ok: false, kind: 'search', timeout: true, url: '', title: '安全验证' };
|
|
495
|
+
});
|
|
496
|
+
assert.ok(claimed.job);
|
|
497
|
+
const result = await pending;
|
|
498
|
+
assert.equal(result.ok, true);
|
|
499
|
+
if (result.ok) {
|
|
500
|
+
assert.equal(result.timedOut, true);
|
|
501
|
+
assert.equal(result.title, '安全验证');
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
await check('cookie 红线:扩展代码只上报名(c.name 映射),值不进任何结果对象', () => {
|
|
505
|
+
assert.ok(backgroundJs.includes('.map((c) => c.name)'));
|
|
506
|
+
assert.ok(!/\.value/.test(backgroundJs), 'background.js 不得触碰 cookie.value');
|
|
507
|
+
});
|
|
508
|
+
await check('长轮询幂等:并发取活只有一个客户端领到 job;后到者空手(快速中止,不挂进程)', async () => {
|
|
509
|
+
const firstAc = new AbortController();
|
|
510
|
+
const secondAc = new AbortController();
|
|
511
|
+
const firstP = claimOnce(port, () => ({ ok: true, kind: 'cookie-names', names: ['cticket'] }), firstAc.signal).catch(() => ({ job: null }));
|
|
512
|
+
let secondP = null;
|
|
513
|
+
try {
|
|
514
|
+
await waitForParkedCount(port, 1);
|
|
515
|
+
secondP = claimOnce(port, null, secondAc.signal).catch(() => ({ job: null }));
|
|
516
|
+
await waitForParkedCount(port, 2);
|
|
517
|
+
const submitP = b.submit({ kind: 'cookie-names', site: 'ctrip-flight', timeoutMs: 5_000 }, { timeoutMs: 5_000 });
|
|
518
|
+
const first = await firstP;
|
|
519
|
+
assert.ok(first.job, '先进入 parked 队列的取活者必须领到 job');
|
|
520
|
+
const outcome = await submitP;
|
|
521
|
+
assert.ok(outcome.ok);
|
|
522
|
+
}
|
|
523
|
+
finally {
|
|
524
|
+
firstAc.abort();
|
|
525
|
+
secondAc.abort();
|
|
526
|
+
if (secondP)
|
|
527
|
+
await secondP;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
await check('超时语义:扩展在线但不回包 → timeout(降级 error,不伪装成功)', async () => {
|
|
531
|
+
const outcome = await b.submit({ kind: 'search', site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01', timeoutMs: 300 }, { timeoutMs: 300 });
|
|
532
|
+
assert.ok(!outcome.ok);
|
|
533
|
+
assert.equal(outcome.ok ? null : outcome.reason, 'timeout');
|
|
534
|
+
});
|
|
535
|
+
await check('扩展未连接:有界等待后 extension-not-connected(用户门 no-spend,不空耗)', async () => {
|
|
536
|
+
const fresh = await mustBridge([0]);
|
|
537
|
+
try {
|
|
538
|
+
const outcome = await fresh.submit({ kind: 'cookie-names', site: 'ctrip-flight' }, { extensionWaitMs: 400, timeoutMs: 2_000 });
|
|
539
|
+
assert.ok(!outcome.ok);
|
|
540
|
+
assert.equal(outcome.ok ? null : outcome.reason, 'extension-not-connected');
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
await fresh.close();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
await check('close:桥关闭后排队任务以 bridge-unavailable 结算(零花费)', async () => {
|
|
547
|
+
const tmp = await mustBridge([0]);
|
|
548
|
+
await heartbeat(tmp.port);
|
|
549
|
+
const submitP = tmp.submit({ kind: 'cookie-names', site: 'ctrip-flight', timeoutMs: 30_000 }, { timeoutMs: 30_000 });
|
|
550
|
+
await tmp.close();
|
|
551
|
+
const outcome = await submitP;
|
|
552
|
+
assert.ok(!outcome.ok);
|
|
553
|
+
assert.equal(outcome.ok ? null : outcome.reason, 'bridge-unavailable');
|
|
554
|
+
});
|
|
555
|
+
/* ---------- ③ 车道语义 ---------- */
|
|
556
|
+
await check('classifyBridgeFailure:只有 extension-not-connected 是用户门(needs-extension),其余 error', () => {
|
|
557
|
+
assert.equal(classifyBridgeFailure('extension-not-connected'), 'needs-extension');
|
|
558
|
+
assert.equal(classifyBridgeFailure('bridge-unavailable'), 'error');
|
|
559
|
+
assert.equal(classifyBridgeFailure('timeout'), 'error');
|
|
560
|
+
assert.equal(classifyBridgeFailure('job-error'), 'error');
|
|
561
|
+
});
|
|
562
|
+
const savedTransportEnv = process.env.GOTRY_SESSION_TRANSPORT;
|
|
563
|
+
const restoreTransportEnv = () => {
|
|
564
|
+
if (savedTransportEnv === undefined)
|
|
565
|
+
delete process.env.GOTRY_SESSION_TRANSPORT;
|
|
566
|
+
else
|
|
567
|
+
process.env.GOTRY_SESSION_TRANSPORT = savedTransportEnv;
|
|
568
|
+
};
|
|
569
|
+
try {
|
|
570
|
+
delete process.env.GOTRY_SESSION_TRANSPORT;
|
|
571
|
+
await check('resolveTransportMode:扩展默认;cdp 须显式 opt-in;profileDir=persistent(测试)', () => {
|
|
572
|
+
assert.equal(resolveTransportMode(), 'extension');
|
|
573
|
+
process.env.GOTRY_SESSION_TRANSPORT = 'cdp';
|
|
574
|
+
assert.equal(resolveTransportMode(), 'cdp');
|
|
575
|
+
assert.equal(resolveTransportMode('/tmp/isolated-profile'), 'persistent');
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
finally {
|
|
579
|
+
restoreTransportEnv();
|
|
580
|
+
}
|
|
581
|
+
const auditDir = mkdtempSync(join(tmpdir(), 'gotry-ext-audit-'));
|
|
582
|
+
const auditPath = join(auditDir, 'session-incidents.jsonl');
|
|
583
|
+
appendExtensionAudit(auditPath, { kind: 'extension-session-job', site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01', jobId: 'search', result: 'body 1024B' });
|
|
584
|
+
await check('审计:扩展 job 落 session-incidents 同款 JSONL(kind 区分,url 截断)', () => {
|
|
585
|
+
const lines = readFileSync(auditPath, 'utf8').trim().split('\n');
|
|
586
|
+
assert.equal(lines.length, 1);
|
|
587
|
+
const entry = JSON.parse(lines[0]);
|
|
588
|
+
assert.equal(entry.kind, 'extension-session-job');
|
|
589
|
+
assert.equal(entry.site, 'ctrip-flight');
|
|
590
|
+
assert.ok(entry.url.length <= 400);
|
|
591
|
+
});
|
|
592
|
+
rmSync(auditDir, { recursive: true, force: true });
|
|
593
|
+
await check('车道闭环(注入临时桥+假扩展):cookie-names 只吐名字;search job 回传正文;登录快路径零交互', async () => {
|
|
594
|
+
const lane = await mustBridge([0]);
|
|
595
|
+
__setSessionBridgeForTest(lane);
|
|
596
|
+
try {
|
|
597
|
+
let done = false;
|
|
598
|
+
const claimLoop = (async () => {
|
|
599
|
+
while (!done) {
|
|
600
|
+
const ac = new AbortController();
|
|
601
|
+
const bail = setTimeout(() => ac.abort(), 2_000);
|
|
602
|
+
try {
|
|
603
|
+
await claimOnce(lane.port, (job) => {
|
|
604
|
+
if (job.kind === 'cookie-names')
|
|
605
|
+
return { ok: true, kind: 'cookie-names', names: ['cticket'] };
|
|
606
|
+
if (job.kind === 'search') {
|
|
607
|
+
const u = String(job.url);
|
|
608
|
+
const isHotel = u.startsWith(`https://${HOTEL_SITE_HOST}/`);
|
|
609
|
+
const isTrain = u.startsWith('https://kyfw.12306.cn/');
|
|
610
|
+
assert.ok(u.startsWith('https://flights.ctrip.com/') || isHotel || isTrain, `search job 只允许已注册站点域,实际 ${u}`);
|
|
611
|
+
const requestedUrl = new URL(u);
|
|
612
|
+
const requestedDate = requestedUrl.searchParams.get('date') ?? '2026-12-01';
|
|
613
|
+
const wrongRoute = requestedUrl.searchParams.get('fs')?.endsWith(',BJP') === true;
|
|
614
|
+
const wrongDateEmpty = requestedDate === '2026-12-02';
|
|
615
|
+
const trainResponseUrl = wrongRoute
|
|
616
|
+
? `https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=${requestedDate}&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM`
|
|
617
|
+
: `https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=${wrongDateEmpty ? '2026-12-01' : requestedDate}&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM`;
|
|
618
|
+
return {
|
|
619
|
+
ok: true,
|
|
620
|
+
kind: 'search',
|
|
621
|
+
body: isHotel
|
|
622
|
+
? JSON.stringify({ data: { hotelList: [{ hotelId: 442516, hotelName: 'Hotel X', star: 5, commentScore: 4.7, priceInfo: { avgPrice: 680 } }] } })
|
|
623
|
+
: isTrain
|
|
624
|
+
? JSON.stringify({ data: { result: wrongDateEmpty ? [] : ['|预订|24000000G1375|G1375|SHH|KMM|SHH|KMM|07:35|15:27|07:52|Y|yp|20261201|x|loc|01|02|Y|0|--|--|--|--|--|--|有|--|--|有|有|有|有|--|ex|st|'], map: { SHH: '上海南', KMM: '昆明' } } })
|
|
625
|
+
: JSON.stringify({ data: { flightItineraryList: [] } }),
|
|
626
|
+
url: isTrain ? trainResponseUrl : u,
|
|
627
|
+
title: isHotel ? '酒店列表' : isTrain ? '12306 车票预订' : '机票列表',
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return { ok: false, error: `unexpected kind ${job.kind}` };
|
|
631
|
+
}, ac.signal);
|
|
632
|
+
}
|
|
633
|
+
catch { /* park 被中止=常态 */ }
|
|
634
|
+
clearTimeout(bail);
|
|
635
|
+
}
|
|
636
|
+
})();
|
|
637
|
+
const cookies = await extensionCookieNames({ site: 'ctrip-flight', domain: SITE_DOMAIN.replace(/^\./, ''), ticketNames: LOGIN_COOKIE_NAMES });
|
|
638
|
+
assert.ok(cookies.ok);
|
|
639
|
+
assert.deepEqual(cookies.ok ? cookies.tickets : [], ['cticket']);
|
|
640
|
+
const search = await extensionSearchJob({ site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01', timeoutMs: 3_000 });
|
|
641
|
+
assert.ok(search.ok);
|
|
642
|
+
assert.equal(search.ok ? search.timedOut : true, false);
|
|
643
|
+
assert.ok(search.ok ? search.body.includes('flightItineraryList') : false);
|
|
644
|
+
assert.equal(search.ok ? search.url : '', 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01', '响应 URL 穿过 background/result/channel');
|
|
645
|
+
// 酒店车道(2026-09-03 实装):per-site 白名单放行 hotels.ctrip.com,嗅探回包走形解析出结构化酒店
|
|
646
|
+
const hotelLane = await extensionSearchJob({ site: 'ctrip-hotel', url: `https://${HOTEL_SITE_HOST}/hotels/list?city=220&checkin=2026-12-01&checkout=2026-12-03`, timeoutMs: 3_000 });
|
|
647
|
+
assert.ok(hotelLane.ok, '酒店 search job 应放行(per-site 白名单)');
|
|
648
|
+
assert.ok(hotelLane.ok ? hotelLane.body.includes('hotelList') : false);
|
|
649
|
+
__resetRateLimiterForTest();
|
|
650
|
+
const hotelSession = await sessionHotelSearch({ to: '迪拜', cityId: 220, checkIn: '2026-12-01', checkOut: '2026-12-03', timeoutMs: 3_000 });
|
|
651
|
+
assert.equal(hotelSession.verdict, 'hit', `酒店会话检索应 hit,实际 ${hotelSession.verdict}:${hotelSession.error ?? ''}`);
|
|
652
|
+
assert.ok(hotelSession.evidence.includes('[会话:ctrip-hotel@'), '酒店证据链 [会话:ctrip-hotel@ts]');
|
|
653
|
+
assert.equal(hotelSession.hotels?.[0]?.name, 'Hotel X');
|
|
654
|
+
assert.equal(hotelSession.hotels?.[0]?.price, 680);
|
|
655
|
+
assert.ok(hotelSession.hotels?.[0]?.jumpUrl?.includes('/hotel/442516'), 'jumpUrl 由 hotelId 构造(预订由人在落地页完成)');
|
|
656
|
+
// 火车车道(2026-09-03 实装):公开查询面无登录闸,per-site 白名单放行 kyfw,管道行解析出结构化车次
|
|
657
|
+
__resetRateLimiterForTest();
|
|
658
|
+
const trainSession = await sessionTrainSearch({ from: '上海', to: '昆明', date: '2026-12-01', timeoutMs: 3_000 });
|
|
659
|
+
assert.equal(trainSession.verdict, 'hit', `火车会话检索应 hit,实际 ${trainSession.verdict}:${trainSession.error ?? ''}`);
|
|
660
|
+
assert.ok(trainSession.evidence.includes('[会话:train-12306@'), '火车证据链 [会话:train-12306@ts]');
|
|
661
|
+
assert.ok(trainSession.evidence.includes('公开查询面'), '证据链标注公开查询面(无登录闸的诚实口径)');
|
|
662
|
+
assert.equal(trainSession.trains?.[0]?.trainCode, 'G1375');
|
|
663
|
+
assert.equal(trainSession.trains?.[0]?.depTime, '07:35');
|
|
664
|
+
assert.equal(trainSession.trains?.[0]?.durationMin, 472);
|
|
665
|
+
assert.ok(trainSession.trains?.[0]?.jumpUrl?.includes('kyfw.12306.cn'), 'jumpUrl=查询落地页(人选车完成预订)');
|
|
666
|
+
assert.equal(trainSession.collection?.response.url, 'https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2026-12-01&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM', '火车响应 URL 穿过扩展桥并绑定 collection');
|
|
667
|
+
__resetRateLimiterForTest();
|
|
668
|
+
const wrongRoute = await sessionTrainSearch({ from: '北京', to: '上海', date: '2026-12-03', timeoutMs: 3_000 });
|
|
669
|
+
assert.equal(wrongRoute.collection, undefined, 'wrong-route response URL → no collection');
|
|
670
|
+
assert.equal(factsFromSessionTrain({ from: '北京', to: '上海', date: '2026-12-03' }, wrongRoute).length, 0, 'wrong-route collector result → zero facts');
|
|
671
|
+
__resetRateLimiterForTest();
|
|
672
|
+
const wrongDateEmpty = await sessionTrainSearch({ from: '上海', to: '昆明', date: '2026-12-02', timeoutMs: 3_000 });
|
|
673
|
+
assert.equal(wrongDateEmpty.outcome.kind, 'recognized-empty', 'wrong-date seam keeps parsed empty presentation');
|
|
674
|
+
assert.equal(wrongDateEmpty.collection, undefined, 'wrong-date empty response URL → no collection');
|
|
675
|
+
assert.equal(factsFromSessionTrain({ from: '上海', to: '昆明', date: '2026-12-02' }, wrongDateEmpty).length, 0, 'wrong-date empty collector result → zero facts');
|
|
676
|
+
const login = await sessionLogin({ site: 'ctrip-flight' });
|
|
677
|
+
assert.equal(login.verdict, 'logged-in');
|
|
678
|
+
assert.deepEqual(login.tickets, ['cticket']);
|
|
679
|
+
assert.ok(login.evidence.includes('自动检测'));
|
|
680
|
+
done = true;
|
|
681
|
+
await claimLoop;
|
|
682
|
+
}
|
|
683
|
+
finally {
|
|
684
|
+
await __resetSessionBridgeForTest();
|
|
685
|
+
__setSessionBridgeForTest(null);
|
|
686
|
+
await lane.close();
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
await check('同次机票会话的检索只能由完成票据预检的扩展 Origin 领取', async () => {
|
|
690
|
+
const lane = await mustBridge([0]);
|
|
691
|
+
__setSessionBridgeForTest(lane);
|
|
692
|
+
const storeAbort = new AbortController();
|
|
693
|
+
const unpackedAbort = new AbortController();
|
|
694
|
+
let storeGotSearch = false;
|
|
695
|
+
let unpackedGotSearch = false;
|
|
696
|
+
let storePoll;
|
|
697
|
+
let unpackedSearchPoll;
|
|
698
|
+
try {
|
|
699
|
+
__resetRateLimiterForTest();
|
|
700
|
+
const cookiePoll = claimOnce(lane.port, null);
|
|
701
|
+
await waitForParkedCount(lane.port, 1);
|
|
702
|
+
const search = sessionFlightSearch({ from: '上海', to: '丽江', date: '2026-12-01', timeoutMs: 3_000 });
|
|
703
|
+
const cookieClaim = await cookiePoll;
|
|
704
|
+
assert.equal(cookieClaim.job?.kind, 'cookie-names');
|
|
705
|
+
// 商店版先候在桥上;它不得接走另一 Origin 已验证登录态后的 search。
|
|
706
|
+
storePoll = claimOnce(lane.port, (job) => {
|
|
707
|
+
storeGotSearch = true;
|
|
708
|
+
return { ok: false, kind: job.kind, error: 'wrong-extension-origin' };
|
|
709
|
+
}, storeAbort.signal, ['ctrip-flight'], EXTENSION_ORIGIN_STORE).catch(() => ({ job: null }));
|
|
710
|
+
await waitForParkedCount(lane.port, 1);
|
|
711
|
+
const cookieResult = await fetch(`http://127.0.0.1:${lane.port}/results/${encodeURIComponent(cookieClaim.job.jobId)}`, {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN },
|
|
714
|
+
body: JSON.stringify({ ok: true, kind: 'cookie-names', names: ['cticket'] }),
|
|
715
|
+
});
|
|
716
|
+
assert.equal(cookieResult.status, 200);
|
|
717
|
+
unpackedSearchPoll = claimOnce(lane.port, (job) => {
|
|
718
|
+
assert.equal(job.kind, 'search');
|
|
719
|
+
unpackedGotSearch = true;
|
|
720
|
+
return { ok: true, kind: 'search', body: JSON.stringify({ data: { flightItineraryList: [] } }), url: job.url, title: '机票列表' };
|
|
721
|
+
}, unpackedAbort.signal, ['ctrip-flight'], EXTENSION_ORIGIN).catch(() => ({ job: null }));
|
|
722
|
+
const result = await search;
|
|
723
|
+
assert.equal(result.verdict, 'miss', `同一 Origin 应完成检索,实际 ${result.verdict}:${result.error ?? ''}`);
|
|
724
|
+
assert.equal(unpackedGotSearch, true);
|
|
725
|
+
assert.equal(storeGotSearch, false);
|
|
726
|
+
}
|
|
727
|
+
finally {
|
|
728
|
+
storeAbort.abort();
|
|
729
|
+
unpackedAbort.abort();
|
|
730
|
+
await Promise.allSettled([storePoll, unpackedSearchPoll].filter((p) => p !== undefined));
|
|
731
|
+
await __resetSessionBridgeForTest();
|
|
732
|
+
__setSessionBridgeForTest(null);
|
|
733
|
+
await lane.close();
|
|
734
|
+
__resetRateLimiterForTest();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
await check('同一 Origin 的两个 Chrome 配置不能交叉领取预检后的检索或提交结果', async () => {
|
|
738
|
+
const lane = await mustBridge([0]);
|
|
739
|
+
__setSessionBridgeForTest(lane);
|
|
740
|
+
const shapeRoot = mkdtempSync(join(tmpdir(), 'gotry-sf272-shape-'));
|
|
741
|
+
const shapeAudit = join(shapeRoot, 'transport-shape.jsonl');
|
|
742
|
+
const clientA = '11111111-1111-4111-8111-111111111111';
|
|
743
|
+
const clientB = '22222222-2222-4222-8222-222222222222';
|
|
744
|
+
const otherAbort = new AbortController();
|
|
745
|
+
const sameAbort = new AbortController();
|
|
746
|
+
let otherGotSearch = false;
|
|
747
|
+
let sameGotSearch = false;
|
|
748
|
+
let otherPoll;
|
|
749
|
+
let samePoll;
|
|
750
|
+
try {
|
|
751
|
+
__resetRateLimiterForTest();
|
|
752
|
+
const cookiePoll = claimOnce(lane.port, null, undefined, ['ctrip-flight'], EXTENSION_ORIGIN, clientA);
|
|
753
|
+
await waitForParkedCount(lane.port, 1);
|
|
754
|
+
const search = sessionFlightSearch({ from: '上海', to: '丽江', date: '2026-12-01', timeoutMs: 3_000, auditPath: shapeAudit });
|
|
755
|
+
const cookieClaim = await cookiePoll;
|
|
756
|
+
assert.equal(cookieClaim.job?.kind, 'cookie-names');
|
|
757
|
+
otherPoll = claimOnce(lane.port, (job) => {
|
|
758
|
+
otherGotSearch = true;
|
|
759
|
+
return { ok: false, kind: job.kind, error: 'wrong-chrome-profile' };
|
|
760
|
+
}, otherAbort.signal, ['ctrip-flight'], EXTENSION_ORIGIN, clientB).catch(() => ({ job: null }));
|
|
761
|
+
await waitForParkedCount(lane.port, 1);
|
|
762
|
+
const wrongClientResult = await fetch(`http://127.0.0.1:${lane.port}/results/${encodeURIComponent(cookieClaim.job.jobId)}`, {
|
|
763
|
+
method: 'POST',
|
|
764
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN, 'x-gotry-client-id': clientB },
|
|
765
|
+
body: JSON.stringify({ ok: false, kind: 'cookie-names', error: 'wrong-chrome-profile' }),
|
|
766
|
+
});
|
|
767
|
+
assert.equal(wrongClientResult.status, 403);
|
|
768
|
+
const cookieResult = await fetch(`http://127.0.0.1:${lane.port}/results/${encodeURIComponent(cookieClaim.job.jobId)}`, {
|
|
769
|
+
method: 'POST',
|
|
770
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN, 'x-gotry-client-id': clientA },
|
|
771
|
+
body: JSON.stringify({ ok: true, kind: 'cookie-names', names: ['cticket'] }),
|
|
772
|
+
});
|
|
773
|
+
assert.equal(cookieResult.status, 200);
|
|
774
|
+
samePoll = claimOnce(lane.port, (job) => {
|
|
775
|
+
assert.equal(job.kind, 'search');
|
|
776
|
+
sameGotSearch = true;
|
|
777
|
+
return { ok: true, kind: 'search', body: JSON.stringify({ data: { flightItineraryList: [] } }), url: job.url, title: '机票列表' };
|
|
778
|
+
}, sameAbort.signal, ['ctrip-flight'], EXTENSION_ORIGIN, clientA).catch(() => ({ job: null }));
|
|
779
|
+
const result = await search;
|
|
780
|
+
assert.equal(result.verdict, 'miss', `同一客户端应完成检索,实际 ${result.verdict}:${result.error ?? ''}`);
|
|
781
|
+
assert.equal(result.transportShape, 'itinerary_list_empty');
|
|
782
|
+
const shapeEvidence = readFileSync(shapeAudit, 'utf8');
|
|
783
|
+
assert.equal(JSON.parse(shapeEvidence).result, 'itinerary_list_empty');
|
|
784
|
+
assert.equal(JSON.parse(shapeEvidence).url, 'https://flights.ctrip.com');
|
|
785
|
+
assert.doesNotMatch(shapeEvidence, /depdate|oneway-sha-ljg|机票列表|cticket/);
|
|
786
|
+
assert.equal(sameGotSearch, true);
|
|
787
|
+
assert.equal(otherGotSearch, false);
|
|
788
|
+
}
|
|
789
|
+
finally {
|
|
790
|
+
otherAbort.abort();
|
|
791
|
+
sameAbort.abort();
|
|
792
|
+
await Promise.allSettled([otherPoll, samePoll].filter((p) => p !== undefined));
|
|
793
|
+
await __resetSessionBridgeForTest();
|
|
794
|
+
__setSessionBridgeForTest(null);
|
|
795
|
+
await lane.close();
|
|
796
|
+
__resetRateLimiterForTest();
|
|
797
|
+
rmSync(shapeRoot, { recursive: true, force: true });
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
await check('机票检索嗅探超时应说明未收到回包,不误报 batchSearch 形状异常', async () => {
|
|
801
|
+
const lane = await mustBridge([0]);
|
|
802
|
+
__setSessionBridgeForTest(lane);
|
|
803
|
+
let done = false;
|
|
804
|
+
const claimLoop = (async () => {
|
|
805
|
+
while (!done) {
|
|
806
|
+
const ac = new AbortController();
|
|
807
|
+
const bail = setTimeout(() => ac.abort(), 2_000);
|
|
808
|
+
try {
|
|
809
|
+
await claimOnce(lane.port, (job) => {
|
|
810
|
+
if (job.kind === 'cookie-names')
|
|
811
|
+
return { ok: true, kind: 'cookie-names', names: ['cticket'] };
|
|
812
|
+
assert.equal(job.kind, 'search');
|
|
813
|
+
assert.equal(job.site, 'ctrip-flight');
|
|
814
|
+
return { ok: false, kind: 'search', timeout: true, url: '', title: '机票列表' };
|
|
815
|
+
}, ac.signal);
|
|
816
|
+
}
|
|
817
|
+
catch { /* 等待取活时中止 */ }
|
|
818
|
+
clearTimeout(bail);
|
|
819
|
+
}
|
|
820
|
+
})();
|
|
821
|
+
try {
|
|
822
|
+
__resetRateLimiterForTest();
|
|
823
|
+
const result = await sessionFlightSearch({ from: '上海', to: '丽江', date: '2026-12-01', timeoutMs: 3_000 });
|
|
824
|
+
assert.equal(result.verdict, 'error');
|
|
825
|
+
assert.equal(result.transportShape, 'sniff_timeout');
|
|
826
|
+
assert.match(result.error ?? '', /嗅探超时|未收到.*回包/);
|
|
827
|
+
assert.doesNotMatch(result.error ?? '', /响应形状异常/);
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
done = true;
|
|
831
|
+
await claimLoop;
|
|
832
|
+
await __resetSessionBridgeForTest();
|
|
833
|
+
__setSessionBridgeForTest(null);
|
|
834
|
+
await lane.close();
|
|
835
|
+
__resetRateLimiterForTest();
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
await check('携程扩展作业错误不得把客户端原文带入会话证据', async () => {
|
|
839
|
+
const sentinel = 'PRIVATE_ROUTE_DATE_COOKIE_VALUE';
|
|
840
|
+
for (const failedKind of ['cookie-names', 'search']) {
|
|
841
|
+
const lane = await mustBridge([0]);
|
|
842
|
+
__setSessionBridgeForTest(lane);
|
|
843
|
+
let done = false;
|
|
844
|
+
const claimLoop = (async () => {
|
|
845
|
+
while (!done) {
|
|
846
|
+
const ac = new AbortController();
|
|
847
|
+
const bail = setTimeout(() => ac.abort(), 2_000);
|
|
848
|
+
try {
|
|
849
|
+
await claimOnce(lane.port, (job) => {
|
|
850
|
+
if (job.kind === failedKind)
|
|
851
|
+
return { ok: false, kind: job.kind, error: sentinel };
|
|
852
|
+
assert.equal(job.kind, 'cookie-names');
|
|
853
|
+
return { ok: true, kind: 'cookie-names', names: ['cticket'] };
|
|
854
|
+
}, ac.signal);
|
|
855
|
+
}
|
|
856
|
+
catch { /* 等待取活时中止 */ }
|
|
857
|
+
clearTimeout(bail);
|
|
858
|
+
}
|
|
859
|
+
})();
|
|
860
|
+
try {
|
|
861
|
+
__resetRateLimiterForTest();
|
|
862
|
+
const result = await sessionFlightSearch({ from: '上海', to: '丽江', date: '2026-12-01', timeoutMs: 3_000 });
|
|
863
|
+
assert.equal(result.verdict, 'error');
|
|
864
|
+
assert.equal(result.transportShape, failedKind === 'search' ? 'search_job_unavailable' : 'precheck_unavailable');
|
|
865
|
+
assert.doesNotMatch(JSON.stringify(result), /PRIVATE_ROUTE_DATE_COOKIE_VALUE/);
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
done = true;
|
|
869
|
+
await claimLoop;
|
|
870
|
+
await __resetSessionBridgeForTest();
|
|
871
|
+
__setSessionBridgeForTest(null);
|
|
872
|
+
await lane.close();
|
|
873
|
+
__resetRateLimiterForTest();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
await check('全链 fail-closed:扩展车道桥端口池全占 → sessionFlightSearch verdict=error(环境故障,非用户门)', async () => {
|
|
878
|
+
// 设计契约:只有 extension-not-connected 才是 user gate (needs-extension);
|
|
879
|
+
// 端口池全占是环境故障(并行 gotry 实例/外部进程占端口)→ verdict=error,
|
|
880
|
+
// 不诱导用户去「装扩展」(装也解决不了)。
|
|
881
|
+
const { createServer } = await import('node:http');
|
|
882
|
+
const blockers = [];
|
|
883
|
+
try {
|
|
884
|
+
for (const port of [8791, 8792, 8793, 8794, 8795]) {
|
|
885
|
+
const s = createServer().listen(port, '127.0.0.1');
|
|
886
|
+
await new Promise((r) => s.once('listening', () => r()));
|
|
887
|
+
blockers.push({ close: () => new Promise((r) => s.close(() => r())) });
|
|
888
|
+
}
|
|
889
|
+
await __resetSessionBridgeForTest();
|
|
890
|
+
const r = await sessionFlightSearch({ from: '上海', to: '丽江', date: '2026-12-01' });
|
|
891
|
+
assert.equal(r.verdict, 'error', `端口池全占应 error(环境故障),实 ${r.verdict}:${r.error}`);
|
|
892
|
+
assert.ok(r.error?.includes('端口池'), '错误面必须点明端口池环境问题');
|
|
893
|
+
assert.ok(!r.error?.includes('一次性安装'), 'error 路径不应诱导用户去装扩展');
|
|
894
|
+
}
|
|
895
|
+
finally {
|
|
896
|
+
await __resetSessionBridgeForTest();
|
|
897
|
+
for (const b of blockers)
|
|
898
|
+
await b.close();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
const waitingRecord = (verdict) => ({
|
|
902
|
+
query_id: 'sf-01', route_segments: [], journey_type: 'direct', currency: '', price: 0, source: '',
|
|
903
|
+
fetched_at: '', verdict, latency_ms: 0, read_guard_blocked: 0,
|
|
904
|
+
});
|
|
905
|
+
await check('capability 路由:站点 job 只派给声明该站点的轮询者(同机多 Chrome 旧版扩展不毒领新站点 job)', async () => {
|
|
906
|
+
const lane = await mustBridge([0]);
|
|
907
|
+
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
908
|
+
try {
|
|
909
|
+
// 旧版扩展(caps 无 dida-portal,模拟商店版)先 parked
|
|
910
|
+
const oldExtP = claimOnce(lane.port, null, undefined, ['ctrip-flight', 'ctrip-hotel', 'train-12306']).catch(() => ({ job: null }));
|
|
911
|
+
await pause(200);
|
|
912
|
+
// dida job 提交:不得派给旧版扩展(它不声明 dida-portal)
|
|
913
|
+
const submitP = lane.submit({ kind: 'cookie-names', site: 'dida-portal' }, { timeoutMs: 3_000 });
|
|
914
|
+
await pause(200);
|
|
915
|
+
const stoleByOld = await Promise.race([oldExtP.then(() => true), pause(400).then(() => false)]);
|
|
916
|
+
assert.ok(!stoleByOld, '旧版扩展(capability 不含 dida-portal)不得领走 dida job');
|
|
917
|
+
// 新版扩展声明 dida-portal → 立即领到
|
|
918
|
+
const newExtGot = await claimOnce(lane.port, (job) => {
|
|
919
|
+
assert.equal(job.kind, 'cookie-names');
|
|
920
|
+
return { ok: true, kind: 'cookie-names', names: ['CN_M_DidaTravel'] };
|
|
921
|
+
}, undefined, ['ctrip-flight', 'ctrip-hotel', 'train-12306', 'dida-portal']);
|
|
922
|
+
assert.ok(newExtGot.job && newExtGot.job.site === 'dida-portal', `声明 dida-portal 的扩展领到 job: ${JSON.stringify(newExtGot.job)}`);
|
|
923
|
+
const outcome = await submitP;
|
|
924
|
+
assert.ok(outcome.ok && outcome.result.names?.includes('CN_M_DidaTravel'), `job 结果正确回传: ${JSON.stringify(outcome)}`);
|
|
925
|
+
// 正向:capability 匹配的 parked 轮询者即时收到新提交的 ctrip job
|
|
926
|
+
const ctripP = lane.submit({ kind: 'cookie-names', site: 'ctrip-flight' }, { timeoutMs: 3_000 });
|
|
927
|
+
await pause(100);
|
|
928
|
+
const parkedOld = await Promise.race([oldExtP, pause(500).then(() => null)]);
|
|
929
|
+
assert.ok(parkedOld && parkedOld.job && parkedOld.job.site === 'ctrip-flight', `旧版扩展领走自己 capability 内的 job: ${JSON.stringify(parkedOld?.job)}`);
|
|
930
|
+
await ctripP.catch(() => ({ ok: false }));
|
|
931
|
+
}
|
|
932
|
+
finally {
|
|
933
|
+
await lane.close();
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
await check('双源合同:needs-extension → waiting_extension(no_spend_waiting_user,waiting-* 同族)', () => {
|
|
937
|
+
const e = evaluateDoubleSource({ session: waitingRecord('needs-extension') });
|
|
938
|
+
assert.equal(e.state, 'waiting_extension');
|
|
939
|
+
assert.equal(e.quota_disposition, 'no_spend_waiting_user');
|
|
940
|
+
assert.equal(e.retry_allowed, false);
|
|
941
|
+
});
|
|
942
|
+
// D-24 自适应文案(issue #117):按本地通道是否落位自动跳过开发者模式/本地通道指引
|
|
943
|
+
await check('needs-extension 自适应文案:本地已落位 → 双通道完整指引(含开发者模式)', () => {
|
|
944
|
+
const s = needsExtensionSummary({ localInstalled: true });
|
|
945
|
+
assert.match(s, /开发者模式「加载已解压的扩展程序」/, '本地通道在 → 保留开发者模式指引');
|
|
946
|
+
assert.ok(s.includes(EXTENSION_STORE_URL), '商店 URL 仍在');
|
|
947
|
+
});
|
|
948
|
+
await check('needs-extension 自适应文案:本地未落位(商店版用户/未装)→ 自动跳过 dev-mode 文案', () => {
|
|
949
|
+
const s = needsExtensionSummary({ localInstalled: false });
|
|
950
|
+
assert.doesNotMatch(s, /开发者模式/, '不再推开发者模式/本地通道(商店版用户不该被指导 load unpacked)');
|
|
951
|
+
assert.doesNotMatch(s, /加载已解压/, '不再出现 load unpacked 字样');
|
|
952
|
+
assert.match(s, /已从 Chrome 商店安装/, '给「已装商店版?」提示(打开 Chrome 即可,无需再装)');
|
|
953
|
+
assert.match(s, /应用商店一键装/, '未装用户仍得商店一键装指引');
|
|
954
|
+
});
|
|
955
|
+
// #559 B 步:影响面行必须显式列出挂的工具 + 不影响哪些(避免用户以为整盘不可用)
|
|
956
|
+
await check('needs-extension 影响面(issue #559 B 步):本地已落位文案末尾必带影响面行', () => {
|
|
957
|
+
const s = needsExtensionSummary({ localInstalled: true });
|
|
958
|
+
assert.match(s, /影响面[::]/, '影响面前缀必现');
|
|
959
|
+
assert.match(s, /gotry_session_search/, '必显挂的账号会话工具');
|
|
960
|
+
assert.match(s, /gotry_session_login/, '必显登录引导工具');
|
|
961
|
+
assert.match(s, /其它工具/, '必显「其它不受影响」(避免用户误以为整盘挂)');
|
|
962
|
+
});
|
|
963
|
+
await check('needs-extension 影响面(issue #559 B 步):本地未落位文案末尾必带影响面行', () => {
|
|
964
|
+
const s = needsExtensionSummary({ localInstalled: false });
|
|
965
|
+
assert.match(s, /影响面[::]/, '影响面前缀必现(商店版用户同样需要知道挂哪些)');
|
|
966
|
+
assert.match(s, /gotry_session_search/, '必显挂的账号会话工具');
|
|
967
|
+
assert.match(s, /其它工具/, '必显「其它不受影响」');
|
|
968
|
+
});
|
|
969
|
+
await check('localExtensionInstalled:HOME 注入可测(manifest 存在=本地通道在)', () => {
|
|
970
|
+
const fakeHome = mkdtempSync(join(tmpdir(), 'gotry-ext-home-'));
|
|
971
|
+
assert.equal(localExtensionInstalled(fakeHome), false, '空 HOME → 本地通道未落位');
|
|
972
|
+
mkdirSync(join(fakeHome, '.gotry', 'extension'), { recursive: true });
|
|
973
|
+
writeFileSync(join(fakeHome, '.gotry', 'extension', 'manifest.json'), '{}', 'utf-8');
|
|
974
|
+
assert.equal(localExtensionInstalled(fakeHome), true, 'manifest 落位 → true');
|
|
975
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
976
|
+
});
|
|
977
|
+
await b.close();
|
|
978
|
+
console.log(`\n§38 extension bridge: ${passed} 段全绿`);
|
|
979
|
+
}
|
|
980
|
+
main().catch((e) => {
|
|
981
|
+
console.error('FAIL:', e);
|
|
982
|
+
// best-effort cleanup on early failure:关掉所有未释放的桥
|
|
983
|
+
try {
|
|
984
|
+
__resetSessionBridgeForTest().catch(() => { });
|
|
985
|
+
}
|
|
986
|
+
catch { }
|
|
987
|
+
process.exit(1);
|
|
988
|
+
});
|