@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.17
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 +51 -13
- package/README.zh-CN.md +26 -12
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +357 -8
- package/bin/gotry-inner.js +442 -60
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +15 -1
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +358 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +147 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +83 -1
- package/dist/capabilities/session-search.js +111 -2
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +59 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +573 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +67 -4
- package/dist/scripts/nightly-evidence.js +59 -3
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +186 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +154 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +234 -60
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +50 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +291 -12
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +377 -0
- package/ts/capabilities/session/extension-channel.ts +95 -0
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-login.ts +63 -2
- package/ts/capabilities/session-search.ts +105 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -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 +557 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +161 -14
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { fork } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
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, __resetSessionBridgeForTest, __setSessionBridgeForTest } from '../capabilities/session/extension-bridge.js';
|
|
9
|
+
import { classifyBridgeFailure, extensionCookieNames, extensionSearchJob } from '../capabilities/session/extension-channel.js';
|
|
10
|
+
import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch } from '../capabilities/session-search.js';
|
|
11
|
+
import { sessionLogin } from '../capabilities/session-login.js';
|
|
12
|
+
import { LOGIN_COOKIE_NAMES, NETWORK_HINTS, SITE_DOMAIN } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
13
|
+
import { evaluateDoubleSource } from '../capabilities/session/benchmark.js';
|
|
14
|
+
const EXT_DIR = fileURLToPath(new URL('../../extension/', import.meta.url));
|
|
15
|
+
const UNREF_CHILD = fileURLToPath(new URL('./fixtures/extension-bridge-unref-child.mjs', import.meta.url));
|
|
16
|
+
const read = (f)=>readFileSync(join(EXT_DIR, f), 'utf8');
|
|
17
|
+
let passed = 0;
|
|
18
|
+
async function check(label, assertion) {
|
|
19
|
+
await assertion();
|
|
20
|
+
passed += 1;
|
|
21
|
+
console.log(` ok - ${label}`);
|
|
22
|
+
}
|
|
23
|
+
const mustBridge = async (ports)=>{
|
|
24
|
+
const created = await createSessionBridge({
|
|
25
|
+
ports
|
|
26
|
+
});
|
|
27
|
+
if (!created.ok) throw new Error(created.summary);
|
|
28
|
+
return created.bridge;
|
|
29
|
+
};
|
|
30
|
+
async function claimOnce(port, respond, signal) {
|
|
31
|
+
const r = await fetch(`http://127.0.0.1:${port}/jobs`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'content-type': 'application/json',
|
|
35
|
+
origin: EXTENSION_ORIGIN
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
extensionVersion: 'test'
|
|
39
|
+
}),
|
|
40
|
+
signal
|
|
41
|
+
});
|
|
42
|
+
const data = await r.json();
|
|
43
|
+
if (data.job && respond) {
|
|
44
|
+
const result = respond(data.job);
|
|
45
|
+
if (result) {
|
|
46
|
+
await fetch(`http://127.0.0.1:${port}/results/${encodeURIComponent(data.job.jobId)}`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'content-type': 'application/json',
|
|
50
|
+
origin: EXTENSION_ORIGIN
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(result)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
async function heartbeat(port) {
|
|
59
|
+
await fetch(`http://127.0.0.1:${port}/health`, {
|
|
60
|
+
headers: {
|
|
61
|
+
origin: EXTENSION_ORIGIN
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function waitForParkedCount(port, expected, timeoutMs = 1_000) {
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
while(Date.now() < deadline){
|
|
68
|
+
const r = await fetch(`http://127.0.0.1:${port}/status`);
|
|
69
|
+
const body = await r.json();
|
|
70
|
+
if (body.parked === expected) return;
|
|
71
|
+
await new Promise((resolve)=>setTimeout(resolve, 10));
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`/status 未在 ${timeoutMs}ms 内报告 parked=${expected}`);
|
|
74
|
+
}
|
|
75
|
+
async function assertParkedClientDoesNotPinHost() {
|
|
76
|
+
const child = fork(UNREF_CHILD, [], {
|
|
77
|
+
execArgv: process.execArgv,
|
|
78
|
+
stdio: [
|
|
79
|
+
'ignore',
|
|
80
|
+
'ignore',
|
|
81
|
+
'pipe',
|
|
82
|
+
'ipc'
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
let stderr = '';
|
|
86
|
+
child.stderr?.on('data', (chunk)=>{
|
|
87
|
+
stderr += chunk.toString('utf8');
|
|
88
|
+
});
|
|
89
|
+
let parkedRequest = null;
|
|
90
|
+
try {
|
|
91
|
+
const port = await new Promise((resolve, reject)=>{
|
|
92
|
+
const timer = setTimeout(()=>reject(new Error('子进程扩展桥未在 2s 内监听')), 2_000);
|
|
93
|
+
child.once('message', (message)=>{
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
const data = message;
|
|
96
|
+
if (data.kind === 'listening' && typeof data.port === 'number') resolve(data.port);
|
|
97
|
+
else reject(new Error(data.summary ?? `子进程消息异常:${JSON.stringify(data)}`));
|
|
98
|
+
});
|
|
99
|
+
child.once('exit', (code, signal)=>{
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
reject(new Error(`子进程过早退出 code=${code} signal=${signal} ${stderr}`));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
parkedRequest = claimOnce(port, null).catch(()=>({
|
|
105
|
+
job: null
|
|
106
|
+
}));
|
|
107
|
+
await waitForParkedCount(port, 1);
|
|
108
|
+
const exitP = new Promise((resolve, reject)=>{
|
|
109
|
+
const timer = setTimeout(()=>{
|
|
110
|
+
child.kill();
|
|
111
|
+
reject(new Error('默认桥被 parked /jobs 长轮询钉住超过 1500ms'));
|
|
112
|
+
}, 1_500);
|
|
113
|
+
child.once('exit', (code, signal)=>{
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
resolve({
|
|
116
|
+
code,
|
|
117
|
+
signal
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
child.send({
|
|
122
|
+
kind: 'release'
|
|
123
|
+
});
|
|
124
|
+
const exited = await exitP;
|
|
125
|
+
assert.equal(exited.code, 0, `子进程应自然退出:${stderr}`);
|
|
126
|
+
assert.equal(exited.signal, null);
|
|
127
|
+
} finally{
|
|
128
|
+
if (child.exitCode === null && child.signalCode === null) child.kill();
|
|
129
|
+
if (parkedRequest) await parkedRequest;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const stripRe = (s)=>s.replace(/\\/g, '');
|
|
133
|
+
async function main() {
|
|
134
|
+
console.log('§38 扩展传输车道合同(extension bridge,全离线)');
|
|
135
|
+
const manifest = JSON.parse(read('manifest.json'));
|
|
136
|
+
const derivedId = createHash('sha256').update(Buffer.from(manifest.key, 'base64')).digest().subarray(0, 16).toString('hex').replace(/[0-9a-f]/g, (c)=>'abcdefghijklmnop'[Number.parseInt(c, 16)]);
|
|
137
|
+
await check('manifest key 派生扩展 ID = Node 侧固定 EXTENSION_ID(unpacked 通道 origin 白名单锚点)', ()=>{
|
|
138
|
+
assert.equal(derivedId, EXTENSION_ID);
|
|
139
|
+
assert.equal(EXTENSION_ORIGIN, `chrome-extension://${EXTENSION_ID}`);
|
|
140
|
+
});
|
|
141
|
+
await check('双通道白名单:商店版 ID(CWS 重签,不认 manifest key)与 unpacked 固定 ID 同信,商店 URL 锚定 item ID', ()=>{
|
|
142
|
+
assert.equal(EXTENSION_ORIGIN_STORE, `chrome-extension://${EXTENSION_ID_STORE}`);
|
|
143
|
+
assert.ok(EXTENSION_ORIGINS.includes(EXTENSION_ORIGIN));
|
|
144
|
+
assert.ok(EXTENSION_ORIGINS.includes(EXTENSION_ORIGIN_STORE));
|
|
145
|
+
assert.equal(EXTENSION_ORIGINS.length, 2);
|
|
146
|
+
assert.ok(EXTENSION_STORE_URL.includes(EXTENSION_ID_STORE));
|
|
147
|
+
});
|
|
148
|
+
await check('manifest 合同:MV3 + 最小权限(cookies/alarms;无 debugger/tabs)', ()=>{
|
|
149
|
+
assert.equal(manifest.manifest_version, 3);
|
|
150
|
+
assert.deepEqual([
|
|
151
|
+
...manifest.permissions
|
|
152
|
+
].sort(), [
|
|
153
|
+
'alarms',
|
|
154
|
+
'cookies'
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
157
|
+
const manifestPorts = manifest.host_permissions.map((p)=>/127\.0\.0\.1:(\d+)/.exec(p)?.[1]).filter((v)=>v != null).map(Number).sort((a, b)=>a - b);
|
|
158
|
+
await check('防漂移:桥端口池(Node BRIDGE_PORTS)= manifest host_permissions 回环面(+ ctrip 星域)', ()=>{
|
|
159
|
+
assert.deepEqual(manifestPorts, [
|
|
160
|
+
...BRIDGE_PORTS
|
|
161
|
+
].sort((a, b)=>a - b));
|
|
162
|
+
assert.ok(manifest.host_permissions.includes('https://*.ctrip.com/*'));
|
|
163
|
+
});
|
|
164
|
+
await check('防漂移:content_scripts 双 world 只挂 flights.ctrip.com(MAIN 嗅探 + ISOLATED 桥)', ()=>{
|
|
165
|
+
assert.equal(manifest.content_scripts.length, 2);
|
|
166
|
+
for (const cs of manifest.content_scripts){
|
|
167
|
+
assert.deepEqual(cs.matches, [
|
|
168
|
+
'https://flights.ctrip.com/*'
|
|
169
|
+
]);
|
|
170
|
+
assert.equal(cs.run_at, 'document_start');
|
|
171
|
+
}
|
|
172
|
+
const worlds = manifest.content_scripts.map((cs)=>cs.world ?? 'ISOLATED').sort();
|
|
173
|
+
assert.deepEqual(worlds, [
|
|
174
|
+
'ISOLATED',
|
|
175
|
+
'MAIN'
|
|
176
|
+
]);
|
|
177
|
+
assert.deepEqual(manifest.content_scripts.map((cs)=>cs.js[0]).sort(), [
|
|
178
|
+
'content-bridge.js',
|
|
179
|
+
'content-main.js'
|
|
180
|
+
]);
|
|
181
|
+
});
|
|
182
|
+
const backgroundJs = read('background.js');
|
|
183
|
+
const contentMainJs = read('content-main.js');
|
|
184
|
+
const contentBridgeJs = read('content-bridge.js');
|
|
185
|
+
await check('防漂移:票据 cookie 名单(Node LOGIN_COOKIE_NAMES)= 扩展 SITES.ticketNames', ()=>{
|
|
186
|
+
for (const name of LOGIN_COOKIE_NAMES){
|
|
187
|
+
assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺票据名 ${name}`);
|
|
188
|
+
}
|
|
189
|
+
assert.ok(backgroundJs.includes("domain: 'ctrip.com'"), 'background.js 站点域应与 LOGIN_TARGETS.domain 对账');
|
|
190
|
+
});
|
|
191
|
+
await check('防漂移:NETWORK_HINTS(Node)= content-main HINT_RE(MAIN-world 嗅探面)', ()=>{
|
|
192
|
+
for (const hint of NETWORK_HINTS){
|
|
193
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 hint ${hint.source}`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
await check('防漂移:检索/登录 URL 面(background 只允许 flights.ctrip.com;ISOLATED 桥转发两类事件)', ()=>{
|
|
197
|
+
assert.ok(backgroundJs.includes('https://flights.ctrip.com/'));
|
|
198
|
+
assert.ok(contentMainJs.includes('gotry-ctrip-sniff'));
|
|
199
|
+
assert.ok(contentBridgeJs.includes('gotry-ctrip-sniff'));
|
|
200
|
+
assert.ok(contentBridgeJs.includes('gotry-page'));
|
|
201
|
+
});
|
|
202
|
+
await check('物理只读形态:扩展全部 fetch 只指向桥回环端口;不用 chrome.debugger', ()=>{
|
|
203
|
+
for (const [name, src] of [
|
|
204
|
+
[
|
|
205
|
+
'background',
|
|
206
|
+
backgroundJs
|
|
207
|
+
],
|
|
208
|
+
[
|
|
209
|
+
'content-main',
|
|
210
|
+
contentMainJs
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
'content-bridge',
|
|
214
|
+
contentBridgeJs
|
|
215
|
+
]
|
|
216
|
+
]){
|
|
217
|
+
const loopbackFetches = src.match(/fetch\(`?http:\/\/127\.0\.0\.1/g) ?? [];
|
|
218
|
+
const allFetches = src.match(/fetch\(/g) ?? [];
|
|
219
|
+
assert.equal(loopbackFetches.length, allFetches.length, `${name}: fetch 必须只指向桥回环端口(扩展零写行为的代码面证据)`);
|
|
220
|
+
}
|
|
221
|
+
assert.ok(!backgroundJs.includes('chrome.debugger'), '扩展不得使用 chrome.debugger(警告条/调试面)');
|
|
222
|
+
});
|
|
223
|
+
const b = await mustBridge([
|
|
224
|
+
0
|
|
225
|
+
]);
|
|
226
|
+
const port = b.port;
|
|
227
|
+
await check('桥只绑回环临时端口;/health 无 Origin 可诊断', async ()=>{
|
|
228
|
+
assert.ok(port > 0);
|
|
229
|
+
const r = await fetch(`http://127.0.0.1:${port}/health`);
|
|
230
|
+
const body = await r.json();
|
|
231
|
+
assert.equal(r.status, 200);
|
|
232
|
+
assert.equal(body.ok, true);
|
|
233
|
+
assert.equal(body.service, 'gotry-session-bridge');
|
|
234
|
+
});
|
|
235
|
+
await check('默认桥:parked 扩展长轮询不得钉住宿主 CLI 进程', assertParkedClientDoesNotPinHost);
|
|
236
|
+
await check('origin 白名单:邪恶源(网页跨域必带)POST /jobs 与 /results 一律 403', async ()=>{
|
|
237
|
+
for (const path of [
|
|
238
|
+
'/jobs',
|
|
239
|
+
'/results/whatever'
|
|
240
|
+
]){
|
|
241
|
+
const r = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: {
|
|
244
|
+
'content-type': 'application/json',
|
|
245
|
+
origin: 'https://evil.example'
|
|
246
|
+
},
|
|
247
|
+
body: '{}'
|
|
248
|
+
});
|
|
249
|
+
assert.equal(r.status, 403, path);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
await check('origin 白名单:商店版扩展源(CWS 重签 ID,2026-09-02 上架)POST 放行且计入心跳', async ()=>{
|
|
253
|
+
const storeLane = await mustBridge([
|
|
254
|
+
0
|
|
255
|
+
]);
|
|
256
|
+
try {
|
|
257
|
+
assert.equal(storeLane.extensionConnected(), false);
|
|
258
|
+
const r = await fetch(`http://127.0.0.1:${storeLane.port}/results/whatever`, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
'content-type': 'application/json',
|
|
262
|
+
origin: EXTENSION_ORIGIN_STORE
|
|
263
|
+
},
|
|
264
|
+
body: '{}'
|
|
265
|
+
});
|
|
266
|
+
assert.equal(r.status, 200, '商店版扩展源不得吃 403(否则商店通道全断)');
|
|
267
|
+
assert.equal(storeLane.extensionConnected(), true, '白名单源的任何请求都刷新心跳');
|
|
268
|
+
} finally{
|
|
269
|
+
await storeLane.close();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
await check('心跳判定:初始未连接;合法请求(health/长轮询)后视为在线', async ()=>{
|
|
273
|
+
assert.equal(b.extensionConnected(), false);
|
|
274
|
+
await heartbeat(port);
|
|
275
|
+
assert.equal(b.extensionConnected(), true);
|
|
276
|
+
});
|
|
277
|
+
await check('提交-回包闭环:cookie-names job 由假扩展取活并回包,submit 原样收果', async ()=>{
|
|
278
|
+
const submitP = b.submit({
|
|
279
|
+
kind: 'cookie-names',
|
|
280
|
+
site: 'ctrip-flight',
|
|
281
|
+
timeoutMs: 5_000
|
|
282
|
+
}, {
|
|
283
|
+
timeoutMs: 5_000
|
|
284
|
+
});
|
|
285
|
+
const claimed = await claimOnce(port, (job)=>{
|
|
286
|
+
assert.equal(job.kind, 'cookie-names');
|
|
287
|
+
assert.equal(job.site, 'ctrip-flight');
|
|
288
|
+
return {
|
|
289
|
+
ok: true,
|
|
290
|
+
kind: 'cookie-names',
|
|
291
|
+
names: [
|
|
292
|
+
'cticket',
|
|
293
|
+
'uid'
|
|
294
|
+
]
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
assert.ok(claimed.job);
|
|
298
|
+
const outcome = await submitP;
|
|
299
|
+
assert.ok(outcome.ok);
|
|
300
|
+
assert.deepEqual(outcome.ok ? outcome.result.names : [], [
|
|
301
|
+
'cticket',
|
|
302
|
+
'uid'
|
|
303
|
+
]);
|
|
304
|
+
});
|
|
305
|
+
await check('cookie 红线:扩展代码只上报名(c.name 映射),值不进任何结果对象', ()=>{
|
|
306
|
+
assert.ok(backgroundJs.includes('.map((c) => c.name)'));
|
|
307
|
+
assert.ok(!/\.value/.test(backgroundJs), 'background.js 不得触碰 cookie.value');
|
|
308
|
+
});
|
|
309
|
+
await check('长轮询幂等:并发取活只有一个客户端领到 job;后到者空手(快速中止,不挂进程)', async ()=>{
|
|
310
|
+
const firstAc = new AbortController();
|
|
311
|
+
const secondAc = new AbortController();
|
|
312
|
+
const firstP = claimOnce(port, ()=>({
|
|
313
|
+
ok: true,
|
|
314
|
+
kind: 'cookie-names',
|
|
315
|
+
names: [
|
|
316
|
+
'cticket'
|
|
317
|
+
]
|
|
318
|
+
}), firstAc.signal).catch(()=>({
|
|
319
|
+
job: null
|
|
320
|
+
}));
|
|
321
|
+
let secondP = null;
|
|
322
|
+
try {
|
|
323
|
+
await waitForParkedCount(port, 1);
|
|
324
|
+
secondP = claimOnce(port, null, secondAc.signal).catch(()=>({
|
|
325
|
+
job: null
|
|
326
|
+
}));
|
|
327
|
+
await waitForParkedCount(port, 2);
|
|
328
|
+
const submitP = b.submit({
|
|
329
|
+
kind: 'cookie-names',
|
|
330
|
+
site: 'ctrip-flight',
|
|
331
|
+
timeoutMs: 5_000
|
|
332
|
+
}, {
|
|
333
|
+
timeoutMs: 5_000
|
|
334
|
+
});
|
|
335
|
+
const first = await firstP;
|
|
336
|
+
assert.ok(first.job, '先进入 parked 队列的取活者必须领到 job');
|
|
337
|
+
const outcome = await submitP;
|
|
338
|
+
assert.ok(outcome.ok);
|
|
339
|
+
} finally{
|
|
340
|
+
firstAc.abort();
|
|
341
|
+
secondAc.abort();
|
|
342
|
+
if (secondP) await secondP;
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
await check('超时语义:扩展在线但不回包 → timeout(降级 error,不伪装成功)', async ()=>{
|
|
346
|
+
const outcome = await b.submit({
|
|
347
|
+
kind: 'search',
|
|
348
|
+
site: 'ctrip-flight',
|
|
349
|
+
url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01',
|
|
350
|
+
timeoutMs: 300
|
|
351
|
+
}, {
|
|
352
|
+
timeoutMs: 300
|
|
353
|
+
});
|
|
354
|
+
assert.ok(!outcome.ok);
|
|
355
|
+
assert.equal(outcome.ok ? null : outcome.reason, 'timeout');
|
|
356
|
+
});
|
|
357
|
+
await check('扩展未连接:有界等待后 extension-not-connected(用户门 no-spend,不空耗)', async ()=>{
|
|
358
|
+
const fresh = await mustBridge([
|
|
359
|
+
0
|
|
360
|
+
]);
|
|
361
|
+
try {
|
|
362
|
+
const outcome = await fresh.submit({
|
|
363
|
+
kind: 'cookie-names',
|
|
364
|
+
site: 'ctrip-flight'
|
|
365
|
+
}, {
|
|
366
|
+
extensionWaitMs: 400,
|
|
367
|
+
timeoutMs: 2_000
|
|
368
|
+
});
|
|
369
|
+
assert.ok(!outcome.ok);
|
|
370
|
+
assert.equal(outcome.ok ? null : outcome.reason, 'extension-not-connected');
|
|
371
|
+
} finally{
|
|
372
|
+
await fresh.close();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
await check('close:桥关闭后排队任务以 bridge-unavailable 结算(零花费)', async ()=>{
|
|
376
|
+
const tmp = await mustBridge([
|
|
377
|
+
0
|
|
378
|
+
]);
|
|
379
|
+
await heartbeat(tmp.port);
|
|
380
|
+
const submitP = tmp.submit({
|
|
381
|
+
kind: 'cookie-names',
|
|
382
|
+
site: 'ctrip-flight',
|
|
383
|
+
timeoutMs: 30_000
|
|
384
|
+
}, {
|
|
385
|
+
timeoutMs: 30_000
|
|
386
|
+
});
|
|
387
|
+
await tmp.close();
|
|
388
|
+
const outcome = await submitP;
|
|
389
|
+
assert.ok(!outcome.ok);
|
|
390
|
+
assert.equal(outcome.ok ? null : outcome.reason, 'bridge-unavailable');
|
|
391
|
+
});
|
|
392
|
+
await check('classifyBridgeFailure:只有 extension-not-connected 是用户门(needs-extension),其余 error', ()=>{
|
|
393
|
+
assert.equal(classifyBridgeFailure('extension-not-connected'), 'needs-extension');
|
|
394
|
+
assert.equal(classifyBridgeFailure('bridge-unavailable'), 'error');
|
|
395
|
+
assert.equal(classifyBridgeFailure('timeout'), 'error');
|
|
396
|
+
assert.equal(classifyBridgeFailure('job-error'), 'error');
|
|
397
|
+
});
|
|
398
|
+
const savedTransportEnv = process.env.GOTRY_SESSION_TRANSPORT;
|
|
399
|
+
const restoreTransportEnv = ()=>{
|
|
400
|
+
if (savedTransportEnv === undefined) delete process.env.GOTRY_SESSION_TRANSPORT;
|
|
401
|
+
else process.env.GOTRY_SESSION_TRANSPORT = savedTransportEnv;
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
delete process.env.GOTRY_SESSION_TRANSPORT;
|
|
405
|
+
await check('resolveTransportMode:扩展默认;cdp 须显式 opt-in;profileDir=persistent(测试)', ()=>{
|
|
406
|
+
assert.equal(resolveTransportMode(), 'extension');
|
|
407
|
+
process.env.GOTRY_SESSION_TRANSPORT = 'cdp';
|
|
408
|
+
assert.equal(resolveTransportMode(), 'cdp');
|
|
409
|
+
assert.equal(resolveTransportMode('/tmp/isolated-profile'), 'persistent');
|
|
410
|
+
});
|
|
411
|
+
} finally{
|
|
412
|
+
restoreTransportEnv();
|
|
413
|
+
}
|
|
414
|
+
const auditDir = mkdtempSync(join(tmpdir(), 'gotry-ext-audit-'));
|
|
415
|
+
const auditPath = join(auditDir, 'session-incidents.jsonl');
|
|
416
|
+
appendExtensionAudit(auditPath, {
|
|
417
|
+
kind: 'extension-session-job',
|
|
418
|
+
site: 'ctrip-flight',
|
|
419
|
+
url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01',
|
|
420
|
+
jobId: 'search',
|
|
421
|
+
result: 'body 1024B'
|
|
422
|
+
});
|
|
423
|
+
await check('审计:扩展 job 落 session-incidents 同款 JSONL(kind 区分,url 截断)', ()=>{
|
|
424
|
+
const lines = readFileSync(auditPath, 'utf8').trim().split('\n');
|
|
425
|
+
assert.equal(lines.length, 1);
|
|
426
|
+
const entry = JSON.parse(lines[0]);
|
|
427
|
+
assert.equal(entry.kind, 'extension-session-job');
|
|
428
|
+
assert.equal(entry.site, 'ctrip-flight');
|
|
429
|
+
assert.ok(entry.url.length <= 400);
|
|
430
|
+
});
|
|
431
|
+
rmSync(auditDir, {
|
|
432
|
+
recursive: true,
|
|
433
|
+
force: true
|
|
434
|
+
});
|
|
435
|
+
await check('车道闭环(注入临时桥+假扩展):cookie-names 只吐名字;search job 回传正文;登录快路径零交互', async ()=>{
|
|
436
|
+
const lane = await mustBridge([
|
|
437
|
+
0
|
|
438
|
+
]);
|
|
439
|
+
__setSessionBridgeForTest(lane);
|
|
440
|
+
try {
|
|
441
|
+
let done = false;
|
|
442
|
+
const claimLoop = (async ()=>{
|
|
443
|
+
while(!done){
|
|
444
|
+
const ac = new AbortController();
|
|
445
|
+
const bail = setTimeout(()=>ac.abort(), 2_000);
|
|
446
|
+
try {
|
|
447
|
+
await claimOnce(lane.port, (job)=>{
|
|
448
|
+
if (job.kind === 'cookie-names') return {
|
|
449
|
+
ok: true,
|
|
450
|
+
kind: 'cookie-names',
|
|
451
|
+
names: [
|
|
452
|
+
'cticket'
|
|
453
|
+
]
|
|
454
|
+
};
|
|
455
|
+
if (job.kind === 'search') {
|
|
456
|
+
assert.ok(String(job.url).startsWith('https://flights.ctrip.com/'));
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
kind: 'search',
|
|
460
|
+
body: JSON.stringify({
|
|
461
|
+
data: {
|
|
462
|
+
flightItineraryList: []
|
|
463
|
+
}
|
|
464
|
+
}),
|
|
465
|
+
title: '机票列表'
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
error: `unexpected kind ${job.kind}`
|
|
471
|
+
};
|
|
472
|
+
}, ac.signal);
|
|
473
|
+
} catch {}
|
|
474
|
+
clearTimeout(bail);
|
|
475
|
+
}
|
|
476
|
+
})();
|
|
477
|
+
const cookies = await extensionCookieNames({
|
|
478
|
+
site: 'ctrip-flight',
|
|
479
|
+
domain: SITE_DOMAIN.replace(/^\./, ''),
|
|
480
|
+
ticketNames: LOGIN_COOKIE_NAMES
|
|
481
|
+
});
|
|
482
|
+
assert.ok(cookies.ok);
|
|
483
|
+
assert.deepEqual(cookies.ok ? cookies.tickets : [], [
|
|
484
|
+
'cticket'
|
|
485
|
+
]);
|
|
486
|
+
const search = await extensionSearchJob({
|
|
487
|
+
site: 'ctrip-flight',
|
|
488
|
+
url: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=2026-12-01',
|
|
489
|
+
timeoutMs: 3_000
|
|
490
|
+
});
|
|
491
|
+
assert.ok(search.ok);
|
|
492
|
+
assert.equal(search.ok ? search.timedOut : true, false);
|
|
493
|
+
assert.ok(search.ok ? search.body.includes('flightItineraryList') : false);
|
|
494
|
+
const login = await sessionLogin({
|
|
495
|
+
site: 'ctrip-flight'
|
|
496
|
+
});
|
|
497
|
+
assert.equal(login.verdict, 'logged-in');
|
|
498
|
+
assert.deepEqual(login.tickets, [
|
|
499
|
+
'cticket'
|
|
500
|
+
]);
|
|
501
|
+
assert.ok(login.evidence.includes('自动检测'));
|
|
502
|
+
done = true;
|
|
503
|
+
await claimLoop;
|
|
504
|
+
} finally{
|
|
505
|
+
await __resetSessionBridgeForTest();
|
|
506
|
+
__setSessionBridgeForTest(null);
|
|
507
|
+
await lane.close();
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
await check('全链 fail-closed:扩展车道桥端口池全占 → sessionFlightSearch verdict=error(环境故障,非用户门)', async ()=>{
|
|
511
|
+
const { createServer } = await import('node:http');
|
|
512
|
+
const blockers = [];
|
|
513
|
+
try {
|
|
514
|
+
for (const port of [
|
|
515
|
+
8791,
|
|
516
|
+
8792,
|
|
517
|
+
8793,
|
|
518
|
+
8794,
|
|
519
|
+
8795
|
|
520
|
+
]){
|
|
521
|
+
const s = createServer().listen(port, '127.0.0.1');
|
|
522
|
+
await new Promise((r)=>s.once('listening', ()=>r()));
|
|
523
|
+
blockers.push({
|
|
524
|
+
close: ()=>new Promise((r)=>s.close(()=>r()))
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
await __resetSessionBridgeForTest();
|
|
528
|
+
const r = await sessionFlightSearch({
|
|
529
|
+
from: '上海',
|
|
530
|
+
to: '丽江',
|
|
531
|
+
date: '2026-12-01'
|
|
532
|
+
});
|
|
533
|
+
assert.equal(r.verdict, 'error', `端口池全占应 error(环境故障),实 ${r.verdict}:${r.error}`);
|
|
534
|
+
assert.ok(r.error?.includes('端口池'), '错误面必须点明端口池环境问题');
|
|
535
|
+
assert.ok(!r.error?.includes('一次性安装'), 'error 路径不应诱导用户去装扩展');
|
|
536
|
+
} finally{
|
|
537
|
+
await __resetSessionBridgeForTest();
|
|
538
|
+
for (const b of blockers)await b.close();
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
const waitingRecord = (verdict)=>({
|
|
542
|
+
query_id: 'sf-01',
|
|
543
|
+
route_segments: [],
|
|
544
|
+
journey_type: 'direct',
|
|
545
|
+
currency: '',
|
|
546
|
+
price: 0,
|
|
547
|
+
source: '',
|
|
548
|
+
fetched_at: '',
|
|
549
|
+
verdict,
|
|
550
|
+
latency_ms: 0,
|
|
551
|
+
read_guard_blocked: 0
|
|
552
|
+
});
|
|
553
|
+
await check('双源合同:needs-extension → waiting_extension(no_spend_waiting_user,waiting-* 同族)', ()=>{
|
|
554
|
+
const e = evaluateDoubleSource({
|
|
555
|
+
session: waitingRecord('needs-extension')
|
|
556
|
+
});
|
|
557
|
+
assert.equal(e.state, 'waiting_extension');
|
|
558
|
+
assert.equal(e.quota_disposition, 'no_spend_waiting_user');
|
|
559
|
+
assert.equal(e.retry_allowed, false);
|
|
560
|
+
});
|
|
561
|
+
await b.close();
|
|
562
|
+
console.log(`\n§38 extension bridge: ${passed} 段全绿`);
|
|
563
|
+
}
|
|
564
|
+
main().catch((e)=>{
|
|
565
|
+
console.error('FAIL:', e);
|
|
566
|
+
try {
|
|
567
|
+
__resetSessionBridgeForTest().catch(()=>{});
|
|
568
|
+
} catch {}
|
|
569
|
+
process.exit(1);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
//# sourceURL=ts/scripts/extension-tests.ts
|