@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,193 +1,256 @@
|
|
|
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
|
+
*/
|
|
1
17
|
import assert from 'node:assert/strict';
|
|
2
18
|
import { fork } from 'node:child_process';
|
|
3
19
|
import { createHash } from 'node:crypto';
|
|
4
20
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
21
|
import { tmpdir } from 'node:os';
|
|
6
22
|
import { join } from 'node:path';
|
|
23
|
+
import { runInNewContext } from 'node:vm';
|
|
7
24
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
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';
|
|
9
|
-
import { classifyBridgeFailure, extensionCookieNames, extensionSearchJob } from '../capabilities/session/extension-channel.js';
|
|
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, extensionSearchJob, } from '../capabilities/session/extension-channel.js';
|
|
10
27
|
import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
|
|
11
28
|
import { sessionLogin } from '../capabilities/session-login.js';
|
|
12
29
|
import { LOGIN_COOKIE_NAMES, NETWORK_HINTS, SITE_DOMAIN } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
13
|
-
import { HOTEL_NETWORK_HINTS, HOTEL_SITE_HOST
|
|
30
|
+
import { HOTEL_NETWORK_HINTS, HOTEL_SITE_HOST } from '../capabilities/session/adapters/ctrip-hotel.js';
|
|
14
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';
|
|
15
33
|
import { evaluateDoubleSource } from '../capabilities/session/benchmark.js';
|
|
34
|
+
import { factsFromSessionTrain } from '../src/bookable-facts.js';
|
|
16
35
|
const EXT_DIR = fileURLToPath(new URL('../../extension/', import.meta.url));
|
|
17
36
|
const UNREF_CHILD = fileURLToPath(new URL('./fixtures/extension-bridge-unref-child.mjs', import.meta.url));
|
|
18
|
-
const read = (f)=>readFileSync(join(EXT_DIR, f), 'utf8');
|
|
37
|
+
const read = (f) => readFileSync(join(EXT_DIR, f), 'utf8');
|
|
38
|
+
async function contentMainRelativeResponseUrlProof(source) {
|
|
39
|
+
const events = [];
|
|
40
|
+
const window = {
|
|
41
|
+
__gotrySniffInstalled: false,
|
|
42
|
+
fetch: async function (_input, _init) {
|
|
43
|
+
return {
|
|
44
|
+
url: 'https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2026-12-01&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM',
|
|
45
|
+
headers: { get: () => null },
|
|
46
|
+
clone: () => ({ text: async () => '{"data":{"result":[]}}' }),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
dispatchEvent: (event) => {
|
|
50
|
+
events.push({ url: String(event.detail?.url ?? ''), body: String(event.detail?.body ?? '') });
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
class FixtureXhr {
|
|
54
|
+
responseType = '';
|
|
55
|
+
responseText = '{"data":{"result":[]}}';
|
|
56
|
+
response = this.responseText;
|
|
57
|
+
responseURL = 'https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2026-12-01&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM';
|
|
58
|
+
listeners = [];
|
|
59
|
+
addEventListener(event, handler) {
|
|
60
|
+
if (event === 'load')
|
|
61
|
+
this.listeners.push(handler);
|
|
62
|
+
}
|
|
63
|
+
open(_method, _url) { }
|
|
64
|
+
send(_body) {
|
|
65
|
+
for (const handler of this.listeners)
|
|
66
|
+
handler();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class FixtureCustomEvent {
|
|
70
|
+
detail;
|
|
71
|
+
constructor(_type, init) { this.detail = init.detail; }
|
|
72
|
+
}
|
|
73
|
+
;
|
|
74
|
+
window.addEventListener = () => { };
|
|
75
|
+
runInNewContext(source, { window, location: { hostname: 'kyfw.12306.cn' }, XMLHttpRequest: FixtureXhr, CustomEvent: FixtureCustomEvent });
|
|
76
|
+
await window.fetch('/otn/leftTicket/queryG', {});
|
|
77
|
+
const xhr = new FixtureXhr();
|
|
78
|
+
xhr.open('GET', '/otn/leftTicket/queryG');
|
|
79
|
+
xhr.send();
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
81
|
+
return events;
|
|
82
|
+
}
|
|
19
83
|
let passed = 0;
|
|
20
84
|
async function check(label, assertion) {
|
|
21
85
|
await assertion();
|
|
22
86
|
passed += 1;
|
|
23
87
|
console.log(` ok - ${label}`);
|
|
24
88
|
}
|
|
25
|
-
const mustBridge = async (ports)=>{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
89
|
+
const mustBridge = async (ports) => {
|
|
90
|
+
// 缺省 EXTENSION_ORIGINS(unpacked + 商店版双通道)——行为测试直接覆盖生产默认白名单
|
|
91
|
+
const created = await createSessionBridge({ ports });
|
|
92
|
+
if (!created.ok)
|
|
93
|
+
throw new Error(created.summary);
|
|
30
94
|
return created.bridge;
|
|
31
95
|
};
|
|
32
|
-
|
|
96
|
+
/** 假扩展客户端:一次长轮询取活 + 回包(Origin=固定扩展源,过桥白名单) */
|
|
97
|
+
async function claimOnce(port, respond, signal, capabilities) {
|
|
33
98
|
const r = await fetch(`http://127.0.0.1:${port}/jobs`, {
|
|
34
99
|
method: 'POST',
|
|
35
|
-
headers: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
body: JSON.stringify({
|
|
40
|
-
extensionVersion: 'test'
|
|
41
|
-
}),
|
|
42
|
-
signal
|
|
100
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN },
|
|
101
|
+
body: JSON.stringify({ extensionVersion: 'test', ...(capabilities ? { capabilities } : {}) }),
|
|
102
|
+
signal,
|
|
43
103
|
});
|
|
44
|
-
const data = await r.json();
|
|
104
|
+
const data = (await r.json());
|
|
45
105
|
if (data.job && respond) {
|
|
46
106
|
const result = respond(data.job);
|
|
47
107
|
if (result) {
|
|
48
108
|
await fetch(`http://127.0.0.1:${port}/results/${encodeURIComponent(data.job.jobId)}`, {
|
|
49
109
|
method: 'POST',
|
|
50
|
-
headers: {
|
|
51
|
-
|
|
52
|
-
origin: EXTENSION_ORIGIN
|
|
53
|
-
},
|
|
54
|
-
body: JSON.stringify(result)
|
|
110
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN },
|
|
111
|
+
body: JSON.stringify(result),
|
|
55
112
|
});
|
|
56
113
|
}
|
|
57
114
|
}
|
|
58
115
|
return data;
|
|
59
116
|
}
|
|
60
117
|
async function heartbeat(port) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
origin: EXTENSION_ORIGIN
|
|
64
|
-
}
|
|
65
|
-
});
|
|
118
|
+
// 带扩展 Origin(与浏览器扩展 SW fetch 同形;无 Origin 的 /health 只是诊断,不记心跳)
|
|
119
|
+
await fetch(`http://127.0.0.1:${port}/health`, { headers: { origin: EXTENSION_ORIGIN } });
|
|
66
120
|
}
|
|
67
121
|
async function waitForParkedCount(port, expected, timeoutMs = 1_000) {
|
|
68
122
|
const deadline = Date.now() + timeoutMs;
|
|
69
|
-
while(Date.now() < deadline){
|
|
123
|
+
while (Date.now() < deadline) {
|
|
70
124
|
const r = await fetch(`http://127.0.0.1:${port}/status`);
|
|
71
|
-
const body = await r.json();
|
|
72
|
-
if (body.parked === expected)
|
|
73
|
-
|
|
125
|
+
const body = (await r.json());
|
|
126
|
+
if (body.parked === expected)
|
|
127
|
+
return;
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
74
129
|
}
|
|
75
130
|
throw new Error(`/status 未在 ${timeoutMs}ms 内报告 parked=${expected}`);
|
|
76
131
|
}
|
|
77
132
|
async function assertParkedClientDoesNotPinHost() {
|
|
78
133
|
const child = fork(UNREF_CHILD, [], {
|
|
79
134
|
execArgv: process.execArgv,
|
|
80
|
-
stdio: [
|
|
81
|
-
'ignore',
|
|
82
|
-
'ignore',
|
|
83
|
-
'pipe',
|
|
84
|
-
'ipc'
|
|
85
|
-
]
|
|
135
|
+
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
|
|
86
136
|
});
|
|
87
137
|
let stderr = '';
|
|
88
|
-
child.stderr?.on('data', (chunk)=>{
|
|
89
|
-
stderr += chunk.toString('utf8');
|
|
90
|
-
});
|
|
138
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
91
139
|
let parkedRequest = null;
|
|
92
140
|
try {
|
|
93
|
-
const port = await new Promise((resolve, reject)=>{
|
|
94
|
-
const timer = setTimeout(()=>reject(new Error('子进程扩展桥未在 2s 内监听')), 2_000);
|
|
95
|
-
child.once('message', (message)=>{
|
|
141
|
+
const port = await new Promise((resolve, reject) => {
|
|
142
|
+
const timer = setTimeout(() => reject(new Error('子进程扩展桥未在 2s 内监听')), 2_000);
|
|
143
|
+
child.once('message', (message) => {
|
|
96
144
|
clearTimeout(timer);
|
|
97
145
|
const data = message;
|
|
98
|
-
if (data.kind === 'listening' && typeof data.port === 'number')
|
|
99
|
-
|
|
146
|
+
if (data.kind === 'listening' && typeof data.port === 'number')
|
|
147
|
+
resolve(data.port);
|
|
148
|
+
else
|
|
149
|
+
reject(new Error(data.summary ?? `子进程消息异常:${JSON.stringify(data)}`));
|
|
100
150
|
});
|
|
101
|
-
child.once('exit', (code, signal)=>{
|
|
151
|
+
child.once('exit', (code, signal) => {
|
|
102
152
|
clearTimeout(timer);
|
|
103
153
|
reject(new Error(`子进程过早退出 code=${code} signal=${signal} ${stderr}`));
|
|
104
154
|
});
|
|
105
155
|
});
|
|
106
|
-
parkedRequest = claimOnce(port, null).catch(()=>({
|
|
107
|
-
job: null
|
|
108
|
-
}));
|
|
156
|
+
parkedRequest = claimOnce(port, null).catch(() => ({ job: null }));
|
|
109
157
|
await waitForParkedCount(port, 1);
|
|
110
|
-
const exitP = new Promise((resolve, reject)=>{
|
|
111
|
-
const timer = setTimeout(()=>{
|
|
158
|
+
const exitP = new Promise((resolve, reject) => {
|
|
159
|
+
const timer = setTimeout(() => {
|
|
112
160
|
child.kill();
|
|
113
161
|
reject(new Error('默认桥被 parked /jobs 长轮询钉住超过 1500ms'));
|
|
114
162
|
}, 1_500);
|
|
115
|
-
child.once('exit', (code, signal)=>{
|
|
163
|
+
child.once('exit', (code, signal) => {
|
|
116
164
|
clearTimeout(timer);
|
|
117
|
-
resolve({
|
|
118
|
-
code,
|
|
119
|
-
signal
|
|
120
|
-
});
|
|
165
|
+
resolve({ code, signal });
|
|
121
166
|
});
|
|
122
167
|
});
|
|
123
|
-
child.send({
|
|
124
|
-
kind: 'release'
|
|
125
|
-
});
|
|
168
|
+
child.send({ kind: 'release' });
|
|
126
169
|
const exited = await exitP;
|
|
127
170
|
assert.equal(exited.code, 0, `子进程应自然退出:${stderr}`);
|
|
128
171
|
assert.equal(exited.signal, null);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
175
|
+
child.kill();
|
|
176
|
+
if (parkedRequest)
|
|
177
|
+
await parkedRequest;
|
|
132
178
|
}
|
|
133
179
|
}
|
|
134
|
-
const stripRe = (s)=>s.replace(/\\/g, '');
|
|
180
|
+
const stripRe = (s) => s.replace(/\\/g, '');
|
|
135
181
|
async function main() {
|
|
136
182
|
console.log('§38 扩展传输车道合同(extension bridge,全离线)');
|
|
183
|
+
/* ---------- ① manifest 合同与防漂移 ---------- */
|
|
137
184
|
const manifest = JSON.parse(read('manifest.json'));
|
|
138
|
-
const derivedId = createHash('sha256')
|
|
139
|
-
|
|
185
|
+
const derivedId = createHash('sha256')
|
|
186
|
+
.update(Buffer.from(manifest.key, 'base64'))
|
|
187
|
+
.digest()
|
|
188
|
+
.subarray(0, 16)
|
|
189
|
+
.toString('hex')
|
|
190
|
+
.replace(/[0-9a-f]/g, (c) => 'abcdefghijklmnop'[Number.parseInt(c, 16)]);
|
|
191
|
+
await check('manifest key 派生扩展 ID = Node 侧固定 EXTENSION_ID(unpacked 通道 origin 白名单锚点)', () => {
|
|
140
192
|
assert.equal(derivedId, EXTENSION_ID);
|
|
141
193
|
assert.equal(EXTENSION_ORIGIN, `chrome-extension://${EXTENSION_ID}`);
|
|
142
194
|
});
|
|
143
|
-
await check('双通道白名单:商店版 ID(CWS 重签,不认 manifest key)与 unpacked 固定 ID 同信,商店 URL 锚定 item ID', ()=>{
|
|
195
|
+
await check('双通道白名单:商店版 ID(CWS 重签,不认 manifest key)与 unpacked 固定 ID 同信,商店 URL 锚定 item ID', () => {
|
|
144
196
|
assert.equal(EXTENSION_ORIGIN_STORE, `chrome-extension://${EXTENSION_ID_STORE}`);
|
|
145
197
|
assert.ok(EXTENSION_ORIGINS.includes(EXTENSION_ORIGIN));
|
|
146
198
|
assert.ok(EXTENSION_ORIGINS.includes(EXTENSION_ORIGIN_STORE));
|
|
147
199
|
assert.equal(EXTENSION_ORIGINS.length, 2);
|
|
148
200
|
assert.ok(EXTENSION_STORE_URL.includes(EXTENSION_ID_STORE));
|
|
149
201
|
});
|
|
150
|
-
await check('manifest 合同:MV3 + 最小权限(cookies/alarms;无
|
|
202
|
+
await check('manifest 合同:MV3 + 最小权限(cookies/alarms;无 storage/options_page/tabs;founder 2026-09-11 不向员工暴露任何配置面)', () => {
|
|
151
203
|
assert.equal(manifest.manifest_version, 3);
|
|
152
|
-
assert.deepEqual([
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
].sort((a, b)=>a - b));
|
|
204
|
+
assert.deepEqual([...manifest.permissions].sort(), ['alarms', 'cookies']);
|
|
205
|
+
assert.ok(!manifest.permissions.includes('storage'), '不得用 storage 持久化配置(配置由 portal 静默派发)');
|
|
206
|
+
assert.equal(manifest.options_page, undefined, '不得挂 options 页(employee-facing UI 全禁)');
|
|
207
|
+
assert.ok(Array.isArray(manifest.optional_host_permissions), '应声明 optional_host_permissions(远程 gotry-backend 域按需授权)');
|
|
208
|
+
});
|
|
209
|
+
const manifestPorts = manifest.host_permissions
|
|
210
|
+
.map((p) => /127\.0\.0\.1:(\d+)/.exec(p)?.[1])
|
|
211
|
+
.filter((v) => v != null)
|
|
212
|
+
.map(Number)
|
|
213
|
+
.sort((a, b) => a - b);
|
|
214
|
+
await check('防漂移:桥端口池(Node BRIDGE_PORTS)= manifest host_permissions 回环面(+ ctrip/dida 星域;2026-09-11 保留站点域:founder 不接受单凭 chrome.cookies API 推断,显式授权链可审计)', () => {
|
|
215
|
+
assert.deepEqual(manifestPorts, [...BRIDGE_PORTS].sort((a, b) => a - b));
|
|
164
216
|
assert.ok(manifest.host_permissions.includes('https://*.ctrip.com/*'));
|
|
217
|
+
assert.ok(manifest.host_permissions.includes('https://*.dida.com/*'));
|
|
218
|
+
assert.ok(manifest.host_permissions.includes('https://dida.com/*'));
|
|
165
219
|
});
|
|
166
|
-
await check('防漂移:content_scripts 双 world 挂 ctrip 双站+12306(MAIN 嗅探 + ISOLATED 桥;2026-09-03
|
|
220
|
+
await check('防漂移:content_scripts 双 world 挂 ctrip 双站+12306+dida(MAIN 嗅探 + ISOLATED 桥;2026-09-03 酒/火实装,2026-09-09 dida 实装)', () => {
|
|
167
221
|
assert.equal(manifest.content_scripts.length, 2);
|
|
168
|
-
for (const cs of manifest.content_scripts){
|
|
169
|
-
assert.deepEqual(cs.matches, [
|
|
170
|
-
'https://flights.ctrip.com/*',
|
|
171
|
-
`https://${HOTEL_SITE_HOST}/*`,
|
|
172
|
-
`https://${TRAIN_SITE_HOST}/*`,
|
|
173
|
-
'https://www.12306.cn/*'
|
|
174
|
-
]);
|
|
222
|
+
for (const cs of manifest.content_scripts) {
|
|
223
|
+
assert.deepEqual(cs.matches, ['https://flights.ctrip.com/*', `https://${HOTEL_SITE_HOST}/*`, `https://${TRAIN_SITE_HOST}/*`, 'https://www.12306.cn/*', `https://${DIDA_SITE_HOST}/*`]);
|
|
175
224
|
assert.equal(cs.run_at, 'document_start');
|
|
176
225
|
}
|
|
177
|
-
const worlds = manifest.content_scripts.map((cs)=>cs.world ?? 'ISOLATED').sort();
|
|
178
|
-
assert.deepEqual(worlds, [
|
|
179
|
-
|
|
180
|
-
'MAIN'
|
|
181
|
-
]);
|
|
182
|
-
assert.deepEqual(manifest.content_scripts.map((cs)=>cs.js[0]).sort(), [
|
|
183
|
-
'content-bridge.js',
|
|
184
|
-
'content-main.js'
|
|
185
|
-
]);
|
|
226
|
+
const worlds = manifest.content_scripts.map((cs) => cs.world ?? 'ISOLATED').sort();
|
|
227
|
+
assert.deepEqual(worlds, ['ISOLATED', 'MAIN']);
|
|
228
|
+
assert.deepEqual(manifest.content_scripts.map((cs) => cs.js[0]).sort(), ['content-bridge.js', 'content-main.js']);
|
|
186
229
|
});
|
|
187
230
|
const backgroundJs = read('background.js');
|
|
188
231
|
const contentMainJs = read('content-main.js');
|
|
189
232
|
const contentBridgeJs = read('content-bridge.js');
|
|
190
|
-
await check('
|
|
233
|
+
await check('配置交付(2026-09-11 hotelbyte 口径:员工零配置):portal 派发 join ticket → SW 静默接收;无 storage/options UI', () => {
|
|
234
|
+
assert.ok(!backgroundJs.includes('chrome.storage'), '背景 SW 不得读 chrome.storage(配置由 portal 派发,非持久化)');
|
|
235
|
+
assert.ok(!backgroundJs.includes('chrome.storage.local.get'), '背景 SW 不得列读 storage.local.gotryRemoteBridge');
|
|
236
|
+
assert.ok(!backgroundJs.includes('chrome.storage.onChanged'), '背景 SW 不得订阅 storage 变更');
|
|
237
|
+
assert.ok(backgroundJs.includes('chrome.runtime.onMessage'), '背景 SW 应订阅 runtime.onMessage 接收 portal 派发');
|
|
238
|
+
assert.ok(backgroundJs.includes('\'gotry-join\''), '背景 SW 应识别 gotry-join 消息(portal content-bridge 派发)');
|
|
239
|
+
assert.ok(backgroundJs.includes('joinTicket'), '背景 SW 应有 joinTicket 模块变量缓存 ticket');
|
|
240
|
+
assert.ok(backgroundJs.includes('isJoinFresh'), '背景 SW 应有 join ticket 有效性校验(过期/token 缺失即弃)');
|
|
241
|
+
assert.ok(contentBridgeJs.includes('__gotryJoinTicket'), 'content-bridge 应在页面里读 window.__gotryJoinTicket');
|
|
242
|
+
assert.ok(contentBridgeJs.includes('type: \'gotry-join\''), 'content-bridge 应把 ticket 转 gotry-join 消息派给 SW');
|
|
243
|
+
assert.ok(contentBridgeJs.includes('chrome.runtime.sendMessage'), 'content-bridge 应用 chrome.runtime.sendMessage 派发');
|
|
244
|
+
assert.ok(!backgroundJs.includes('options.html'), '扩展不得引用 options.html/options.js(employee 零配置)');
|
|
245
|
+
});
|
|
246
|
+
await check('dida multiCollect(2026-09-11,推荐流双接口):背景分类函数 + waitSniffMulti 分桶结算', () => {
|
|
247
|
+
assert.ok(backgroundJs.includes('classifyDidaSniff'), '背景 SW 应抽 dida sniff 分类函数');
|
|
248
|
+
assert.ok(backgroundJs.includes('SearchHomepageRecommendHotels'), '多回包分类应含 hotels 接口');
|
|
249
|
+
assert.ok(backgroundJs.includes('SearchHomepageRecommendPrices'), '多回包分类应含 recommendPrices 接口');
|
|
250
|
+
assert.ok(backgroundJs.includes('waitSniffMulti'), '应有多回包分桶结算版本(双齐即返)');
|
|
251
|
+
assert.ok(backgroundJs.includes('job.multiCollect'), '背景 SW 应按 job.multiCollect 分派单首包/多回包');
|
|
252
|
+
});
|
|
253
|
+
await check('版本跟随主版本:manifest version_name = package.json version;version = 四段投影(0.0.1-rc.N → 0.0.1.N,founder 2026-09-03 拍板)', () => {
|
|
191
254
|
const pkgVersion = JSON.parse(readFileSync(join(EXT_DIR, '..', 'package.json'), 'utf8')).version;
|
|
192
255
|
assert.equal(manifest.version_name, pkgVersion, 'version_name 应与 gotry 主版本逐字一致');
|
|
193
256
|
const m = /^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(pkgVersion);
|
|
@@ -195,260 +258,212 @@ async function main() {
|
|
|
195
258
|
assert.equal(manifest.version, `${m[1]}.${m[2]}.${m[3]}.${m[4]}`, 'version 应为主版本的四段投影(Chrome manifest 只收点分整数)');
|
|
196
259
|
assert.equal(manifest.content_scripts.length, 2);
|
|
197
260
|
});
|
|
198
|
-
await check('防漂移:票据 cookie 名单(Node LOGIN_COOKIE_NAMES)= 扩展 SITES.ticketNames', ()=>{
|
|
199
|
-
for (const name of LOGIN_COOKIE_NAMES){
|
|
261
|
+
await check('防漂移:票据 cookie 名单(Node LOGIN_COOKIE_NAMES)= 扩展 SITES.ticketNames', () => {
|
|
262
|
+
for (const name of LOGIN_COOKIE_NAMES) {
|
|
200
263
|
assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺票据名 ${name}`);
|
|
201
264
|
}
|
|
202
265
|
assert.ok(backgroundJs.includes("domain: 'ctrip.com'"), 'background.js 站点域应与 LOGIN_TARGETS.domain 对账');
|
|
203
266
|
});
|
|
204
|
-
await check('防漂移:NETWORK_HINTS + HOTEL_NETWORK_HINTS(Node)= content-main 嗅探面(MAIN-world)', ()=>{
|
|
205
|
-
for (const hint of NETWORK_HINTS){
|
|
267
|
+
await check('防漂移:NETWORK_HINTS + HOTEL_NETWORK_HINTS(Node)= content-main 嗅探面(MAIN-world)', () => {
|
|
268
|
+
for (const hint of NETWORK_HINTS) {
|
|
206
269
|
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 hint ${hint.source}`);
|
|
207
270
|
}
|
|
208
|
-
for (const hint of HOTEL_NETWORK_HINTS){
|
|
271
|
+
for (const hint of HOTEL_NETWORK_HINTS) {
|
|
209
272
|
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺酒店 hint ${hint.source}`);
|
|
210
273
|
}
|
|
211
|
-
for (const hint of TRAIN_NETWORK_HINTS){
|
|
274
|
+
for (const hint of TRAIN_NETWORK_HINTS) {
|
|
212
275
|
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺火车 hint ${hint.source}`);
|
|
213
276
|
}
|
|
214
277
|
assert.ok(contentMainJs.includes('12306'), 'content-main.js 应感知 12306 页域(火车嗅探作用域)');
|
|
215
278
|
assert.ok(contentMainJs.includes('hotels\\.ctrip\\.com'), 'content-main.js 应感知酒店页域(形状嗅探兜底的作用域)');
|
|
216
279
|
});
|
|
217
|
-
await check('防漂移:酒店形状签名(Node looksLikeHotelListBody 同款)= content-main HOTEL_BODY_SIG_RE', ()=>{
|
|
280
|
+
await check('防漂移:酒店形状签名(Node looksLikeHotelListBody 同款)= content-main HOTEL_BODY_SIG_RE', () => {
|
|
218
281
|
assert.ok(stripRe(contentMainJs).includes('"hotelList"|"hotelMatchInfos"|"hotelName"'), '形状嗅探签名两侧必须逐字一致');
|
|
219
282
|
});
|
|
220
|
-
await check('防漂移:检索/登录 URL 面(background per-site 白名单 flights+hotels;ISOLATED 桥转发两类事件)', ()=>{
|
|
283
|
+
await check('防漂移:检索/登录 URL 面(background per-site 白名单 flights+hotels;ISOLATED 桥转发两类事件)', () => {
|
|
221
284
|
assert.ok(backgroundJs.includes('https://flights.ctrip.com/'), 'background 应含机票检索白名单');
|
|
222
285
|
assert.ok(backgroundJs.includes(`https://${HOTEL_SITE_HOST}/`), 'background 应含酒店检索白名单(2026-09-03 实装)');
|
|
223
286
|
assert.ok(backgroundJs.includes('https://kyfw.12306.cn/'), 'background 应含火车检索白名单(2026-09-03 实装)');
|
|
224
287
|
assert.ok(backgroundJs.includes("'ctrip-hotel'"), 'background SITES 应注册 ctrip-hotel');
|
|
225
288
|
assert.ok(backgroundJs.includes("'train-12306'"), 'background SITES 应注册 train-12306');
|
|
289
|
+
assert.ok(backgroundJs.includes("'dida-portal'"), 'background SITES 应注册 dida-portal(2026-09-09 实装)');
|
|
290
|
+
assert.ok(backgroundJs.includes('https://portal.dida.com/'), 'background 应含 dida 检索白名单');
|
|
226
291
|
assert.ok(contentMainJs.includes('gotry-ctrip-sniff'));
|
|
227
292
|
assert.ok(contentBridgeJs.includes('gotry-ctrip-sniff'));
|
|
293
|
+
assert.ok(contentBridgeJs.includes('url: d.url'), 'content bridge 应转发嗅探响应 URL');
|
|
294
|
+
assert.ok(backgroundJs.includes('url: String(msg.url ??'), 'background 应保留嗅探响应 URL');
|
|
295
|
+
assert.ok(contentMainJs.includes('typeof res.url === \'string\''), 'content-main fetch 应读取浏览器 response.url');
|
|
296
|
+
assert.ok(contentMainJs.includes('typeof xhr.responseURL === \'string\''), 'content-main XHR 应读取浏览器 responseURL');
|
|
228
297
|
assert.ok(contentBridgeJs.includes('gotry-page'));
|
|
229
298
|
});
|
|
230
|
-
await check('
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
],
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
299
|
+
await check('相对 12306 输入 → fetch/XHR 均转发同一响应的绝对 URL 与 body', async () => {
|
|
300
|
+
const events = await contentMainRelativeResponseUrlProof(contentMainJs);
|
|
301
|
+
const expectedUrl = 'https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=2026-12-01&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM';
|
|
302
|
+
assert.deepEqual(events, [
|
|
303
|
+
{ url: expectedUrl, body: '{"data":{"result":[]}}' },
|
|
304
|
+
{ url: expectedUrl, body: '{"data":{"result":[]}}' },
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
await check('防漂移(Dida):DIDA_NETWORK_HINTS(Node)= content-main 嗅探面;票据名= background SITES;manifest 覆盖 portal.dida.com', () => {
|
|
308
|
+
for (const hint of DIDA_NETWORK_HINTS) {
|
|
309
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 dida hint ${hint.source}`);
|
|
310
|
+
}
|
|
311
|
+
assert.ok(contentMainJs.includes('portal\\.dida\\.com'), 'content-main.js 应感知 dida 页域');
|
|
312
|
+
for (const name of DIDA_LOGIN_COOKIE_NAMES) {
|
|
313
|
+
assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺 dida 票据名 ${name}`);
|
|
314
|
+
}
|
|
315
|
+
assert.ok(backgroundJs.includes("domain: 'dida.com'"), 'background.js dida 站点域应与 LOGIN_TARGETS.domain 对账');
|
|
316
|
+
const manifestAny = JSON.parse(readFileSync(join(EXT_DIR, 'manifest.json'), 'utf8'));
|
|
317
|
+
assert.ok(manifestAny.host_permissions.includes('https://*.dida.com/*'), 'manifest host_permissions 应含 dida 域');
|
|
318
|
+
assert.ok(manifestAny.content_scripts.every((b) => b.matches.includes(`https://${DIDA_SITE_HOST}/*`)), 'manifest 两组 content_scripts 均应注入 portal.dida.com');
|
|
319
|
+
assert.ok(stripRe(contentMainJs).includes('"HotelPriceList"|"RatePlanList"'), 'dida 形状签名两侧必须逐字一致');
|
|
320
|
+
});
|
|
321
|
+
await check('物理只读形态:扩展全部 fetch 只指向桥(loopback / 远程桥 URL / bridgeBase() / joinTicket.bridgeUrl 含 .replace 派生 / 变量别名 url);不用 chrome.debugger;站点域一律 zero fetch', () => {
|
|
322
|
+
for (const [name, src] of [['background', backgroundJs], ['content-main', contentMainJs], ['content-bridge', contentBridgeJs]]) {
|
|
323
|
+
// joinTicket.bridgeUrl 分支用 [^}]* 而非 [^)]*:允许同一变量的 .replace(...) 等纯派生表达式(如去尾斜杠),
|
|
324
|
+
// 派生链一旦写出嵌套 `${}` 或新目标即不再命中——只放宽对同一变量的匹配,不放宽目标集合(fail-closed)。
|
|
325
|
+
const bridgeFetches = src.match(/fetch\(`?(?:http:\/\/127\.0\.0\.1|\$\{bridgeBase\(\)\}|\$\{remoteBridge\.baseUrl\}|\$\{joinTicket\.bridgeUrl[^}]*\})|\bfetch\(\s*url\b/g) ?? [];
|
|
246
326
|
const allFetches = src.match(/fetch\(/g) ?? [];
|
|
247
|
-
assert.equal(
|
|
327
|
+
assert.equal(bridgeFetches.length, allFetches.length, `${name}: fetch 必须只指向桥——扩展零写行为的代码面证据`);
|
|
248
328
|
}
|
|
249
329
|
assert.ok(!backgroundJs.includes('chrome.debugger'), '扩展不得使用 chrome.debugger(警告条/调试面)');
|
|
250
330
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
]);
|
|
331
|
+
/* ---------- ② 桥行为(临时端口 + 假扩展客户端) ---------- */
|
|
332
|
+
const b = await mustBridge([0]);
|
|
254
333
|
const port = b.port;
|
|
255
|
-
await check('桥只绑回环临时端口;/health 无 Origin 可诊断', async ()=>{
|
|
334
|
+
await check('桥只绑回环临时端口;/health 无 Origin 可诊断', async () => {
|
|
256
335
|
assert.ok(port > 0);
|
|
257
336
|
const r = await fetch(`http://127.0.0.1:${port}/health`);
|
|
258
|
-
const body = await r.json();
|
|
337
|
+
const body = (await r.json());
|
|
259
338
|
assert.equal(r.status, 200);
|
|
260
339
|
assert.equal(body.ok, true);
|
|
261
340
|
assert.equal(body.service, 'gotry-session-bridge');
|
|
262
341
|
});
|
|
263
342
|
await check('默认桥:parked 扩展长轮询不得钉住宿主 CLI 进程', assertParkedClientDoesNotPinHost);
|
|
264
|
-
await check('origin 白名单:邪恶源(网页跨域必带)POST /jobs 与 /results 一律 403', async ()=>{
|
|
265
|
-
for (const path of [
|
|
266
|
-
'/jobs',
|
|
267
|
-
'/results/whatever'
|
|
268
|
-
]){
|
|
343
|
+
await check('origin 白名单:邪恶源(网页跨域必带)POST /jobs 与 /results 一律 403', async () => {
|
|
344
|
+
for (const path of ['/jobs', '/results/whatever']) {
|
|
269
345
|
const r = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
270
346
|
method: 'POST',
|
|
271
|
-
headers: {
|
|
272
|
-
|
|
273
|
-
origin: 'https://evil.example'
|
|
274
|
-
},
|
|
275
|
-
body: '{}'
|
|
347
|
+
headers: { 'content-type': 'application/json', origin: 'https://evil.example' },
|
|
348
|
+
body: '{}',
|
|
276
349
|
});
|
|
277
350
|
assert.equal(r.status, 403, path);
|
|
278
351
|
}
|
|
279
352
|
});
|
|
280
|
-
await check('origin 白名单:商店版扩展源(CWS 重签 ID,2026-09-02 上架)POST 放行且计入心跳', async ()=>{
|
|
281
|
-
const storeLane = await mustBridge([
|
|
282
|
-
0
|
|
283
|
-
]);
|
|
353
|
+
await check('origin 白名单:商店版扩展源(CWS 重签 ID,2026-09-02 上架)POST 放行且计入心跳', async () => {
|
|
354
|
+
const storeLane = await mustBridge([0]);
|
|
284
355
|
try {
|
|
285
356
|
assert.equal(storeLane.extensionConnected(), false);
|
|
286
357
|
const r = await fetch(`http://127.0.0.1:${storeLane.port}/results/whatever`, {
|
|
287
358
|
method: 'POST',
|
|
288
|
-
headers: {
|
|
289
|
-
|
|
290
|
-
origin: EXTENSION_ORIGIN_STORE
|
|
291
|
-
},
|
|
292
|
-
body: '{}'
|
|
359
|
+
headers: { 'content-type': 'application/json', origin: EXTENSION_ORIGIN_STORE },
|
|
360
|
+
body: '{}',
|
|
293
361
|
});
|
|
294
362
|
assert.equal(r.status, 200, '商店版扩展源不得吃 403(否则商店通道全断)');
|
|
295
363
|
assert.equal(storeLane.extensionConnected(), true, '白名单源的任何请求都刷新心跳');
|
|
296
|
-
}
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
297
366
|
await storeLane.close();
|
|
298
367
|
}
|
|
299
368
|
});
|
|
300
|
-
await check('心跳判定:初始未连接;合法请求(health/长轮询)后视为在线', async ()=>{
|
|
369
|
+
await check('心跳判定:初始未连接;合法请求(health/长轮询)后视为在线', async () => {
|
|
301
370
|
assert.equal(b.extensionConnected(), false);
|
|
302
371
|
await heartbeat(port);
|
|
303
372
|
assert.equal(b.extensionConnected(), true);
|
|
304
373
|
});
|
|
305
|
-
await check('提交-回包闭环:cookie-names job 由假扩展取活并回包,submit 原样收果', async ()=>{
|
|
306
|
-
const submitP = b.submit({
|
|
307
|
-
|
|
308
|
-
site: 'ctrip-flight',
|
|
309
|
-
timeoutMs: 5_000
|
|
310
|
-
}, {
|
|
311
|
-
timeoutMs: 5_000
|
|
312
|
-
});
|
|
313
|
-
const claimed = await claimOnce(port, (job)=>{
|
|
374
|
+
await check('提交-回包闭环:cookie-names job 由假扩展取活并回包,submit 原样收果', async () => {
|
|
375
|
+
const submitP = b.submit({ kind: 'cookie-names', site: 'ctrip-flight', timeoutMs: 5_000 }, { timeoutMs: 5_000 });
|
|
376
|
+
const claimed = await claimOnce(port, (job) => {
|
|
314
377
|
assert.equal(job.kind, 'cookie-names');
|
|
315
378
|
assert.equal(job.site, 'ctrip-flight');
|
|
316
|
-
return {
|
|
317
|
-
ok: true,
|
|
318
|
-
kind: 'cookie-names',
|
|
319
|
-
names: [
|
|
320
|
-
'cticket',
|
|
321
|
-
'uid'
|
|
322
|
-
]
|
|
323
|
-
};
|
|
379
|
+
return { ok: true, kind: 'cookie-names', names: ['cticket', 'uid'] };
|
|
324
380
|
});
|
|
325
381
|
assert.ok(claimed.job);
|
|
326
382
|
const outcome = await submitP;
|
|
327
383
|
assert.ok(outcome.ok);
|
|
328
|
-
assert.deepEqual(outcome.ok ? outcome.result.names : [], [
|
|
329
|
-
'cticket',
|
|
330
|
-
'uid'
|
|
331
|
-
]);
|
|
384
|
+
assert.deepEqual(outcome.ok ? outcome.result.names : [], ['cticket', 'uid']);
|
|
332
385
|
});
|
|
333
|
-
await check('cookie 红线:扩展代码只上报名(c.name 映射),值不进任何结果对象', ()=>{
|
|
386
|
+
await check('cookie 红线:扩展代码只上报名(c.name 映射),值不进任何结果对象', () => {
|
|
334
387
|
assert.ok(backgroundJs.includes('.map((c) => c.name)'));
|
|
335
388
|
assert.ok(!/\.value/.test(backgroundJs), 'background.js 不得触碰 cookie.value');
|
|
336
389
|
});
|
|
337
|
-
await check('长轮询幂等:并发取活只有一个客户端领到 job;后到者空手(快速中止,不挂进程)', async ()=>{
|
|
390
|
+
await check('长轮询幂等:并发取活只有一个客户端领到 job;后到者空手(快速中止,不挂进程)', async () => {
|
|
338
391
|
const firstAc = new AbortController();
|
|
339
392
|
const secondAc = new AbortController();
|
|
340
|
-
const firstP = claimOnce(port, ()=>({
|
|
341
|
-
ok: true,
|
|
342
|
-
kind: 'cookie-names',
|
|
343
|
-
names: [
|
|
344
|
-
'cticket'
|
|
345
|
-
]
|
|
346
|
-
}), firstAc.signal).catch(()=>({
|
|
347
|
-
job: null
|
|
348
|
-
}));
|
|
393
|
+
const firstP = claimOnce(port, () => ({ ok: true, kind: 'cookie-names', names: ['cticket'] }), firstAc.signal).catch(() => ({ job: null }));
|
|
349
394
|
let secondP = null;
|
|
350
395
|
try {
|
|
351
396
|
await waitForParkedCount(port, 1);
|
|
352
|
-
secondP = claimOnce(port, null, secondAc.signal).catch(()=>({
|
|
353
|
-
job: null
|
|
354
|
-
}));
|
|
397
|
+
secondP = claimOnce(port, null, secondAc.signal).catch(() => ({ job: null }));
|
|
355
398
|
await waitForParkedCount(port, 2);
|
|
356
|
-
const submitP = b.submit({
|
|
357
|
-
kind: 'cookie-names',
|
|
358
|
-
site: 'ctrip-flight',
|
|
359
|
-
timeoutMs: 5_000
|
|
360
|
-
}, {
|
|
361
|
-
timeoutMs: 5_000
|
|
362
|
-
});
|
|
399
|
+
const submitP = b.submit({ kind: 'cookie-names', site: 'ctrip-flight', timeoutMs: 5_000 }, { timeoutMs: 5_000 });
|
|
363
400
|
const first = await firstP;
|
|
364
401
|
assert.ok(first.job, '先进入 parked 队列的取活者必须领到 job');
|
|
365
402
|
const outcome = await submitP;
|
|
366
403
|
assert.ok(outcome.ok);
|
|
367
|
-
}
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
368
406
|
firstAc.abort();
|
|
369
407
|
secondAc.abort();
|
|
370
|
-
if (secondP)
|
|
408
|
+
if (secondP)
|
|
409
|
+
await secondP;
|
|
371
410
|
}
|
|
372
411
|
});
|
|
373
|
-
await check('超时语义:扩展在线但不回包 → timeout(降级 error,不伪装成功)', async ()=>{
|
|
374
|
-
const outcome = await b.submit({
|
|
375
|
-
kind: 'search',
|
|
376
|
-
site: 'ctrip-flight',
|
|
377
|
-
url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01',
|
|
378
|
-
timeoutMs: 300
|
|
379
|
-
}, {
|
|
380
|
-
timeoutMs: 300
|
|
381
|
-
});
|
|
412
|
+
await check('超时语义:扩展在线但不回包 → timeout(降级 error,不伪装成功)', async () => {
|
|
413
|
+
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 });
|
|
382
414
|
assert.ok(!outcome.ok);
|
|
383
415
|
assert.equal(outcome.ok ? null : outcome.reason, 'timeout');
|
|
384
416
|
});
|
|
385
|
-
await check('扩展未连接:有界等待后 extension-not-connected(用户门 no-spend,不空耗)', async ()=>{
|
|
386
|
-
const fresh = await mustBridge([
|
|
387
|
-
0
|
|
388
|
-
]);
|
|
417
|
+
await check('扩展未连接:有界等待后 extension-not-connected(用户门 no-spend,不空耗)', async () => {
|
|
418
|
+
const fresh = await mustBridge([0]);
|
|
389
419
|
try {
|
|
390
|
-
const outcome = await fresh.submit({
|
|
391
|
-
kind: 'cookie-names',
|
|
392
|
-
site: 'ctrip-flight'
|
|
393
|
-
}, {
|
|
394
|
-
extensionWaitMs: 400,
|
|
395
|
-
timeoutMs: 2_000
|
|
396
|
-
});
|
|
420
|
+
const outcome = await fresh.submit({ kind: 'cookie-names', site: 'ctrip-flight' }, { extensionWaitMs: 400, timeoutMs: 2_000 });
|
|
397
421
|
assert.ok(!outcome.ok);
|
|
398
422
|
assert.equal(outcome.ok ? null : outcome.reason, 'extension-not-connected');
|
|
399
|
-
}
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
400
425
|
await fresh.close();
|
|
401
426
|
}
|
|
402
427
|
});
|
|
403
|
-
await check('close:桥关闭后排队任务以 bridge-unavailable 结算(零花费)', async ()=>{
|
|
404
|
-
const tmp = await mustBridge([
|
|
405
|
-
0
|
|
406
|
-
]);
|
|
428
|
+
await check('close:桥关闭后排队任务以 bridge-unavailable 结算(零花费)', async () => {
|
|
429
|
+
const tmp = await mustBridge([0]);
|
|
407
430
|
await heartbeat(tmp.port);
|
|
408
|
-
const submitP = tmp.submit({
|
|
409
|
-
kind: 'cookie-names',
|
|
410
|
-
site: 'ctrip-flight',
|
|
411
|
-
timeoutMs: 30_000
|
|
412
|
-
}, {
|
|
413
|
-
timeoutMs: 30_000
|
|
414
|
-
});
|
|
431
|
+
const submitP = tmp.submit({ kind: 'cookie-names', site: 'ctrip-flight', timeoutMs: 30_000 }, { timeoutMs: 30_000 });
|
|
415
432
|
await tmp.close();
|
|
416
433
|
const outcome = await submitP;
|
|
417
434
|
assert.ok(!outcome.ok);
|
|
418
435
|
assert.equal(outcome.ok ? null : outcome.reason, 'bridge-unavailable');
|
|
419
436
|
});
|
|
420
|
-
|
|
437
|
+
/* ---------- ③ 车道语义 ---------- */
|
|
438
|
+
await check('classifyBridgeFailure:只有 extension-not-connected 是用户门(needs-extension),其余 error', () => {
|
|
421
439
|
assert.equal(classifyBridgeFailure('extension-not-connected'), 'needs-extension');
|
|
422
440
|
assert.equal(classifyBridgeFailure('bridge-unavailable'), 'error');
|
|
423
441
|
assert.equal(classifyBridgeFailure('timeout'), 'error');
|
|
424
442
|
assert.equal(classifyBridgeFailure('job-error'), 'error');
|
|
425
443
|
});
|
|
426
444
|
const savedTransportEnv = process.env.GOTRY_SESSION_TRANSPORT;
|
|
427
|
-
const restoreTransportEnv = ()=>{
|
|
428
|
-
if (savedTransportEnv === undefined)
|
|
429
|
-
|
|
445
|
+
const restoreTransportEnv = () => {
|
|
446
|
+
if (savedTransportEnv === undefined)
|
|
447
|
+
delete process.env.GOTRY_SESSION_TRANSPORT;
|
|
448
|
+
else
|
|
449
|
+
process.env.GOTRY_SESSION_TRANSPORT = savedTransportEnv;
|
|
430
450
|
};
|
|
431
451
|
try {
|
|
432
452
|
delete process.env.GOTRY_SESSION_TRANSPORT;
|
|
433
|
-
await check('resolveTransportMode:扩展默认;cdp 须显式 opt-in;profileDir=persistent(测试)', ()=>{
|
|
453
|
+
await check('resolveTransportMode:扩展默认;cdp 须显式 opt-in;profileDir=persistent(测试)', () => {
|
|
434
454
|
assert.equal(resolveTransportMode(), 'extension');
|
|
435
455
|
process.env.GOTRY_SESSION_TRANSPORT = 'cdp';
|
|
436
456
|
assert.equal(resolveTransportMode(), 'cdp');
|
|
437
457
|
assert.equal(resolveTransportMode('/tmp/isolated-profile'), 'persistent');
|
|
438
458
|
});
|
|
439
|
-
}
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
440
461
|
restoreTransportEnv();
|
|
441
462
|
}
|
|
442
463
|
const auditDir = mkdtempSync(join(tmpdir(), 'gotry-ext-audit-'));
|
|
443
464
|
const auditPath = join(auditDir, 'session-incidents.jsonl');
|
|
444
|
-
appendExtensionAudit(auditPath, {
|
|
445
|
-
|
|
446
|
-
site: 'ctrip-flight',
|
|
447
|
-
url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01',
|
|
448
|
-
jobId: 'search',
|
|
449
|
-
result: 'body 1024B'
|
|
450
|
-
});
|
|
451
|
-
await check('审计:扩展 job 落 session-incidents 同款 JSONL(kind 区分,url 截断)', ()=>{
|
|
465
|
+
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' });
|
|
466
|
+
await check('审计:扩展 job 落 session-incidents 同款 JSONL(kind 区分,url 截断)', () => {
|
|
452
467
|
const lines = readFileSync(auditPath, 'utf8').trim().split('\n');
|
|
453
468
|
assert.equal(lines.length, 1);
|
|
454
469
|
const entry = JSON.parse(lines[0]);
|
|
@@ -456,123 +471,73 @@ async function main() {
|
|
|
456
471
|
assert.equal(entry.site, 'ctrip-flight');
|
|
457
472
|
assert.ok(entry.url.length <= 400);
|
|
458
473
|
});
|
|
459
|
-
rmSync(auditDir, {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
});
|
|
463
|
-
await check('车道闭环(注入临时桥+假扩展):cookie-names 只吐名字;search job 回传正文;登录快路径零交互', async ()=>{
|
|
464
|
-
const lane = await mustBridge([
|
|
465
|
-
0
|
|
466
|
-
]);
|
|
474
|
+
rmSync(auditDir, { recursive: true, force: true });
|
|
475
|
+
await check('车道闭环(注入临时桥+假扩展):cookie-names 只吐名字;search job 回传正文;登录快路径零交互', async () => {
|
|
476
|
+
const lane = await mustBridge([0]);
|
|
467
477
|
__setSessionBridgeForTest(lane);
|
|
468
478
|
try {
|
|
469
479
|
let done = false;
|
|
470
|
-
const claimLoop = (async ()=>{
|
|
471
|
-
while(!done){
|
|
480
|
+
const claimLoop = (async () => {
|
|
481
|
+
while (!done) {
|
|
472
482
|
const ac = new AbortController();
|
|
473
|
-
const bail = setTimeout(()=>ac.abort(), 2_000);
|
|
483
|
+
const bail = setTimeout(() => ac.abort(), 2_000);
|
|
474
484
|
try {
|
|
475
|
-
await claimOnce(lane.port, (job)=>{
|
|
476
|
-
if (job.kind === 'cookie-names')
|
|
477
|
-
ok: true,
|
|
478
|
-
kind: 'cookie-names',
|
|
479
|
-
names: [
|
|
480
|
-
'cticket'
|
|
481
|
-
]
|
|
482
|
-
};
|
|
485
|
+
await claimOnce(lane.port, (job) => {
|
|
486
|
+
if (job.kind === 'cookie-names')
|
|
487
|
+
return { ok: true, kind: 'cookie-names', names: ['cticket'] };
|
|
483
488
|
if (job.kind === 'search') {
|
|
484
489
|
const u = String(job.url);
|
|
485
490
|
const isHotel = u.startsWith(`https://${HOTEL_SITE_HOST}/`);
|
|
486
491
|
const isTrain = u.startsWith('https://kyfw.12306.cn/');
|
|
487
492
|
assert.ok(u.startsWith('https://flights.ctrip.com/') || isHotel || isTrain, `search job 只允许已注册站点域,实际 ${u}`);
|
|
493
|
+
const requestedUrl = new URL(u);
|
|
494
|
+
const requestedDate = requestedUrl.searchParams.get('date') ?? '2026-12-01';
|
|
495
|
+
const wrongRoute = requestedUrl.searchParams.get('fs')?.endsWith(',BJP') === true;
|
|
496
|
+
const wrongDateEmpty = requestedDate === '2026-12-02';
|
|
497
|
+
const trainResponseUrl = wrongRoute
|
|
498
|
+
? `https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=${requestedDate}&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM`
|
|
499
|
+
: `https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date=${wrongDateEmpty ? '2026-12-01' : requestedDate}&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=KMM`;
|
|
488
500
|
return {
|
|
489
501
|
ok: true,
|
|
490
502
|
kind: 'search',
|
|
491
|
-
body: isHotel
|
|
492
|
-
data: {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
commentScore: 4.7,
|
|
499
|
-
priceInfo: {
|
|
500
|
-
avgPrice: 680
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
]
|
|
504
|
-
}
|
|
505
|
-
}) : isTrain ? JSON.stringify({
|
|
506
|
-
data: {
|
|
507
|
-
result: [
|
|
508
|
-
'|预订|24000000G1375|G1375|SHH|KMM|SHH|KMM|07:35|15:27|07:52|Y|yp|x|x|loc|01|02|Y|0|--|--|--|--|--|--|有|--|--|有|有|有|有|--|ex|st|'
|
|
509
|
-
],
|
|
510
|
-
map: {
|
|
511
|
-
SHH: '上海南',
|
|
512
|
-
KMM: '昆明'
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}) : JSON.stringify({
|
|
516
|
-
data: {
|
|
517
|
-
flightItineraryList: []
|
|
518
|
-
}
|
|
519
|
-
}),
|
|
520
|
-
title: isHotel ? '酒店列表' : isTrain ? '12306 车票预订' : '机票列表'
|
|
503
|
+
body: isHotel
|
|
504
|
+
? JSON.stringify({ data: { hotelList: [{ hotelId: 442516, hotelName: 'Hotel X', star: 5, commentScore: 4.7, priceInfo: { avgPrice: 680 } }] } })
|
|
505
|
+
: isTrain
|
|
506
|
+
? 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: '昆明' } } })
|
|
507
|
+
: JSON.stringify({ data: { flightItineraryList: [] } }),
|
|
508
|
+
url: isTrain ? trainResponseUrl : u,
|
|
509
|
+
title: isHotel ? '酒店列表' : isTrain ? '12306 车票预订' : '机票列表',
|
|
521
510
|
};
|
|
522
511
|
}
|
|
523
|
-
return {
|
|
524
|
-
ok: false,
|
|
525
|
-
error: `unexpected kind ${job.kind}`
|
|
526
|
-
};
|
|
512
|
+
return { ok: false, error: `unexpected kind ${job.kind}` };
|
|
527
513
|
}, ac.signal);
|
|
528
|
-
}
|
|
514
|
+
}
|
|
515
|
+
catch { /* park 被中止=常态 */ }
|
|
529
516
|
clearTimeout(bail);
|
|
530
517
|
}
|
|
531
518
|
})();
|
|
532
|
-
const cookies = await extensionCookieNames({
|
|
533
|
-
site: 'ctrip-flight',
|
|
534
|
-
domain: SITE_DOMAIN.replace(/^\./, ''),
|
|
535
|
-
ticketNames: LOGIN_COOKIE_NAMES
|
|
536
|
-
});
|
|
519
|
+
const cookies = await extensionCookieNames({ site: 'ctrip-flight', domain: SITE_DOMAIN.replace(/^\./, ''), ticketNames: LOGIN_COOKIE_NAMES });
|
|
537
520
|
assert.ok(cookies.ok);
|
|
538
|
-
assert.deepEqual(cookies.ok ? cookies.tickets : [], [
|
|
539
|
-
|
|
540
|
-
]);
|
|
541
|
-
const search = await extensionSearchJob({
|
|
542
|
-
site: 'ctrip-flight',
|
|
543
|
-
url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01',
|
|
544
|
-
timeoutMs: 3_000
|
|
545
|
-
});
|
|
521
|
+
assert.deepEqual(cookies.ok ? cookies.tickets : [], ['cticket']);
|
|
522
|
+
const search = await extensionSearchJob({ site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01', timeoutMs: 3_000 });
|
|
546
523
|
assert.ok(search.ok);
|
|
547
524
|
assert.equal(search.ok ? search.timedOut : true, false);
|
|
548
525
|
assert.ok(search.ok ? search.body.includes('flightItineraryList') : false);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
timeoutMs: 3_000
|
|
553
|
-
});
|
|
526
|
+
assert.equal(search.ok ? search.url : '', 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01', '响应 URL 穿过 background/result/channel');
|
|
527
|
+
// 酒店车道(2026-09-03 实装):per-site 白名单放行 hotels.ctrip.com,嗅探回包走形解析出结构化酒店
|
|
528
|
+
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 });
|
|
554
529
|
assert.ok(hotelLane.ok, '酒店 search job 应放行(per-site 白名单)');
|
|
555
530
|
assert.ok(hotelLane.ok ? hotelLane.body.includes('hotelList') : false);
|
|
556
531
|
__resetRateLimiterForTest();
|
|
557
|
-
const hotelSession = await sessionHotelSearch({
|
|
558
|
-
to: '迪拜',
|
|
559
|
-
cityId: 220,
|
|
560
|
-
checkIn: '2026-12-01',
|
|
561
|
-
checkOut: '2026-12-03',
|
|
562
|
-
timeoutMs: 3_000
|
|
563
|
-
});
|
|
532
|
+
const hotelSession = await sessionHotelSearch({ to: '迪拜', cityId: 220, checkIn: '2026-12-01', checkOut: '2026-12-03', timeoutMs: 3_000 });
|
|
564
533
|
assert.equal(hotelSession.verdict, 'hit', `酒店会话检索应 hit,实际 ${hotelSession.verdict}:${hotelSession.error ?? ''}`);
|
|
565
534
|
assert.ok(hotelSession.evidence.includes('[会话:ctrip-hotel@'), '酒店证据链 [会话:ctrip-hotel@ts]');
|
|
566
535
|
assert.equal(hotelSession.hotels?.[0]?.name, 'Hotel X');
|
|
567
536
|
assert.equal(hotelSession.hotels?.[0]?.price, 680);
|
|
568
537
|
assert.ok(hotelSession.hotels?.[0]?.jumpUrl?.includes('/hotel/442516'), 'jumpUrl 由 hotelId 构造(预订由人在落地页完成)');
|
|
538
|
+
// 火车车道(2026-09-03 实装):公开查询面无登录闸,per-site 白名单放行 kyfw,管道行解析出结构化车次
|
|
569
539
|
__resetRateLimiterForTest();
|
|
570
|
-
const trainSession = await sessionTrainSearch({
|
|
571
|
-
from: '上海',
|
|
572
|
-
to: '昆明',
|
|
573
|
-
date: '2026-12-01',
|
|
574
|
-
timeoutMs: 3_000
|
|
575
|
-
});
|
|
540
|
+
const trainSession = await sessionTrainSearch({ from: '上海', to: '昆明', date: '2026-12-01', timeoutMs: 3_000 });
|
|
576
541
|
assert.equal(trainSession.verdict, 'hit', `火车会话检索应 hit,实际 ${trainSession.verdict}:${trainSession.error ?? ''}`);
|
|
577
542
|
assert.ok(trainSession.evidence.includes('[会话:train-12306@'), '火车证据链 [会话:train-12306@ts]');
|
|
578
543
|
assert.ok(trainSession.evidence.includes('公开查询面'), '证据链标注公开查询面(无登录闸的诚实口径)');
|
|
@@ -580,112 +545,124 @@ async function main() {
|
|
|
580
545
|
assert.equal(trainSession.trains?.[0]?.depTime, '07:35');
|
|
581
546
|
assert.equal(trainSession.trains?.[0]?.durationMin, 472);
|
|
582
547
|
assert.ok(trainSession.trains?.[0]?.jumpUrl?.includes('kyfw.12306.cn'), 'jumpUrl=查询落地页(人选车完成预订)');
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
});
|
|
548
|
+
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');
|
|
549
|
+
__resetRateLimiterForTest();
|
|
550
|
+
const wrongRoute = await sessionTrainSearch({ from: '北京', to: '上海', date: '2026-12-03', timeoutMs: 3_000 });
|
|
551
|
+
assert.equal(wrongRoute.collection, undefined, 'wrong-route response URL → no collection');
|
|
552
|
+
assert.equal(factsFromSessionTrain({ from: '北京', to: '上海', date: '2026-12-03' }, wrongRoute).length, 0, 'wrong-route collector result → zero facts');
|
|
553
|
+
__resetRateLimiterForTest();
|
|
554
|
+
const wrongDateEmpty = await sessionTrainSearch({ from: '上海', to: '昆明', date: '2026-12-02', timeoutMs: 3_000 });
|
|
555
|
+
assert.equal(wrongDateEmpty.outcome.kind, 'recognized-empty', 'wrong-date seam keeps parsed empty presentation');
|
|
556
|
+
assert.equal(wrongDateEmpty.collection, undefined, 'wrong-date empty response URL → no collection');
|
|
557
|
+
assert.equal(factsFromSessionTrain({ from: '上海', to: '昆明', date: '2026-12-02' }, wrongDateEmpty).length, 0, 'wrong-date empty collector result → zero facts');
|
|
558
|
+
const login = await sessionLogin({ site: 'ctrip-flight' });
|
|
586
559
|
assert.equal(login.verdict, 'logged-in');
|
|
587
|
-
assert.deepEqual(login.tickets, [
|
|
588
|
-
'cticket'
|
|
589
|
-
]);
|
|
560
|
+
assert.deepEqual(login.tickets, ['cticket']);
|
|
590
561
|
assert.ok(login.evidence.includes('自动检测'));
|
|
591
562
|
done = true;
|
|
592
563
|
await claimLoop;
|
|
593
|
-
}
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
594
566
|
await __resetSessionBridgeForTest();
|
|
595
567
|
__setSessionBridgeForTest(null);
|
|
596
568
|
await lane.close();
|
|
597
569
|
}
|
|
598
570
|
});
|
|
599
|
-
await check('全链 fail-closed:扩展车道桥端口池全占 → sessionFlightSearch verdict=error(环境故障,非用户门)', async ()=>{
|
|
571
|
+
await check('全链 fail-closed:扩展车道桥端口池全占 → sessionFlightSearch verdict=error(环境故障,非用户门)', async () => {
|
|
572
|
+
// 设计契约:只有 extension-not-connected 才是 user gate (needs-extension);
|
|
573
|
+
// 端口池全占是环境故障(并行 gotry 实例/外部进程占端口)→ verdict=error,
|
|
574
|
+
// 不诱导用户去「装扩展」(装也解决不了)。
|
|
600
575
|
const { createServer } = await import('node:http');
|
|
601
576
|
const blockers = [];
|
|
602
577
|
try {
|
|
603
|
-
for (const port of [
|
|
604
|
-
8791,
|
|
605
|
-
8792,
|
|
606
|
-
8793,
|
|
607
|
-
8794,
|
|
608
|
-
8795
|
|
609
|
-
]){
|
|
578
|
+
for (const port of [8791, 8792, 8793, 8794, 8795]) {
|
|
610
579
|
const s = createServer().listen(port, '127.0.0.1');
|
|
611
|
-
await new Promise((r)=>s.once('listening', ()=>r()));
|
|
612
|
-
blockers.push({
|
|
613
|
-
close: ()=>new Promise((r)=>s.close(()=>r()))
|
|
614
|
-
});
|
|
580
|
+
await new Promise((r) => s.once('listening', () => r()));
|
|
581
|
+
blockers.push({ close: () => new Promise((r) => s.close(() => r())) });
|
|
615
582
|
}
|
|
616
583
|
await __resetSessionBridgeForTest();
|
|
617
|
-
const r = await sessionFlightSearch({
|
|
618
|
-
from: '上海',
|
|
619
|
-
to: '丽江',
|
|
620
|
-
date: '2026-12-01'
|
|
621
|
-
});
|
|
584
|
+
const r = await sessionFlightSearch({ from: '上海', to: '丽江', date: '2026-12-01' });
|
|
622
585
|
assert.equal(r.verdict, 'error', `端口池全占应 error(环境故障),实 ${r.verdict}:${r.error}`);
|
|
623
586
|
assert.ok(r.error?.includes('端口池'), '错误面必须点明端口池环境问题');
|
|
624
587
|
assert.ok(!r.error?.includes('一次性安装'), 'error 路径不应诱导用户去装扩展');
|
|
625
|
-
}
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
626
590
|
await __resetSessionBridgeForTest();
|
|
627
|
-
for (const b of blockers)
|
|
591
|
+
for (const b of blockers)
|
|
592
|
+
await b.close();
|
|
628
593
|
}
|
|
629
594
|
});
|
|
630
|
-
const waitingRecord = (verdict)=>({
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
595
|
+
const waitingRecord = (verdict) => ({
|
|
596
|
+
query_id: 'sf-01', route_segments: [], journey_type: 'direct', currency: '', price: 0, source: '',
|
|
597
|
+
fetched_at: '', verdict, latency_ms: 0, read_guard_blocked: 0,
|
|
598
|
+
});
|
|
599
|
+
await check('capability 路由:站点 job 只派给声明该站点的轮询者(同机多 Chrome 旧版扩展不毒领新站点 job)', async () => {
|
|
600
|
+
const lane = await mustBridge([0]);
|
|
601
|
+
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
602
|
+
try {
|
|
603
|
+
// 旧版扩展(caps 无 dida-portal,模拟商店版)先 parked
|
|
604
|
+
const oldExtP = claimOnce(lane.port, null, undefined, ['ctrip-flight', 'ctrip-hotel', 'train-12306']).catch(() => ({ job: null }));
|
|
605
|
+
await pause(200);
|
|
606
|
+
// dida job 提交:不得派给旧版扩展(它不声明 dida-portal)
|
|
607
|
+
const submitP = lane.submit({ kind: 'cookie-names', site: 'dida-portal' }, { timeoutMs: 3_000 });
|
|
608
|
+
await pause(200);
|
|
609
|
+
const stoleByOld = await Promise.race([oldExtP.then(() => true), pause(400).then(() => false)]);
|
|
610
|
+
assert.ok(!stoleByOld, '旧版扩展(capability 不含 dida-portal)不得领走 dida job');
|
|
611
|
+
// 新版扩展声明 dida-portal → 立即领到
|
|
612
|
+
const newExtGot = await claimOnce(lane.port, (job) => {
|
|
613
|
+
assert.equal(job.kind, 'cookie-names');
|
|
614
|
+
return { ok: true, kind: 'cookie-names', names: ['CN_M_DidaTravel'] };
|
|
615
|
+
}, undefined, ['ctrip-flight', 'ctrip-hotel', 'train-12306', 'dida-portal']);
|
|
616
|
+
assert.ok(newExtGot.job && newExtGot.job.site === 'dida-portal', `声明 dida-portal 的扩展领到 job: ${JSON.stringify(newExtGot.job)}`);
|
|
617
|
+
const outcome = await submitP;
|
|
618
|
+
assert.ok(outcome.ok && outcome.result.names?.includes('CN_M_DidaTravel'), `job 结果正确回传: ${JSON.stringify(outcome)}`);
|
|
619
|
+
// 正向:capability 匹配的 parked 轮询者即时收到新提交的 ctrip job
|
|
620
|
+
const ctripP = lane.submit({ kind: 'cookie-names', site: 'ctrip-flight' }, { timeoutMs: 3_000 });
|
|
621
|
+
await pause(100);
|
|
622
|
+
const parkedOld = await Promise.race([oldExtP, pause(500).then(() => null)]);
|
|
623
|
+
assert.ok(parkedOld && parkedOld.job && parkedOld.job.site === 'ctrip-flight', `旧版扩展领走自己 capability 内的 job: ${JSON.stringify(parkedOld?.job)}`);
|
|
624
|
+
await ctripP.catch(() => ({ ok: false }));
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
await lane.close();
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
await check('双源合同:needs-extension → waiting_extension(no_spend_waiting_user,waiting-* 同族)', () => {
|
|
631
|
+
const e = evaluateDoubleSource({ session: waitingRecord('needs-extension') });
|
|
646
632
|
assert.equal(e.state, 'waiting_extension');
|
|
647
633
|
assert.equal(e.quota_disposition, 'no_spend_waiting_user');
|
|
648
634
|
assert.equal(e.retry_allowed, false);
|
|
649
635
|
});
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
});
|
|
636
|
+
// D-24 自适应文案(issue #117):按本地通道是否落位自动跳过开发者模式/本地通道指引
|
|
637
|
+
await check('needs-extension 自适应文案:本地已落位 → 双通道完整指引(含开发者模式)', () => {
|
|
638
|
+
const s = needsExtensionSummary({ localInstalled: true });
|
|
654
639
|
assert.match(s, /开发者模式「加载已解压的扩展程序」/, '本地通道在 → 保留开发者模式指引');
|
|
655
640
|
assert.ok(s.includes(EXTENSION_STORE_URL), '商店 URL 仍在');
|
|
656
641
|
});
|
|
657
|
-
await check('needs-extension 自适应文案:本地未落位(商店版用户/未装)→ 自动跳过 dev-mode 文案', ()=>{
|
|
658
|
-
const s = needsExtensionSummary({
|
|
659
|
-
localInstalled: false
|
|
660
|
-
});
|
|
642
|
+
await check('needs-extension 自适应文案:本地未落位(商店版用户/未装)→ 自动跳过 dev-mode 文案', () => {
|
|
643
|
+
const s = needsExtensionSummary({ localInstalled: false });
|
|
661
644
|
assert.doesNotMatch(s, /开发者模式/, '不再推开发者模式/本地通道(商店版用户不该被指导 load unpacked)');
|
|
662
645
|
assert.doesNotMatch(s, /加载已解压/, '不再出现 load unpacked 字样');
|
|
663
646
|
assert.match(s, /已从 Chrome 商店安装/, '给「已装商店版?」提示(打开 Chrome 即可,无需再装)');
|
|
664
647
|
assert.match(s, /应用商店一键装/, '未装用户仍得商店一键装指引');
|
|
665
648
|
});
|
|
666
|
-
await check('localExtensionInstalled:HOME 注入可测(manifest 存在=本地通道在)', ()=>{
|
|
649
|
+
await check('localExtensionInstalled:HOME 注入可测(manifest 存在=本地通道在)', () => {
|
|
667
650
|
const fakeHome = mkdtempSync(join(tmpdir(), 'gotry-ext-home-'));
|
|
668
651
|
assert.equal(localExtensionInstalled(fakeHome), false, '空 HOME → 本地通道未落位');
|
|
669
|
-
mkdirSync(join(fakeHome, '.gotry', 'extension'), {
|
|
670
|
-
recursive: true
|
|
671
|
-
});
|
|
652
|
+
mkdirSync(join(fakeHome, '.gotry', 'extension'), { recursive: true });
|
|
672
653
|
writeFileSync(join(fakeHome, '.gotry', 'extension', 'manifest.json'), '{}', 'utf-8');
|
|
673
654
|
assert.equal(localExtensionInstalled(fakeHome), true, 'manifest 落位 → true');
|
|
674
|
-
rmSync(fakeHome, {
|
|
675
|
-
recursive: true,
|
|
676
|
-
force: true
|
|
677
|
-
});
|
|
655
|
+
rmSync(fakeHome, { recursive: true, force: true });
|
|
678
656
|
});
|
|
679
657
|
await b.close();
|
|
680
658
|
console.log(`\n§38 extension bridge: ${passed} 段全绿`);
|
|
681
659
|
}
|
|
682
|
-
main().catch((e)=>{
|
|
660
|
+
main().catch((e) => {
|
|
683
661
|
console.error('FAIL:', e);
|
|
662
|
+
// best-effort cleanup on early failure:关掉所有未释放的桥
|
|
684
663
|
try {
|
|
685
|
-
__resetSessionBridgeForTest().catch(()=>{});
|
|
686
|
-
}
|
|
664
|
+
__resetSessionBridgeForTest().catch(() => { });
|
|
665
|
+
}
|
|
666
|
+
catch { }
|
|
687
667
|
process.exit(1);
|
|
688
668
|
});
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
//# sourceURL=ts/scripts/extension-tests.ts
|