@danceiny/gotry 0.2.0-rc.31 → 0.2.0-rc.33
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +63 -53
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/client/client.js +66 -21
- package/dist/capabilities/doctor.js +7 -8
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/bridge-ledger.js +168 -67
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +148 -47
- package/dist/capabilities/session/gui-flight.js +949 -0
- package/dist/capabilities/session/gui-hotel.js +470 -0
- package/dist/capabilities/session-search.js +102 -14
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
- package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
- package/dist/scripts/bootstrap-tests.js +23 -6
- package/dist/scripts/bridge-ledger-tests.js +20 -18
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +19 -4
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +5 -0
- package/dist/scripts/gotry-web-client-tests.js +20 -1
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +1 -1
- package/dist/scripts/mutation-check.js +153 -0
- package/dist/scripts/nightly-evidence.js +4 -111
- package/dist/scripts/session-tests.js +211 -6
- package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
- package/dist/scripts/tenant-capacity-probe.js +263 -0
- package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
- package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
- package/dist/scripts/tenant-executor-binding-tests.js +203 -0
- package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
- package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
- package/dist/scripts/tenant-seam-quota-tests.js +383 -0
- package/dist/scripts/tenant-seam-test-kit.js +292 -0
- package/dist/scripts/tenant-state-copies-tests.js +607 -0
- package/dist/scripts/tenant-state-drill-tests.js +298 -0
- package/dist/scripts/tenant-state-drill-worker.js +164 -0
- package/dist/scripts/tenant-state-durable-conformance.js +101 -0
- package/dist/scripts/tenant-state-handoff-tests.js +854 -0
- package/dist/scripts/tenant-state-lease-conformance.js +142 -0
- package/dist/scripts/tenant-state-lease-worker.js +28 -0
- package/dist/scripts/tenant-state-router-tests.js +464 -0
- package/dist/scripts/tenant-state-test-kit.js +92 -0
- package/dist/scripts/tenant-state-tests.js +564 -0
- package/dist/src/backend/modules/booking-executor.js +61 -6
- package/dist/src/backend/modules/session-search.js +136 -39
- package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/dist/src/booking-surface/dsh-planner.js +150 -19
- package/dist/src/booking-surface/index.js +1 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
- package/dist/src/booking-surface/server.js +193 -80
- package/dist/src/booking-surface/startup.js +153 -0
- package/dist/src/booking-surface/tenancy.js +188 -0
- package/dist/src/gotry-web-api.js +64 -2
- package/dist/src/index.js +70 -18
- package/dist/src/llm-price-table.js +148 -0
- package/dist/src/tenant-seam/index.js +8 -0
- package/dist/src/tenant-seam/llm-broker.js +508 -0
- package/dist/src/tenant-seam/process-governor.js +281 -0
- package/dist/src/tenant-seam/quotas.js +360 -0
- package/dist/src/tenant-seam/seam.js +92 -0
- package/dist/src/tenant-state/durable-store.js +158 -0
- package/dist/src/tenant-state/epoch-copies.js +422 -0
- package/dist/src/tenant-state/errors.js +108 -0
- package/dist/src/tenant-state/ids.js +101 -0
- package/dist/src/tenant-state/index.js +11 -0
- package/dist/src/tenant-state/lease-store.js +203 -0
- package/dist/src/tenant-state/tenant-lease.js +150 -0
- package/dist/src/tenant-state/tenant-router.js +650 -0
- package/extension/manifest.json +2 -2
- package/package.json +5 -1
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/bridge-ledger.ts +192 -78
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +185 -67
- package/ts/capabilities/session/gui-flight.ts +1022 -0
- package/ts/capabilities/session/gui-hotel.ts +505 -0
- package/ts/capabilities/session-search.ts +130 -18
- package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/ts/src/booking-surface/dsh-planner.ts +169 -19
- package/ts/src/booking-surface/index.ts +1 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
- package/ts/src/booking-surface/server.ts +215 -84
- package/ts/src/booking-surface/startup.ts +155 -0
- package/ts/src/booking-surface/tenancy.ts +242 -0
- package/ts/src/gotry-web-api.ts +51 -2
- package/ts/src/index.ts +67 -19
- package/ts/src/llm-price-table.ts +180 -0
- package/ts/src/tenant-seam/index.ts +9 -0
- package/ts/src/tenant-seam/llm-broker.ts +520 -0
- package/ts/src/tenant-seam/process-governor.ts +343 -0
- package/ts/src/tenant-seam/quotas.ts +433 -0
- package/ts/src/tenant-seam/seam.ts +142 -0
- package/ts/src/tenant-state/durable-store.ts +171 -0
- package/ts/src/tenant-state/epoch-copies.ts +491 -0
- package/ts/src/tenant-state/errors.ts +128 -0
- package/ts/src/tenant-state/ids.ts +108 -0
- package/ts/src/tenant-state/index.ts +12 -0
- package/ts/src/tenant-state/lease-store.ts +266 -0
- package/ts/src/tenant-state/tenant-lease.ts +175 -0
- package/ts/src/tenant-state/tenant-router.ts +743 -0
|
@@ -31,6 +31,8 @@ import Database from 'better-sqlite3';
|
|
|
31
31
|
import { EXTENSION_ORIGIN, createBridgeJobQueue, } from '../capabilities/session/extension-bridge.js';
|
|
32
32
|
import { BRIDGE_SITE_COOLDOWN_MS, BridgeLedgerStore, bridgeLedgerDbPath, openBridgeLedgerStore, } from '../capabilities/session/bridge-ledger.js';
|
|
33
33
|
import { startSessionSearchModule } from '../src/backend/modules/session-search.js';
|
|
34
|
+
// 账本方法的组织参数必填(漏传是编译错误):本套件验证的是缺省的单车道,统一传 local
|
|
35
|
+
const LOCAL = 'local';
|
|
34
36
|
let passed = 0;
|
|
35
37
|
async function check(label, assertion) {
|
|
36
38
|
await assertion();
|
|
@@ -180,7 +182,7 @@ async function main() {
|
|
|
180
182
|
assert.equal(row?.claimed_by_client, CLAIM_CLIENT);
|
|
181
183
|
assert.equal(row?.attempts, 1);
|
|
182
184
|
assert.ok(row?.claimed_at !== null, 'claimed_at 落账');
|
|
183
|
-
const pacing = storeC.pacingOf('dida-portal');
|
|
185
|
+
const pacing = storeC.pacingOf(LOCAL, 'dida-portal');
|
|
184
186
|
assert.ok(pacing && pacing.cooldownUntil > Date.now(), '导航类作业领取即记节律冷却');
|
|
185
187
|
// 正常善果回包 → resolved + result 保留
|
|
186
188
|
const r = await postResult(server.port, polled.job.jobId, { ok: true, kind: 'search', body: '{"rates":[]}', title: 'ok' }, { clientId: CLAIM_CLIENT });
|
|
@@ -213,7 +215,7 @@ async function main() {
|
|
|
213
215
|
const row = storeC.getJob(polled.job.jobId);
|
|
214
216
|
assert.equal(row?.status, 'unresolved', '曾被领取的超时终态 = unresolved');
|
|
215
217
|
assert.ok(row?.result_json?.includes('timeout'), '超时结算留 timeout 备注');
|
|
216
|
-
const pacing = storeC.pacingOf('train-12306');
|
|
218
|
+
const pacing = storeC.pacingOf(LOCAL, 'train-12306');
|
|
217
219
|
assert.ok(pacing && pacing.cooldownUntil >= Date.now() + BRIDGE_SITE_COOLDOWN_MS - 2_000, '超时结算把站点冷却推满');
|
|
218
220
|
}
|
|
219
221
|
finally {
|
|
@@ -224,7 +226,7 @@ async function main() {
|
|
|
224
226
|
const server = await serveQueue(queueC);
|
|
225
227
|
try {
|
|
226
228
|
const pending = queueC.submit({ kind: 'search', site: 'ctrip-hotel', url: 'https://hotels.ctrip.com/hotels/list' }, { timeoutMs: 300, extensionWaitMs: 30_000 });
|
|
227
|
-
const pacingAtSubmit = storeC.pacingOf('ctrip-hotel');
|
|
229
|
+
const pacingAtSubmit = storeC.pacingOf(LOCAL, 'ctrip-hotel');
|
|
228
230
|
assert.ok(pacingAtSubmit && pacingAtSubmit.cooldownUntil > Date.now(), '导航类作业提交即记节律(与 sessionDidaSearch 内存闸同语义:提交即承诺一次导航)');
|
|
229
231
|
const outcome = await pending;
|
|
230
232
|
assert.equal(outcome.ok, false);
|
|
@@ -238,7 +240,7 @@ async function main() {
|
|
|
238
240
|
raw.close();
|
|
239
241
|
}
|
|
240
242
|
assert.equal(voided.length, 1, '未被领取的超时作业 = void');
|
|
241
|
-
const pacingAfter = storeC.pacingOf('ctrip-hotel');
|
|
243
|
+
const pacingAfter = storeC.pacingOf(LOCAL, 'ctrip-hotel');
|
|
242
244
|
assert.equal(pacingAfter?.cooldownUntil, pacingAtSubmit?.cooldownUntil, '从未被领取的超时结算不追加冷却(站点未被打)');
|
|
243
245
|
}
|
|
244
246
|
finally {
|
|
@@ -273,12 +275,12 @@ async function main() {
|
|
|
273
275
|
let server;
|
|
274
276
|
try {
|
|
275
277
|
// 直接落账模拟上一进程崩溃后的持久状态(不经过队列 close——close 会把未领取作业落 void)
|
|
276
|
-
storeE.insertQueuedJob({ jobId: 'rec-queued-fresh', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, t0 + 60_000);
|
|
277
|
-
storeE.insertQueuedJob({ jobId: 'rec-queued-stale', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, t0 - 1_000);
|
|
278
|
-
storeE.insertQueuedJob({ jobId: 'rec-claimed-live', kind: 'search', site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list' }, t0 + 60_000);
|
|
279
|
-
storeE.markClaimed('rec-claimed-live', { origin: EXTENSION_ORIGIN, clientId: LIVE_CLIENT });
|
|
280
|
-
storeE.insertQueuedJob({ jobId: 'rec-claimed-dead', kind: 'search', site: 'ctrip-hotel', url: 'https://hotels.ctrip.com/hotels/list' }, t0 - 1_000);
|
|
281
|
-
storeE.markClaimed('rec-claimed-dead', { origin: EXTENSION_ORIGIN });
|
|
278
|
+
storeE.insertQueuedJob(LOCAL, { jobId: 'rec-queued-fresh', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, t0 + 60_000);
|
|
279
|
+
storeE.insertQueuedJob(LOCAL, { jobId: 'rec-queued-stale', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, t0 - 1_000);
|
|
280
|
+
storeE.insertQueuedJob(LOCAL, { jobId: 'rec-claimed-live', kind: 'search', site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list' }, t0 + 60_000);
|
|
281
|
+
storeE.markClaimed(LOCAL, 'rec-claimed-live', { origin: EXTENSION_ORIGIN, clientId: LIVE_CLIENT });
|
|
282
|
+
storeE.insertQueuedJob(LOCAL, { jobId: 'rec-claimed-dead', kind: 'search', site: 'ctrip-hotel', url: 'https://hotels.ctrip.com/hotels/list' }, t0 - 1_000);
|
|
283
|
+
storeE.markClaimed(LOCAL, 'rec-claimed-dead', { origin: EXTENSION_ORIGIN });
|
|
282
284
|
q = createBridgeJobQueue({ store: storeE });
|
|
283
285
|
server = await serveQueue(q);
|
|
284
286
|
// 恢复先于任何轮询执行(等价挂载路径:模块启动即 recoverOnBoot)
|
|
@@ -289,7 +291,7 @@ async function main() {
|
|
|
289
291
|
assert.equal(summary.settledUnresolved, 1);
|
|
290
292
|
assert.equal(storeE.getJob('rec-queued-stale')?.status, 'void', '未被领取即过期 → void,不再派发(零花费)');
|
|
291
293
|
assert.equal(storeE.getJob('rec-claimed-dead')?.status, 'unresolved', '领取后超时 → unresolved');
|
|
292
|
-
assert.ok(storeE.pacingOf('ctrip-hotel') && storeE.pacingOf('ctrip-hotel').cooldownUntil > Date.now(), '超时结算记节律冷却');
|
|
294
|
+
assert.ok(storeE.pacingOf(LOCAL, 'ctrip-hotel') && storeE.pacingOf(LOCAL, 'ctrip-hotel').cooldownUntil > Date.now(), '超时结算记节律冷却');
|
|
293
295
|
// 重排作业可被新进程的扩展领取并善果落账
|
|
294
296
|
const polled = await pollJobs(server.port, { clientId: LIVE_CLIENT, capabilities: ['dida-portal'] });
|
|
295
297
|
assert.equal(polled.job?.jobId, 'rec-queued-fresh', '恢复的 queued 作业重排进队列');
|
|
@@ -314,13 +316,13 @@ async function main() {
|
|
|
314
316
|
const server = await serveQueue(queueC);
|
|
315
317
|
try {
|
|
316
318
|
const CLIENT = randomUUID();
|
|
317
|
-
const pacingBefore = storeC.pacingOf('dida-portal');
|
|
319
|
+
const pacingBefore = storeC.pacingOf(LOCAL, 'dida-portal');
|
|
318
320
|
const first = queueC.submit({ kind: 'cookie-names', site: 'dida-portal', timeoutMs: 3_000 }, { timeoutMs: 3_000 });
|
|
319
321
|
const polled = await pollJobs(server.port, { clientId: CLIENT, capabilities: ['dida-portal'] });
|
|
320
322
|
assert.ok(polled.job, 'cookie-names 作业应被领取');
|
|
321
323
|
await postResult(server.port, polled.job.jobId, { ok: true, kind: 'cookie-names', names: ['dida_session_sig'] }, { clientId: CLIENT });
|
|
322
324
|
assert.equal((await first).ok, true);
|
|
323
|
-
const row1 = storeC.getClient(CLIENT);
|
|
325
|
+
const row1 = storeC.getClient(LOCAL, CLIENT);
|
|
324
326
|
assert.ok(row1, '轮询即注册客户端');
|
|
325
327
|
assert.equal(row1?.origin, EXTENSION_ORIGIN);
|
|
326
328
|
assert.deepEqual(JSON.parse(row1?.capabilities_json ?? '[]'), ['dida-portal']);
|
|
@@ -332,11 +334,11 @@ async function main() {
|
|
|
332
334
|
assert.ok(polled2.job, '第二次轮询仍可领取');
|
|
333
335
|
await postResult(server.port, polled2.job.jobId, { ok: true, kind: 'cookie-names', names: [] }, { clientId: CLIENT });
|
|
334
336
|
assert.equal((await second).ok, true);
|
|
335
|
-
const row2 = storeC.getClient(CLIENT);
|
|
337
|
+
const row2 = storeC.getClient(LOCAL, CLIENT);
|
|
336
338
|
assert.deepEqual(JSON.parse(row2?.capabilities_json ?? '[]'), ['dida-portal', 'train-12306'], 'upsert 覆盖能力清单');
|
|
337
339
|
assert.ok((row2?.last_seen_at ?? 0) >= seen1, 'last_seen_at 单调刷新');
|
|
338
340
|
assert.deepEqual(JSON.parse(row2?.login_sites_json ?? '[]'), ['dida-portal'], 'upsert 不回退 login_sites');
|
|
339
|
-
const pacingAfter = storeC.pacingOf('dida-portal');
|
|
341
|
+
const pacingAfter = storeC.pacingOf(LOCAL, 'dida-portal');
|
|
340
342
|
assert.deepEqual(pacingAfter, pacingBefore, 'cookie-names 非导航作业,不推动节律');
|
|
341
343
|
}
|
|
342
344
|
finally {
|
|
@@ -352,8 +354,8 @@ async function main() {
|
|
|
352
354
|
try {
|
|
353
355
|
// 预置一条上一进程遗留的已超时 claimed 作业:模块启动即应结算 unresolved
|
|
354
356
|
const seed = openBridgeLedgerStore(rootG);
|
|
355
|
-
seed.insertQueuedJob({ jobId: 'mount-recover-dead', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, Date.now() - 1_000);
|
|
356
|
-
seed.markClaimed('mount-recover-dead', { origin: EXTENSION_ORIGIN });
|
|
357
|
+
seed.insertQueuedJob(LOCAL, { jobId: 'mount-recover-dead', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, Date.now() - 1_000);
|
|
358
|
+
seed.markClaimed(LOCAL, 'mount-recover-dead', { origin: EXTENSION_ORIGIN });
|
|
357
359
|
seed.close();
|
|
358
360
|
const mod = startSessionSearchModule({
|
|
359
361
|
apiKey: () => 'ledger-test-key',
|
|
@@ -369,7 +371,7 @@ async function main() {
|
|
|
369
371
|
// recoverOnBoot 已在模块启动路径跑过:遗留 claimed 超时作业被结算
|
|
370
372
|
assert.equal(probe.getJob('mount-recover-dead')?.status, 'unresolved', '启动即恢复结算遗留超时作业');
|
|
371
373
|
// 账本节律读判定:冷却内 429(带 retry-after)
|
|
372
|
-
probe.applyCooldown('dida-portal');
|
|
374
|
+
probe.applyCooldown(LOCAL, 'dida-portal');
|
|
373
375
|
const hot = await driveModuleSearch(mod, 'Bearer ledger-test-key');
|
|
374
376
|
assert.equal(hot.statusCode, 429, '账本冷却内检索被节律闸拒绝');
|
|
375
377
|
assert.ok(String(hot.headers['retry-after'] ?? '').length > 0, '429 携带 retry-after');
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Supplier metadata fixtures; no vendor requests, credentials or browser session. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseCtripHotelCities, resolveCtripHotelCity, parseCtripCityCatalog, parseCtripCitySuggestions, matchCtripCity, resolveCtripFlightCity } from '../capabilities/session/ctrip-city.js';
|
|
4
|
+
import { resolveFlightEntryUrl } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
5
|
+
const catalog = { ResponseStatus: { Ack: 'Success' }, responseHead: { errorCode: '0' }, recommendGroupList: [{ indexedCity: { cityList: [
|
|
6
|
+
{ id: 2, code: 'SHA', name: '上海', eName: 'Shanghai', airportlst: [{ code: 'PVG', name: '上海浦东国际机场' }] },
|
|
7
|
+
{ id: 58, code: 'HKG', name: '中国香港', nameEn: 'Hong Kong' },
|
|
8
|
+
] }, geographicalRegionList: [{ hotCityList: [{ cityList: [{ id: 220, code: 'DXB', name: '迪拜', eName: 'Dubai', specName: '迪拜(阿联酋)', sameSpecCity: 1 }] }] }] }] };
|
|
9
|
+
const cities = parseCtripCityCatalog(catalog);
|
|
10
|
+
for (const [name, code] of [['迪拜(阿联酋)', 'DXB'], ['DXB', 'DXB'], ['香港', 'HKG'], ['Hong Kong', 'HKG'], ['Shanghai', 'SHA'], ['上海浦东国际机场', 'PVG']]) {
|
|
11
|
+
const r = matchCtripCity(name, cities);
|
|
12
|
+
assert.ok(r.ok, name);
|
|
13
|
+
assert.equal(r.city.code, code);
|
|
14
|
+
}
|
|
15
|
+
assert.equal(matchCtripCity('Dubai', cities).ok, false, 'supplier-marked same-name cities require qualification');
|
|
16
|
+
assert.equal(matchCtripCity('迪', cities).ok, false, 'no prefix or nearby-city substitution');
|
|
17
|
+
assert.equal(matchCtripCity('同名城', [{ name: '同名城', code: 'AAA', aliases: ['同名城'], context: '甲国' }, { name: '同名城', code: 'BBB', aliases: ['同名城'], context: '乙国' }]).ok, false);
|
|
18
|
+
assert.throws(() => parseCtripCityCatalog({ ...catalog, responseHead: { errorCode: '1' } }));
|
|
19
|
+
const suggestions = { Data: [{ Type: 4, Name: '未缓存城市', Code: 'NEW', EName: 'New City', Country: '示例国', Nearby: [{ Type: 4, Name: '邻近城市', Code: 'NEA' }] }] };
|
|
20
|
+
assert.equal(parseCtripCitySuggestions(suggestions).length, 1);
|
|
21
|
+
let requests = [];
|
|
22
|
+
const fakeFetch = async (url) => {
|
|
23
|
+
requests.push(String(url));
|
|
24
|
+
return Response.json(requests.length === 1 ? catalog : suggestions);
|
|
25
|
+
};
|
|
26
|
+
const resolved = await resolveCtripFlightCity('未缓存城市', fakeFetch);
|
|
27
|
+
assert.ok(resolved.ok);
|
|
28
|
+
assert.equal(resolved.city.code, 'NEW');
|
|
29
|
+
assert.equal(requests.length, 2, 'catalog miss queries supplier autocomplete');
|
|
30
|
+
const blocked = await resolveCtripFlightCity('未知城市', async () => new Response('whaleguard block'));
|
|
31
|
+
assert.ok(!blocked.ok && blocked.reason === 'unavailable', 'challenge is unavailable, not unsupported/no inventory');
|
|
32
|
+
const oversized = await resolveCtripFlightCity('未知城市', async () => new Response('x'.repeat(512001)));
|
|
33
|
+
assert.ok(!oversized.ok && oversized.reason === 'unavailable');
|
|
34
|
+
const entry = await resolveFlightEntryUrl('迪拜(阿联酋)', '香港', '2026-12-01', {}, async (q) => matchCtripCity(q, cities));
|
|
35
|
+
assert.ok(entry.ok && entry.url?.includes('oneway-dxb-hkg'));
|
|
36
|
+
let lookups = 0;
|
|
37
|
+
assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', 'bad-date', {}, async () => { lookups++; throw Error('should not run'); })).ok);
|
|
38
|
+
assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', '2026-12-01', { fromCode: '../dxb' }, async () => { lookups++; throw Error('should not run'); })).ok);
|
|
39
|
+
assert.equal(lookups, 0);
|
|
40
|
+
console.log('Ctrip city metadata: international names/airports, ambiguity, autocomplete fallback, challenge/bounds and exact URL PASS');
|
|
41
|
+
const hotelRecord = (id, name, english, country) => ({ keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: String(id), displayTexts: [{ key: 'MAIN_TITLE', value: english }] } }, controlInfo: { regionInfo: { basicCityModel: { cityId: id }, displayCityModel: { cityName: name, countryName: country, destinationNameEN: english } } } });
|
|
42
|
+
const hotels = { ResponseStatus: { Ack: 'Success' }, data: { mainKeywordList: { keywords: [hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), hotelRecord(596578, '迪拜', 'Dubai', '印度')] } } };
|
|
43
|
+
const hotelFetch = async () => Response.json(hotels);
|
|
44
|
+
const ambiguous = await resolveCtripHotelCity('迪拜', undefined, hotelFetch);
|
|
45
|
+
assert.ok(!ambiguous.ok && ambiguous.reason === 'ambiguous');
|
|
46
|
+
assert.equal(ambiguous.candidates.length, 2);
|
|
47
|
+
const qualified = await resolveCtripHotelCity('迪拜', '阿联酋', hotelFetch);
|
|
48
|
+
assert.ok(qualified.ok && qualified.city.id === '220');
|
|
49
|
+
const rejected = await resolveCtripHotelCity('迪拜', undefined, async () => Response.json({ ...hotels, data: { ...hotels.data, htlSpiderActionErrorCode: 'captcha' } }));
|
|
50
|
+
assert.ok(!rejected.ok && rejected.reason === 'unavailable');
|
|
51
|
+
assert.throws(() => parseCtripHotelCities({ ...hotels, data: { mainKeywordList: { keywords: [{ ...hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: '58' } } }] } } }));
|
|
52
|
+
console.log('Hotel destination namespace: same-name city ambiguity, country confirmation, challenge and ID conflict PASS');
|
|
53
|
+
// Exercise the real registered schema and tool dispatch, using an isolated state root.
|
|
54
|
+
const { apply } = await import('../src/index.js');
|
|
55
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
56
|
+
const { tmpdir } = await import('node:os');
|
|
57
|
+
const { join } = await import('node:path');
|
|
58
|
+
const root = mkdtempSync(join(tmpdir(), 'gotry-city-tool-'));
|
|
59
|
+
const tools = [];
|
|
60
|
+
const effects = [];
|
|
61
|
+
try {
|
|
62
|
+
apply({ tools: { register: (t) => tools.push(t) }, systemPrompt: { variable: () => { } }, get: () => undefined, on: () => () => { } }, { stateRoot: root, timeoutMs: 1000, hbcliBin: 'unused', sessionAccess: 'allow' }, { effect: (async (fx) => { effects.push(fx); return { result: { ok: false, verdict: 'error', error: 'fixture only', evidence: '[fixture:city-tool]' }, trace: { effect: fx.effect, channel: 'browser', attempts: 1, backoffMs: 0, breaker: 'off', evidence: [] } }; }) });
|
|
63
|
+
const session = tools.find(t => t.name === 'gotry_session_search');
|
|
64
|
+
const hotel = tools.find(t => t.name === 'gotry_hotel_search');
|
|
65
|
+
const parameters = session.parameters?.properties ?? session.parameters;
|
|
66
|
+
assert.ok(parameters.country && parameters.fromCityCode && parameters.toCityCode, 'model must see disambiguation parameters on session search');
|
|
67
|
+
const hotelParameters = hotel.parameters?.properties ?? hotel.parameters;
|
|
68
|
+
assert.ok(!hotelParameters.country, 'HBCLI hotel tool must not advertise an ignored session parameter');
|
|
69
|
+
await session.execute({ kind: 'hotel', to: '迪拜', country: '阿联酋' }, {});
|
|
70
|
+
assert.equal(effects.at(-1).effect, 'SESSION_HOTEL_SEARCH');
|
|
71
|
+
assert.equal(effects.at(-1).params.country, '阿联酋');
|
|
72
|
+
await session.execute({ kind: 'flight', from: '迪拜', to: '香港', date: '2099-12-01', fromCityCode: 'DXB', toCityCode: 'HKG' }, {});
|
|
73
|
+
assert.equal(effects.at(-1).effect, 'SESSION_FLIGHT_SEARCH');
|
|
74
|
+
assert.equal(effects.at(-1).params.fromCityCode, 'DXB');
|
|
75
|
+
assert.equal(effects.at(-1).params.toCityCode, 'HKG');
|
|
76
|
+
console.log('Registered session tool: country/code schema and effect dispatch PASS');
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|