@danceiny/gotry 0.0.1-rc.17 → 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 +185 -148
  2. package/README.zh-CN.md +195 -154
  3. package/bin/gotry-bootstrap.js +246 -26
  4. package/bin/gotry-inner.js +68 -38
  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 +115 -44
  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
@@ -0,0 +1,121 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { runDoctorChecks, renderDoctorReportMd, nodeOk } from '../capabilities/doctor.js';
6
+ import { apply } from '../src/index.js';
7
+ const tmp = await mkdtemp(join(tmpdir(), 'gotry-doctor-test-'));
8
+ const emptyRepo = join(tmp, 'repo-empty');
9
+ const emptyHome = join(tmp, 'home-empty');
10
+ await mkdir(emptyRepo, {
11
+ recursive: true
12
+ });
13
+ await mkdir(emptyHome, {
14
+ recursive: true
15
+ });
16
+ const r1 = await runDoctorChecks({
17
+ repoRoot: emptyRepo,
18
+ homeDir: emptyHome,
19
+ env: {}
20
+ });
21
+ const byId = (id)=>r1.items.find((i)=>i.id === id);
22
+ assert.ok(byId('agent-reach'), 'agent-reach 项存在');
23
+ assert.equal(byId('agent-reach').status, 'missing', `空仓 agent-reach=missing,实际 ${byId('agent-reach').status}`);
24
+ assert.match(byId('agent-reach').fix ?? '', /npx gotry doctor --fix/, 'missing 项带精确补装指引');
25
+ assert.equal(byId('extension').status, 'missing', '空 home 扩展=missing');
26
+ assert.equal(byId('flyai').status, 'degraded', '无 key flyai=degraded(有共享试用,非缺失)');
27
+ assert.equal(byId('llm-key').status, 'ok', 'LLM key 恒 ok(让渡面)');
28
+ assert.equal(r1.ok, false, '有 missing 项时报告 ok=false');
29
+ assert.match(r1.summary, /待处理/, 'summary 指明待处理项');
30
+ console.log('1. 空 tmp 环境 → missing/degraded 分级正确,LLM key 让渡 OK');
31
+ const repoPartial = join(tmp, 'repo-partial');
32
+ await mkdir(join(repoPartial, '.venv/bin'), {
33
+ recursive: true
34
+ });
35
+ await writeFile(join(repoPartial, '.venv/bin/python'), '#!/bin/sh\n', {
36
+ mode: 0o755
37
+ });
38
+ const r2a = await runDoctorChecks({
39
+ repoRoot: repoPartial,
40
+ homeDir: emptyHome,
41
+ env: {}
42
+ });
43
+ assert.equal(r2a.items.find((i)=>i.id === 'agent-reach').status, 'degraded', '只 python → degraded(半可用显式区分)');
44
+ const repoFull = join(tmp, 'repo-full');
45
+ await mkdir(join(repoFull, '.venv/bin'), {
46
+ recursive: true
47
+ });
48
+ await writeFile(join(repoFull, '.venv/bin/python'), '#!/bin/sh\n', {
49
+ mode: 0o755
50
+ });
51
+ await writeFile(join(repoFull, '.venv/bin/agent-reach'), '#!/bin/sh\n', {
52
+ mode: 0o755
53
+ });
54
+ const r2b = await runDoctorChecks({
55
+ repoRoot: repoFull,
56
+ homeDir: emptyHome,
57
+ env: {}
58
+ });
59
+ assert.equal(r2b.items.find((i)=>i.id === 'agent-reach').status, 'ok', 'python+agent-reach 齐 → ok');
60
+ console.log('2. .venv 三态(missing/degraded/ok)分级 OK');
61
+ const r3 = await runDoctorChecks({
62
+ repoRoot: emptyRepo,
63
+ homeDir: emptyHome,
64
+ env: {
65
+ FLYAI_API_KEY: 'test-key'
66
+ }
67
+ });
68
+ assert.equal(r3.items.find((i)=>i.id === 'flyai').status, 'ok', '有正式 key → flyai=ok');
69
+ console.log('3. FLYAI_API_KEY 注入 → flyai ok OK');
70
+ assert.equal(nodeOk('22.14.0'), false, '22.14 不足');
71
+ assert.equal(nodeOk('22.15.0'), true, '22.15 达标');
72
+ assert.equal(nodeOk('23.0.0'), true, '23.x 达标');
73
+ console.log('4. nodeOk 边界 OK');
74
+ const md = renderDoctorReportMd(r1);
75
+ assert.match(md, /# GoTry 依赖体检报告/, '报告标题');
76
+ assert.match(md, /`npx gotry doctor --fix`/, '命令类 fix 加反引号');
77
+ assert.match(md, /flyai\.open\.fliggy\.com 控制台申请正式 key/, 'prose 类 fix 原样呈现');
78
+ assert.ok(!md.includes('`到 flyai'), 'prose 类 fix 不应整体包反引号');
79
+ assert.match(md, /LLM key/, '让渡面(LLM key)照常入表');
80
+ console.log('5. 报告渲染 OK');
81
+ const smokeRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-state-'));
82
+ const registered = [];
83
+ const ctx = {
84
+ tools: {
85
+ register: (t)=>registered.push(t)
86
+ },
87
+ systemPrompt: {
88
+ variable: ()=>{}
89
+ },
90
+ on: ()=>()=>{}
91
+ };
92
+ apply(ctx, {
93
+ stateRoot: smokeRoot,
94
+ timeoutMs: 30_000,
95
+ hbcliBin: 'hbcli-not-on-path',
96
+ sessionAccess: 'ask'
97
+ });
98
+ const doctorTool = registered.find((t)=>t.name === 'gotry_doctor');
99
+ assert.ok(doctorTool, 'gotry_doctor 已注册');
100
+ const out = await doctorTool.execute({
101
+ query: {}
102
+ }, null);
103
+ assert.equal(out.ok, true, '体检工具 ok=true(体检本身成功,与依赖是否缺失无关)');
104
+ assert.equal(out.verdict, 'needs-attention', '注入环境外仍能出 verdict(本机真实状态,不判 all-clear)');
105
+ assert.ok(Array.isArray(out.items) && out.items.length >= 6, 'items 齐全');
106
+ assert.ok(out.report_path && out.report_path.includes(smokeRoot), `报告落在 isolated stateRoot: ${out.report_path}`);
107
+ const reportMd = await readFile(out.report_path, 'utf-8');
108
+ assert.match(reportMd, /# GoTry 依赖体检报告/, '落盘报告可预览');
109
+ await rm(tmp, {
110
+ recursive: true,
111
+ force: true
112
+ });
113
+ await rm(smokeRoot, {
114
+ recursive: true,
115
+ force: true
116
+ });
117
+ console.log('6. gotry_doctor 工具面 + 报告落盘 OK');
118
+ console.log('doctor-tests: 全部通过');
119
+
120
+
121
+ //# sourceURL=ts/scripts/doctor-tests.ts
@@ -468,7 +468,280 @@ assert.equal(m10a.result.rates?.rooms?.length, 1, 'RATES 夹具回放');
468
468
  assert.equal(m10b.result.avail?.avail, true, 'CHECK_AVAIL 夹具回放');
469
469
  assert.match(m10a.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
470
470
  console.log('10c. mock 解译器夹具回放(预订链 CI 形态)OK');
471
- console.log('EFFECT INTERPRETER TESTS: 10/10 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应,纯离线)');
471
+ __resetEffectBreakersForTest();
472
+ let flakyCalls = 0;
473
+ const ipTimeout = makeProductionInterpreter({
474
+ sleep: sleep0,
475
+ breakers: new Map(),
476
+ handlers: {
477
+ HBCLI_HOTEL_SEARCH: async ()=>{
478
+ flakyCalls += 1;
479
+ if (flakyCalls === 1) return {
480
+ via: 'hbcli-error',
481
+ exitCode: -1,
482
+ result: null,
483
+ evidence: '[实时API:hbcli@timeout@ts]',
484
+ latencyMs: 1,
485
+ error: 'timeout after 30000ms'
486
+ };
487
+ return {
488
+ via: 'hbcli-realtime',
489
+ exitCode: 0,
490
+ result: {
491
+ hotels: []
492
+ },
493
+ evidence: '[实时API:hbcli@ts]',
494
+ latencyMs: 1
495
+ };
496
+ }
497
+ }
498
+ });
499
+ const itp11 = await ipTimeout({
500
+ effect: 'HBCLI_HOTEL_SEARCH',
501
+ params: {
502
+ destination: '迪拜'
503
+ }
504
+ });
505
+ assert.equal(itp11.trace.attempts, 2, `timeout 重试一次(attempts=2),实际 ${itp11.trace.attempts}`);
506
+ assert.equal(itp11.result?.via, 'hbcli-realtime', '重试后恢复实时');
507
+ assert.equal(itp11.trace.breaker, 'closed', '恢复即成功,断路不计数');
508
+ let hardCalls = 0;
509
+ const ipEnoent = makeProductionInterpreter({
510
+ sleep: sleep0,
511
+ breakers: new Map(),
512
+ handlers: {
513
+ HBCLI_HOTEL_SEARCH: async ()=>{
514
+ hardCalls += 1;
515
+ return {
516
+ via: 'hbcli-error',
517
+ exitCode: -1,
518
+ result: null,
519
+ evidence: '[实时API:hbcli@spawn_error@ts]',
520
+ latencyMs: 1,
521
+ error: 'spawn hbcli ENOENT'
522
+ };
523
+ }
524
+ }
525
+ });
526
+ const itp11b = await ipEnoent({
527
+ effect: 'HBCLI_HOTEL_SEARCH',
528
+ params: {
529
+ destination: '迪拜'
530
+ }
531
+ });
532
+ assert.equal(itp11b.trace.attempts, 1, 'ENOENT 类永不重试(切换不是重试契约不变)');
533
+ console.log('11. HBCLI×timeout 冷启动重试(timeout→attempts=2 恢复实时;ENOENT→attempts=1)OK');
534
+ __resetEffectBreakersForTest();
535
+ const sixChannels = [
536
+ [
537
+ 'ANYTHING_SEARCH',
538
+ 'cli',
539
+ {
540
+ ok: true,
541
+ via: 'hbcli-anything',
542
+ verdict: 'hit',
543
+ evidence: 'e',
544
+ latencyMs: 1
545
+ }
546
+ ],
547
+ [
548
+ 'WEB_READ',
549
+ 'api',
550
+ {
551
+ ok: true,
552
+ via: 'r.jina.ai',
553
+ evidence: 'e',
554
+ latencyMs: 1
555
+ }
556
+ ],
557
+ [
558
+ 'GITHUB_SEARCH',
559
+ 'cli',
560
+ {
561
+ ok: true,
562
+ verdict: 'found',
563
+ evidence: 'e',
564
+ latencyMs: 1
565
+ }
566
+ ],
567
+ [
568
+ 'VIDEO_SUBTITLE',
569
+ 'cli',
570
+ {
571
+ ok: true,
572
+ verdict: 'found',
573
+ evidence: 'e',
574
+ latencyMs: 1
575
+ }
576
+ ],
577
+ [
578
+ 'AGENT_REACH',
579
+ 'cli',
580
+ {
581
+ ok: true,
582
+ verdict: 'found',
583
+ channel: 'v2ex',
584
+ method: 'read',
585
+ evidence: 'e',
586
+ latencyMs: 1
587
+ }
588
+ ],
589
+ [
590
+ 'SESSION_LOGIN',
591
+ 'browser',
592
+ {
593
+ ok: true,
594
+ via: 'session-login',
595
+ verdict: 'pending',
596
+ evidence: 'e',
597
+ latencyMs: 1
598
+ }
599
+ ]
600
+ ];
601
+ for (const [fx, channel, obs] of sixChannels){
602
+ const ip = makeProductionInterpreter({
603
+ sleep: sleep0,
604
+ breakers: new Map(),
605
+ handlers: {
606
+ [fx]: async ()=>obs
607
+ }
608
+ });
609
+ const out = await ip({
610
+ effect: fx,
611
+ params: {}
612
+ });
613
+ assert.equal(out.result, obs, `${fx} 透传原样`);
614
+ assert.equal(out.trace.channel, channel, `${fx} trace.channel=${channel}(策略表行在案)`);
615
+ assert.equal(out.trace.attempts, 1, `${fx} 成功单次执行`);
616
+ }
617
+ console.log('12a. 六渠道入表(透传 + trace.channel 与策略表一致)OK');
618
+ for (const fx of [
619
+ 'AGENT_REACH',
620
+ 'SESSION_LOGIN'
621
+ ]){
622
+ const ip = makeProductionInterpreter({
623
+ sleep: sleep0,
624
+ breakers: new Map(),
625
+ handlers: {
626
+ [fx]: async ()=>{
627
+ throw new Error('upstream says no');
628
+ }
629
+ }
630
+ });
631
+ const out = await ip({
632
+ effect: fx,
633
+ params: {}
634
+ });
635
+ assert.equal(out.result, null, `${fx} 抛错 → result=null`);
636
+ assert.equal(out.trace.attempts, 1, `${fx} 永不重试(attempts=1)`);
637
+ }
638
+ console.log('12b. AGENT_REACH/SESSION_LOGIN 永不重试(反射桥透传/浏览器风控红线)OK');
639
+ __resetEffectBreakersForTest();
640
+ let webCalls = 0;
641
+ const ipWeb = makeProductionInterpreter({
642
+ sleep: sleep0,
643
+ breakers: new Map(),
644
+ handlers: {
645
+ WEB_READ: async ()=>{
646
+ webCalls += 1;
647
+ if (webCalls === 1) throw new Error('socket hang up');
648
+ return {
649
+ ok: true,
650
+ via: 'r.jina.ai',
651
+ evidence: 'e',
652
+ latencyMs: 1
653
+ };
654
+ }
655
+ }
656
+ });
657
+ const outWeb = await ipWeb({
658
+ effect: 'WEB_READ',
659
+ params: {
660
+ url: 'https://example.com'
661
+ }
662
+ });
663
+ assert.equal(outWeb.trace.attempts, 2, `WEB_READ 瞬时抖动重试 1 次,实际 ${outWeb.trace.attempts}`);
664
+ let ghTimeoutCalls = 0;
665
+ const ipGhTimeout = makeProductionInterpreter({
666
+ sleep: sleep0,
667
+ breakers: new Map(),
668
+ handlers: {
669
+ GITHUB_SEARCH: async ()=>{
670
+ ghTimeoutCalls += 1;
671
+ if (ghTimeoutCalls === 1) return {
672
+ ok: false,
673
+ verdict: 'timeout',
674
+ evidence: 'e',
675
+ latencyMs: 1,
676
+ stdout: '',
677
+ stderr: ''
678
+ };
679
+ return {
680
+ ok: true,
681
+ verdict: 'found',
682
+ evidence: 'e',
683
+ latencyMs: 1
684
+ };
685
+ }
686
+ }
687
+ });
688
+ const outGhT = await ipGhTimeout({
689
+ effect: 'GITHUB_SEARCH',
690
+ params: {
691
+ query: 'x'
692
+ }
693
+ });
694
+ assert.equal(outGhT.trace.attempts, 2, 'GITHUB timeout 类重试 1 次(冷启动同 HBCLI 实况)');
695
+ let ghNiCalls = 0;
696
+ const ipGhNi = makeProductionInterpreter({
697
+ sleep: sleep0,
698
+ breakers: new Map(),
699
+ handlers: {
700
+ GITHUB_SEARCH: async ()=>{
701
+ ghNiCalls += 1;
702
+ return {
703
+ ok: false,
704
+ verdict: 'not-installed',
705
+ evidence: 'e',
706
+ latencyMs: 0
707
+ };
708
+ }
709
+ }
710
+ });
711
+ const outGhNi = await ipGhNi({
712
+ effect: 'GITHUB_SEARCH',
713
+ params: {
714
+ query: 'x'
715
+ }
716
+ });
717
+ assert.equal(outGhNi.trace.attempts, 1, 'not-installed 是配置态(needs-setup 族)不重试');
718
+ console.log('12c. WEB_READ/GITHUB/VIDEO 策略行(timeout 重试/配置态不重试)OK');
719
+ __resetEffectBreakersForTest();
720
+ const mockIp = makeMockInterpreter({
721
+ ANYTHING_SEARCH: {
722
+ ok: true,
723
+ via: 'hbcli-anything',
724
+ verdict: 'hit',
725
+ evidence: '[实时API:hbcli-anything@ts]',
726
+ latencyMs: 1
727
+ }
728
+ });
729
+ const outMock = await mockIp({
730
+ effect: 'ANYTHING_SEARCH',
731
+ params: {
732
+ keyword: '大理'
733
+ }
734
+ });
735
+ assert.equal(outMock.result.verdict, 'hit', 'mock 夹具回放原样');
736
+ assert.equal(outMock.trace.channel, 'mock', 'mock 通道标注');
737
+ const outMockBad = await mockIp({
738
+ effect: 'SESSION_LOGIN',
739
+ params: {}
740
+ });
741
+ assert.equal(outMockBad.result, null, '未登记夹具 → result=null');
742
+ assert.equal(outMockBad.trace.declined, 'unknown-effect', '未登记夹具 → 结构化拒绝');
743
+ console.log('12d. mock 夹具回放/未登记拒绝(新效应同语义)OK');
744
+ console.log('EFFECT INTERPRETER TESTS: 12/12 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应/HBCLI timeout 重试/D-23 六渠道入表,纯离线)');
472
745
 
473
746
 
474
747
  //# sourceURL=ts/scripts/effect-tests.ts
@@ -1,15 +1,17 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { fork } from 'node:child_process';
3
3
  import { createHash } from 'node:crypto';
4
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
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';
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
9
  import { classifyBridgeFailure, extensionCookieNames, extensionSearchJob } from '../capabilities/session/extension-channel.js';
10
- import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch } from '../capabilities/session-search.js';
10
+ import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
11
11
  import { sessionLogin } from '../capabilities/session-login.js';
12
12
  import { LOGIN_COOKIE_NAMES, NETWORK_HINTS, SITE_DOMAIN } from '../capabilities/session/adapters/ctrip-flight.js';
13
+ import { HOTEL_NETWORK_HINTS, HOTEL_SITE_HOST, buildHotelEntryUrl } from '../capabilities/session/adapters/ctrip-hotel.js';
14
+ import { TRAIN_NETWORK_HINTS, TRAIN_SITE_HOST } from '../capabilities/session/adapters/rail-12306.js';
13
15
  import { evaluateDoubleSource } from '../capabilities/session/benchmark.js';
14
16
  const EXT_DIR = fileURLToPath(new URL('../../extension/', import.meta.url));
15
17
  const UNREF_CHILD = fileURLToPath(new URL('./fixtures/extension-bridge-unref-child.mjs', import.meta.url));
@@ -161,11 +163,14 @@ async function main() {
161
163
  ].sort((a, b)=>a - b));
162
164
  assert.ok(manifest.host_permissions.includes('https://*.ctrip.com/*'));
163
165
  });
164
- await check('防漂移:content_scripts 双 world 只挂 flights.ctrip.com(MAIN 嗅探 + ISOLATED 桥)', ()=>{
166
+ await check('防漂移:content_scripts 双 world 挂 ctrip 双站+12306(MAIN 嗅探 + ISOLATED 桥;2026-09-03 酒/火实装)', ()=>{
165
167
  assert.equal(manifest.content_scripts.length, 2);
166
168
  for (const cs of manifest.content_scripts){
167
169
  assert.deepEqual(cs.matches, [
168
- 'https://flights.ctrip.com/*'
170
+ 'https://flights.ctrip.com/*',
171
+ `https://${HOTEL_SITE_HOST}/*`,
172
+ `https://${TRAIN_SITE_HOST}/*`,
173
+ 'https://www.12306.cn/*'
169
174
  ]);
170
175
  assert.equal(cs.run_at, 'document_start');
171
176
  }
@@ -182,19 +187,42 @@ async function main() {
182
187
  const backgroundJs = read('background.js');
183
188
  const contentMainJs = read('content-main.js');
184
189
  const contentBridgeJs = read('content-bridge.js');
190
+ await check('版本跟随主版本:manifest version_name = package.json version;version = 四段投影(0.0.1-rc.N → 0.0.1.N,founder 2026-09-03 拍板)', ()=>{
191
+ const pkgVersion = JSON.parse(readFileSync(join(EXT_DIR, '..', 'package.json'), 'utf8')).version;
192
+ assert.equal(manifest.version_name, pkgVersion, 'version_name 应与 gotry 主版本逐字一致');
193
+ const m = /^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(pkgVersion);
194
+ assert.ok(m, `主版本形态应为 x.y.z-rc.N,实际 ${pkgVersion}`);
195
+ assert.equal(manifest.version, `${m[1]}.${m[2]}.${m[3]}.${m[4]}`, 'version 应为主版本的四段投影(Chrome manifest 只收点分整数)');
196
+ assert.equal(manifest.content_scripts.length, 2);
197
+ });
185
198
  await check('防漂移:票据 cookie 名单(Node LOGIN_COOKIE_NAMES)= 扩展 SITES.ticketNames', ()=>{
186
199
  for (const name of LOGIN_COOKIE_NAMES){
187
200
  assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺票据名 ${name}`);
188
201
  }
189
202
  assert.ok(backgroundJs.includes("domain: 'ctrip.com'"), 'background.js 站点域应与 LOGIN_TARGETS.domain 对账');
190
203
  });
191
- await check('防漂移:NETWORK_HINTS(Node)= content-main HINT_RE(MAIN-world 嗅探面)', ()=>{
204
+ await check('防漂移:NETWORK_HINTS + HOTEL_NETWORK_HINTS(Node)= content-main 嗅探面(MAIN-world)', ()=>{
192
205
  for (const hint of NETWORK_HINTS){
193
206
  assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 hint ${hint.source}`);
194
207
  }
208
+ for (const hint of HOTEL_NETWORK_HINTS){
209
+ assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺酒店 hint ${hint.source}`);
210
+ }
211
+ for (const hint of TRAIN_NETWORK_HINTS){
212
+ assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺火车 hint ${hint.source}`);
213
+ }
214
+ assert.ok(contentMainJs.includes('12306'), 'content-main.js 应感知 12306 页域(火车嗅探作用域)');
215
+ assert.ok(contentMainJs.includes('hotels\\.ctrip\\.com'), 'content-main.js 应感知酒店页域(形状嗅探兜底的作用域)');
216
+ });
217
+ await check('防漂移:酒店形状签名(Node looksLikeHotelListBody 同款)= content-main HOTEL_BODY_SIG_RE', ()=>{
218
+ assert.ok(stripRe(contentMainJs).includes('"hotelList"|"hotelMatchInfos"|"hotelName"'), '形状嗅探签名两侧必须逐字一致');
195
219
  });
196
- await check('防漂移:检索/登录 URL 面(background 只允许 flights.ctrip.com;ISOLATED 桥转发两类事件)', ()=>{
197
- assert.ok(backgroundJs.includes('https://flights.ctrip.com/'));
220
+ await check('防漂移:检索/登录 URL 面(background per-site 白名单 flights+hotels;ISOLATED 桥转发两类事件)', ()=>{
221
+ assert.ok(backgroundJs.includes('https://flights.ctrip.com/'), 'background 应含机票检索白名单');
222
+ assert.ok(backgroundJs.includes(`https://${HOTEL_SITE_HOST}/`), 'background 应含酒店检索白名单(2026-09-03 实装)');
223
+ assert.ok(backgroundJs.includes('https://kyfw.12306.cn/'), 'background 应含火车检索白名单(2026-09-03 实装)');
224
+ assert.ok(backgroundJs.includes("'ctrip-hotel'"), 'background SITES 应注册 ctrip-hotel');
225
+ assert.ok(backgroundJs.includes("'train-12306'"), 'background SITES 应注册 train-12306');
198
226
  assert.ok(contentMainJs.includes('gotry-ctrip-sniff'));
199
227
  assert.ok(contentBridgeJs.includes('gotry-ctrip-sniff'));
200
228
  assert.ok(contentBridgeJs.includes('gotry-page'));
@@ -453,16 +481,43 @@ async function main() {
453
481
  ]
454
482
  };
455
483
  if (job.kind === 'search') {
456
- assert.ok(String(job.url).startsWith('https://flights.ctrip.com/'));
484
+ const u = String(job.url);
485
+ const isHotel = u.startsWith(`https://${HOTEL_SITE_HOST}/`);
486
+ const isTrain = u.startsWith('https://kyfw.12306.cn/');
487
+ assert.ok(u.startsWith('https://flights.ctrip.com/') || isHotel || isTrain, `search job 只允许已注册站点域,实际 ${u}`);
457
488
  return {
458
489
  ok: true,
459
490
  kind: 'search',
460
- body: JSON.stringify({
491
+ body: isHotel ? JSON.stringify({
492
+ data: {
493
+ hotelList: [
494
+ {
495
+ hotelId: 442516,
496
+ hotelName: 'Hotel X',
497
+ star: 5,
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({
461
516
  data: {
462
517
  flightItineraryList: []
463
518
  }
464
519
  }),
465
- title: '机票列表'
520
+ title: isHotel ? '酒店列表' : isTrain ? '12306 车票预订' : '机票列表'
466
521
  };
467
522
  }
468
523
  return {
@@ -491,6 +546,40 @@ async function main() {
491
546
  assert.ok(search.ok);
492
547
  assert.equal(search.ok ? search.timedOut : true, false);
493
548
  assert.ok(search.ok ? search.body.includes('flightItineraryList') : false);
549
+ const hotelLane = await extensionSearchJob({
550
+ site: 'ctrip-hotel',
551
+ url: `https://${HOTEL_SITE_HOST}/hotels/list?city=220&checkin=2026-12-01&checkout=2026-12-03`,
552
+ timeoutMs: 3_000
553
+ });
554
+ assert.ok(hotelLane.ok, '酒店 search job 应放行(per-site 白名单)');
555
+ assert.ok(hotelLane.ok ? hotelLane.body.includes('hotelList') : false);
556
+ __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
+ });
564
+ assert.equal(hotelSession.verdict, 'hit', `酒店会话检索应 hit,实际 ${hotelSession.verdict}:${hotelSession.error ?? ''}`);
565
+ assert.ok(hotelSession.evidence.includes('[会话:ctrip-hotel@'), '酒店证据链 [会话:ctrip-hotel@ts]');
566
+ assert.equal(hotelSession.hotels?.[0]?.name, 'Hotel X');
567
+ assert.equal(hotelSession.hotels?.[0]?.price, 680);
568
+ assert.ok(hotelSession.hotels?.[0]?.jumpUrl?.includes('/hotel/442516'), 'jumpUrl 由 hotelId 构造(预订由人在落地页完成)');
569
+ __resetRateLimiterForTest();
570
+ const trainSession = await sessionTrainSearch({
571
+ from: '上海',
572
+ to: '昆明',
573
+ date: '2026-12-01',
574
+ timeoutMs: 3_000
575
+ });
576
+ assert.equal(trainSession.verdict, 'hit', `火车会话检索应 hit,实际 ${trainSession.verdict}:${trainSession.error ?? ''}`);
577
+ assert.ok(trainSession.evidence.includes('[会话:train-12306@'), '火车证据链 [会话:train-12306@ts]');
578
+ assert.ok(trainSession.evidence.includes('公开查询面'), '证据链标注公开查询面(无登录闸的诚实口径)');
579
+ assert.equal(trainSession.trains?.[0]?.trainCode, 'G1375');
580
+ assert.equal(trainSession.trains?.[0]?.depTime, '07:35');
581
+ assert.equal(trainSession.trains?.[0]?.durationMin, 472);
582
+ assert.ok(trainSession.trains?.[0]?.jumpUrl?.includes('kyfw.12306.cn'), 'jumpUrl=查询落地页(人选车完成预订)');
494
583
  const login = await sessionLogin({
495
584
  site: 'ctrip-flight'
496
585
  });
@@ -558,6 +647,35 @@ async function main() {
558
647
  assert.equal(e.quota_disposition, 'no_spend_waiting_user');
559
648
  assert.equal(e.retry_allowed, false);
560
649
  });
650
+ await check('needs-extension 自适应文案:本地已落位 → 双通道完整指引(含开发者模式)', ()=>{
651
+ const s = needsExtensionSummary({
652
+ localInstalled: true
653
+ });
654
+ assert.match(s, /开发者模式「加载已解压的扩展程序」/, '本地通道在 → 保留开发者模式指引');
655
+ assert.ok(s.includes(EXTENSION_STORE_URL), '商店 URL 仍在');
656
+ });
657
+ await check('needs-extension 自适应文案:本地未落位(商店版用户/未装)→ 自动跳过 dev-mode 文案', ()=>{
658
+ const s = needsExtensionSummary({
659
+ localInstalled: false
660
+ });
661
+ assert.doesNotMatch(s, /开发者模式/, '不再推开发者模式/本地通道(商店版用户不该被指导 load unpacked)');
662
+ assert.doesNotMatch(s, /加载已解压/, '不再出现 load unpacked 字样');
663
+ assert.match(s, /已从 Chrome 商店安装/, '给「已装商店版?」提示(打开 Chrome 即可,无需再装)');
664
+ assert.match(s, /应用商店一键装/, '未装用户仍得商店一键装指引');
665
+ });
666
+ await check('localExtensionInstalled:HOME 注入可测(manifest 存在=本地通道在)', ()=>{
667
+ const fakeHome = mkdtempSync(join(tmpdir(), 'gotry-ext-home-'));
668
+ assert.equal(localExtensionInstalled(fakeHome), false, '空 HOME → 本地通道未落位');
669
+ mkdirSync(join(fakeHome, '.gotry', 'extension'), {
670
+ recursive: true
671
+ });
672
+ writeFileSync(join(fakeHome, '.gotry', 'extension', 'manifest.json'), '{}', 'utf-8');
673
+ assert.equal(localExtensionInstalled(fakeHome), true, 'manifest 落位 → true');
674
+ rmSync(fakeHome, {
675
+ recursive: true,
676
+ force: true
677
+ });
678
+ });
561
679
  await b.close();
562
680
  console.log(`\n§38 extension bridge: ${passed} 段全绿`);
563
681
  }