@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.19

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.
Files changed (137) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +236 -3
  4. package/bin/gotry-inner.js +66 -20
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +87 -2
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. package/ts/src/tool-budget.ts +0 -165
@@ -3,7 +3,9 @@ import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { classifyRequest, isSubmitText } from '../capabilities/session/read-guard.js';
5
5
  import { buildEntryUrl, parseBatchSearch } from '../capabilities/session/adapters/ctrip-flight.js';
6
- import { sessionFlightSearch, __resetRateLimiterForTest, classifyTransportFailure } from '../capabilities/session-search.js';
6
+ import { buildHotelEntryUrl, parseCtripHotelList, looksLikeHotelListBody } from '../capabilities/session/adapters/ctrip-hotel.js';
7
+ import { buildTrainEntryUrl, parseLeftTicketQuery, STATION_TELECODES } from '../capabilities/session/adapters/rail-12306.js';
8
+ import { sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, trainStationUnresolvedHint, hotelCityUnresolvedHint, __resetRateLimiterForTest, classifyTransportFailure } from '../capabilities/session-search.js';
7
9
  import { flyaiSearch } from '../capabilities/flyai.js';
8
10
  import { createConsentGate } from '../capabilities/session-consent.js';
9
11
  import { sessionLogin, pollTicketNames, LOGIN_TARGETS } from '../capabilities/session-login.js';
@@ -445,6 +447,404 @@ console.log('H. flyaiSearch hotel(飞猪官方 search-hotel)');
445
447
  } else {
446
448
  console.log(' SKIP - 登录 live 探针默认关(GOTRY_SESSION_LIVE=1 opt-in;工具面/桌面入口才真调)');
447
449
  }
450
+ }console.log('K. 酒店适配器(buildHotelEntryUrl/走形解析/形状嗅探/城市码指引/节律与日期闸)');
451
+ {
452
+ const h1 = buildHotelEntryUrl({
453
+ to: '迪拜',
454
+ cityId: 220,
455
+ checkIn: '2026-12-01',
456
+ checkOut: '2026-12-03',
457
+ adults: 2
458
+ });
459
+ assert(h1.ok && h1.url === 'https://hotels.ctrip.com/hotels/list?city=220&checkin=2026-12-01&checkout=2026-12-03&adult=2', 'cityId 显式覆盖 + 参数序', h1);
460
+ const h2 = buildHotelEntryUrl({
461
+ to: '上海'
462
+ });
463
+ assert(h2.ok && h2.url === 'https://hotels.ctrip.com/hotels/list?city=2', '码表内城市(上海=2,实测校准)', h2);
464
+ const h3 = buildHotelEntryUrl({
465
+ to: '不在码表的城市'
466
+ });
467
+ assert(!h3.ok && h3.unresolved?.includes('不在码表的城市') === true, '未收录城市 unresolved(不猜 id,不造数)', h3);
468
+ assert(/city=/.test(hotelCityUnresolvedHint([
469
+ '迪拜'
470
+ ])), '未收录指引带 cityId 发现路径(web 搜 list 页 URL)');
471
+ const hDubai = buildHotelEntryUrl({
472
+ to: '迪拜'
473
+ });
474
+ assert(hDubai.ok && hDubai.url.includes('city=220'), '迪拜=220(实测,轨迹里 agent 构造的 id 正确)', hDubai);
475
+ const hLijiang = buildHotelEntryUrl({
476
+ to: '丽江'
477
+ });
478
+ assert(hLijiang.ok && hLijiang.url.includes('city=37'), '丽江=37(实测)', hLijiang);
479
+ const realShape = JSON.stringify({
480
+ initListData: {
481
+ hotelList: [
482
+ {
483
+ hotelInfo: {
484
+ summary: {
485
+ hotelId: '3732301',
486
+ nameInfo: {
487
+ name: 'Ibis Styles Dubai Jumeira',
488
+ names: [
489
+ 'Ibis Styles Dubai Jumeira'
490
+ ]
491
+ },
492
+ hotelStar: {
493
+ star: 3
494
+ },
495
+ positionInfo: {
496
+ address: 'Al Mina Road - Jumeirah 1'
497
+ }
498
+ },
499
+ commentInfo: {
500
+ commentScore: '4.4'
501
+ },
502
+ roomInfo: [
503
+ {
504
+ priceInfo: {
505
+ price: 520
506
+ }
507
+ },
508
+ {
509
+ priceInfo: {
510
+ price: 468,
511
+ displayPrice: '¥468起'
512
+ }
513
+ }
514
+ ]
515
+ }
516
+ }
517
+ ]
518
+ }
519
+ });
520
+ const realHotels = parseCtripHotelList(realShape);
521
+ assert(realHotels.length === 1, '真实形态(hotelInfo 包裹)解析命中', realHotels);
522
+ assert(realHotels[0].name === 'Ibis Styles Dubai Jumeira' && realHotels[0].hotelId === '3732301' && realHotels[0].star === 3 && realHotels[0].score === '4.4', '结构化字段(名/id/星/评分)', realHotels[0]);
523
+ assert(realHotels[0].price === 468, '多房型取最低价(roomInfo[].priceInfo.price 官方路径)', realHotels[0]);
524
+ assert(realHotels[0].jumpUrl === 'https://hotels.ctrip.com/hotel/3732301', '真实形态 jumpUrl 构造', realHotels[0]);
525
+ const encryptedShape = JSON.stringify({
526
+ hotelList: [
527
+ {
528
+ hotelInfo: {
529
+ summary: {
530
+ hotelId: '999',
531
+ nameInfo: {
532
+ names: [
533
+ 'Atlantis The Palm'
534
+ ]
535
+ },
536
+ hotelStar: {
537
+ star: 5
538
+ }
539
+ },
540
+ commentInfo: {
541
+ commentScore: '4.8'
542
+ },
543
+ roomInfo: [
544
+ {
545
+ priceInfo: {
546
+ priceToken: 'ENCRYPTED_TOKEN',
547
+ isHiddenPrice: false
548
+ }
549
+ }
550
+ ]
551
+ }
552
+ }
553
+ ]
554
+ });
555
+ const encHotels = parseCtripHotelList(encryptedShape);
556
+ assert(encHotels.length === 1, '加密价条目不丢弃(名/星/评分仍可呈现)', encHotels);
557
+ assert(encHotels[0].price === 0 && !encHotels[0].priceRaw, '加密价不伪造价格(价以 jumpUrl 落地页为准)', encHotels[0]);
558
+ const displayShape = JSON.stringify({
559
+ hotelList: [
560
+ {
561
+ hotelInfo: {
562
+ summary: {
563
+ hotelId: '888',
564
+ nameInfo: {
565
+ names: [
566
+ 'Rove Downtown'
567
+ ]
568
+ }
569
+ },
570
+ roomInfo: [
571
+ {
572
+ priceInfo: {
573
+ displayPrice: '¥7xx'
574
+ }
575
+ }
576
+ ]
577
+ }
578
+ }
579
+ ]
580
+ });
581
+ const dispHotels = parseCtripHotelList(displayShape);
582
+ assert(dispHotels.length === 1 && dispHotels[0].priceRaw === '¥7xx' && dispHotels[0].price === 0, 'displayPrice 字符串价原样保留(打码口径)', dispHotels[0]);
583
+ const fixture = JSON.stringify({
584
+ data: {
585
+ hotelList: [
586
+ {
587
+ hotelId: 442516,
588
+ hotelName: 'Dubai Marriott',
589
+ star: 5,
590
+ commentScore: 4.7,
591
+ position: {
592
+ address: 'Sheikh Zayed Rd'
593
+ },
594
+ priceInfo: {
595
+ avgPrice: 680,
596
+ total: 1360
597
+ }
598
+ },
599
+ {
600
+ hotelId: '999',
601
+ hotelName: 'Rove Downtown',
602
+ priceInfo: {
603
+ priceDisplay: '¥7xx'
604
+ }
605
+ }
606
+ ]
607
+ }
608
+ });
609
+ const hotels1 = parseCtripHotelList(fixture);
610
+ assert(hotels1.length === 2, '走形解析命中 hotelList 数组', hotels1);
611
+ assert(hotels1[0].name === 'Dubai Marriott' && hotels1[0].price === 680 && hotels1[0].star === 5 && hotels1[0].score === '4.7', '归一化字段(名/价/星/评分)', hotels1[0]);
612
+ assert(hotels1[0].jumpUrl === 'https://hotels.ctrip.com/hotel/442516', 'jumpUrl 由 hotelId 构造(预订由人完成)', hotels1[0]);
613
+ assert(hotels1[1].price === 0 && hotels1[1].priceRaw === '¥7xx', '打码价原样保留不数值化(不伪装真价)', hotels1[1]);
614
+ const hotels2 = parseCtripHotelList(JSON.stringify({
615
+ hotelMatchInfos: [
616
+ {
617
+ name: 'Atlantis',
618
+ price: 2100,
619
+ score: '4.8',
620
+ hotelId: '123'
621
+ }
622
+ ]
623
+ }));
624
+ assert(hotels2.length === 1 && hotels2[0].price === 2100, '扁平形态(顶层 price 数字)同样命中', hotels2);
625
+ assert(parseCtripHotelList('not json{').length === 0 && parseCtripHotelList('{"a":1}').length === 0, 'malformed/无签名 一律返空(不抛错)');
626
+ assert(looksLikeHotelListBody('{"data":{"hotelList":[]}}') === true, '签名命中 hotelList');
627
+ assert(looksLikeHotelListBody('{"data":{"userList":[]}}') === false, '无签名不转发(防无关响应误投)');
628
+ assert(looksLikeHotelListBody('x'.repeat(2_000_001)) === false, '超大响应体不嗅探(上限把关)');
629
+ const dPast = await sessionHotelSearch({
630
+ to: '上海',
631
+ checkIn: '2026-01-01',
632
+ checkOut: '2026-01-03'
633
+ });
634
+ assert(dPast.ok === false && dPast.verdict === 'error' && /不是未来合法区间/.test(dPast.error ?? ''), '过去入住日代码层拒绝,不发上游', dPast);
635
+ const dPair = await sessionHotelSearch({
636
+ to: '上海',
637
+ checkIn: '2026-12-01'
638
+ });
639
+ assert(dPair.ok === false && /成对/.test(dPair.error ?? ''), 'checkIn/checkOut 须成对', dPair);
640
+ __resetRateLimiterForTest();
641
+ const e1 = await sessionHotelSearch({
642
+ to: '不在码表的城市'
643
+ });
644
+ assert(e1.ok === false && e1.verdict === 'error' && /city=/.test(e1.error ?? ''), '未收录城市 → error + cityId 指引', e1);
645
+ const e2 = await sessionHotelSearch({
646
+ to: '上海'
647
+ });
648
+ assert(e2.ok === false && e2.verdict === 'cooldown', '节律闸:同站点 30s 内第二调 → cooldown(不发起导航)', e2);
649
+ }console.log('L. 火车适配器(buildTrainEntryUrl/parseLeftTicketQuery/电报码指引/节律闸)');
650
+ {
651
+ const t1 = buildTrainEntryUrl({
652
+ from: '上海',
653
+ to: '昆明',
654
+ date: '2026-12-01'
655
+ });
656
+ assert(t1.ok && t1.url.includes('fs=%E4%B8%8A%E6%B5%B7%2CSHH') && t1.url.includes('ts=%E6%98%86%E6%98%8E%2CKMM') && t1.url.includes('date=2026-12-01'), '城市电报码(fs=城市名,SHH 形态)', t1);
657
+ const t2 = buildTrainEntryUrl({
658
+ from: '丽江',
659
+ to: '大理',
660
+ date: '2026-12-01',
661
+ fromStationTelecode: 'EHM',
662
+ toStationTelecode: 'KDM'
663
+ });
664
+ assert(t2.ok && t2.url.includes('%2CEHM') && t2.url.includes('%2CKDM'), '显式电报码覆盖(码表外城市)', t2);
665
+ const t3 = buildTrainEntryUrl({
666
+ from: '敦煌',
667
+ to: '西双版纳',
668
+ date: '2026-12-01'
669
+ });
670
+ assert(!t3.ok && t3.unresolved?.includes('敦煌') === true, '码表外城市 unresolved(官方站表亦无城市组,不猜码)', t3);
671
+ assert(/fromStationTelecode/.test(trainStationUnresolvedHint([
672
+ '敦煌'
673
+ ])), '未收录指引带电报码发现路径(web 搜 kyfw 查询页 URL)');
674
+ {
675
+ const snap = JSON.parse(readFileSync(join(import.meta.dirname, '..', '..', 'data', 'stations-12306-verify.json'), 'utf-8'));
676
+ const names = Object.keys(snap.cities);
677
+ assert(names.length >= 129, `快照覆盖 ${names.length} 城`);
678
+ let drift = 0;
679
+ for (const [city, v] of Object.entries(snap.cities)){
680
+ if (STATION_TELECODES[city] !== v.telecode) {
681
+ drift += 1;
682
+ console.log(` FAIL-detail - 电报码漂移: ${city} 表=${STATION_TELECODES[city]} 快照=${v.telecode}`);
683
+ }
684
+ }
685
+ assert(drift === 0, `电报码表 129 城与官方快照零漂移(漂移 ${drift} 处)`);
686
+ assert(Object.keys(STATION_TELECODES).length === names.length, '表与快照城市集合一致(无表外多城)');
687
+ }
688
+ const t4 = buildTrainEntryUrl({
689
+ from: '南宁',
690
+ to: '丽江',
691
+ date: '2026-12-01'
692
+ });
693
+ assert(t4.ok && t4.url.includes('%2CNNZ'), '南宁=NNZ(官方站表校准,曾错 NIZ)且丽江已入表=LHM', t4);
694
+ const makeRow = (seats)=>{
695
+ const c = new Array(56).fill('');
696
+ c[2] = '24000000G1375';
697
+ c[3] = 'G1375';
698
+ c[4] = 'SHH';
699
+ c[5] = 'KMM';
700
+ c[6] = 'SHH';
701
+ c[7] = 'KMM';
702
+ c[8] = '07:35';
703
+ c[9] = '15:27';
704
+ c[10] = '07:52';
705
+ c[11] = 'Y';
706
+ c[12] = 'yp';
707
+ c[13] = '20261201';
708
+ c[14] = 'x';
709
+ c[15] = 'loc';
710
+ c[16] = '01';
711
+ c[17] = '02';
712
+ c[18] = 'Y';
713
+ c[19] = '0';
714
+ for (const [k, v] of Object.entries(seats))c[Number(k)] = v;
715
+ return c.join('|');
716
+ };
717
+ const entry = buildTrainEntryUrl({
718
+ from: '上海',
719
+ to: '昆明',
720
+ date: '2026-12-01'
721
+ });
722
+ assert(entry.ok, 'entry 构造成功');
723
+ const probeSeats = {};
724
+ for(let i = 20; i <= 33; i++)probeSeats[i] = String(i);
725
+ const probeRow = makeRow(probeSeats);
726
+ const probe = parseLeftTicketQuery(JSON.stringify({
727
+ data: {
728
+ result: [
729
+ probeRow
730
+ ],
731
+ map: {
732
+ SHH: '上海南',
733
+ KMM: '昆明'
734
+ }
735
+ }
736
+ }), entry.url ?? '');
737
+ assert(probe.length === 1, '探针行解析', probe);
738
+ const expectSeats = [
739
+ [
740
+ '其他(通勤)',
741
+ '20'
742
+ ],
743
+ [
744
+ '高级软卧',
745
+ '21'
746
+ ],
747
+ [
748
+ '其他',
749
+ '22'
750
+ ],
751
+ [
752
+ '软卧(一等卧)',
753
+ '23'
754
+ ],
755
+ [
756
+ '软座',
757
+ '24'
758
+ ],
759
+ [
760
+ '特等座',
761
+ '25'
762
+ ],
763
+ [
764
+ '无座',
765
+ '26'
766
+ ],
767
+ [
768
+ 'yp_b(上游席别)',
769
+ '27'
770
+ ],
771
+ [
772
+ '硬卧(二等卧)',
773
+ '28'
774
+ ],
775
+ [
776
+ '硬座',
777
+ '29'
778
+ ],
779
+ [
780
+ '二等座',
781
+ '30'
782
+ ],
783
+ [
784
+ '一等座',
785
+ '31'
786
+ ],
787
+ [
788
+ '商务座',
789
+ '32'
790
+ ],
791
+ [
792
+ '动卧',
793
+ '33'
794
+ ]
795
+ ];
796
+ for (const [label, idxVal] of expectSeats){
797
+ assert(probe[0].seats[label] === idxVal, `座位桶 ${label} 下标=${idxVal}(官方 cN)`, probe[0].seats);
798
+ }
799
+ assert(probe[0].fromStation === '上海南' && probe[0].toStation === '昆明', '站名= data.map[电报码](官方 cN 口径,非行内索引)', probe[0]);
800
+ const trains1 = parseLeftTicketQuery(JSON.stringify({
801
+ data: {
802
+ result: [
803
+ makeRow({
804
+ 25: '有',
805
+ 26: '有',
806
+ 28: '有',
807
+ 29: '有',
808
+ 30: '有',
809
+ 31: '有',
810
+ 32: '有'
811
+ })
812
+ ],
813
+ map: {
814
+ SHH: '上海南',
815
+ KMM: '昆明'
816
+ }
817
+ }
818
+ }), entry.url ?? '');
819
+ assert(trains1.length === 1, '管道行解析命中', trains1);
820
+ assert(trains1[0].trainCode === 'G1375' && trains1[0].depTime === '07:35' && trains1[0].arrTime === '15:27' && trains1[0].durationMin === 472, '车次/时刻/历时归一化', trains1[0]);
821
+ assert(trains1[0].seats['商务座'] === '有' && trains1[0].seats['二等座'] === '有' && trains1[0].seats['特等座'] === '有' && trains1[0].seats['硬卧(二等卧)'] === '有', '余票分桶(官方索引)', trains1[0].seats);
822
+ assert(!('price' in trains1[0]), '列表接口无票价——不伪装价格(诚实面)');
823
+ const shifted = makeRow({
824
+ 25: '有'
825
+ }).split('|').slice(1).join('|');
826
+ assert(parseLeftTicketQuery(JSON.stringify({
827
+ data: {
828
+ result: [
829
+ shifted
830
+ ],
831
+ map: {}
832
+ }
833
+ }), entry.url ?? '').length === 0, '索引漂移行(签名失配)整行跳过,fail-visible');
834
+ assert(parseLeftTicketQuery('not json', entry.url ?? '').length === 0 && parseLeftTicketQuery('{"data":{"result":[]}}', entry.url ?? '').length === 0, 'malformed/空 一律返空(不抛错)');
835
+ __resetRateLimiterForTest();
836
+ const e1 = await sessionTrainSearch({
837
+ from: '不在码表',
838
+ to: '也不在',
839
+ date: '2026-12-01'
840
+ });
841
+ assert(e1.ok === false && e1.verdict === 'error' && /fromStationTelecode/.test(e1.error ?? ''), '未收录电报码 → error + 发现路径指引', e1);
842
+ const e2 = await sessionTrainSearch({
843
+ from: '上海',
844
+ to: '昆明',
845
+ date: '2026-12-01'
846
+ });
847
+ assert(e2.ok === false && e2.verdict === 'cooldown', '节律闸:同站点 30s 内第二调 → cooldown(不发起导航)', e2);
448
848
  }if (process.env.GOTRY_SESSION_TEST_FORCE_FAILURE === '1') {
449
849
  assert(false, 'forced session failure propagation proof');
450
850
  }