@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.
Files changed (128) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/gotry-bootstrap.js +63 -53
  4. package/bin/gotry-hbcli-release.d.ts +3 -0
  5. package/bin/gotry-hbcli-release.js +4 -0
  6. package/client/client.js +66 -21
  7. package/dist/capabilities/doctor.js +7 -8
  8. package/dist/capabilities/effect.js +45 -12
  9. package/dist/capabilities/flyai-config.js +12 -10
  10. package/dist/capabilities/flyai.js +87 -56
  11. package/dist/capabilities/hbcli-config.js +156 -0
  12. package/dist/capabilities/hbcli.js +29 -2
  13. package/dist/capabilities/resilience.js +6 -6
  14. package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
  16. package/dist/capabilities/session/bridge-ledger.js +168 -67
  17. package/dist/capabilities/session/ctrip-city.js +186 -0
  18. package/dist/capabilities/session/extension-bridge.js +148 -47
  19. package/dist/capabilities/session/gui-flight.js +949 -0
  20. package/dist/capabilities/session/gui-hotel.js +470 -0
  21. package/dist/capabilities/session-search.js +102 -14
  22. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
  23. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
  24. package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
  25. package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
  26. package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
  27. package/dist/scripts/bootstrap-tests.js +23 -6
  28. package/dist/scripts/bridge-ledger-tests.js +20 -18
  29. package/dist/scripts/ctrip-city-tests.js +80 -0
  30. package/dist/scripts/effect-tests.js +602 -504
  31. package/dist/scripts/extension-tests.js +14 -8
  32. package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
  33. package/dist/scripts/flight-malformed-tests.js +1 -1
  34. package/dist/scripts/flyai-setup-tests.js +19 -4
  35. package/dist/scripts/flyai-tests.js +768 -653
  36. package/dist/scripts/gotry-web-api-tests.js +5 -0
  37. package/dist/scripts/gotry-web-client-tests.js +20 -1
  38. package/dist/scripts/gui-flight-live.js +27 -0
  39. package/dist/scripts/hbcli-tests.js +1 -1
  40. package/dist/scripts/hbcli-upgrade-tests.js +107 -0
  41. package/dist/scripts/hbcli-web-setup-tests.js +114 -0
  42. package/dist/scripts/hotel-date-gate-tests.js +1 -1
  43. package/dist/scripts/hotel-result-tests.js +169 -0
  44. package/dist/scripts/issue-271-liveness-tests.js +1 -1
  45. package/dist/scripts/mutation-check.js +153 -0
  46. package/dist/scripts/nightly-evidence.js +4 -111
  47. package/dist/scripts/session-tests.js +211 -6
  48. package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
  49. package/dist/scripts/tenant-capacity-probe.js +263 -0
  50. package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
  51. package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
  52. package/dist/scripts/tenant-executor-binding-tests.js +203 -0
  53. package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
  54. package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
  55. package/dist/scripts/tenant-seam-quota-tests.js +383 -0
  56. package/dist/scripts/tenant-seam-test-kit.js +292 -0
  57. package/dist/scripts/tenant-state-copies-tests.js +607 -0
  58. package/dist/scripts/tenant-state-drill-tests.js +298 -0
  59. package/dist/scripts/tenant-state-drill-worker.js +164 -0
  60. package/dist/scripts/tenant-state-durable-conformance.js +101 -0
  61. package/dist/scripts/tenant-state-handoff-tests.js +854 -0
  62. package/dist/scripts/tenant-state-lease-conformance.js +142 -0
  63. package/dist/scripts/tenant-state-lease-worker.js +28 -0
  64. package/dist/scripts/tenant-state-router-tests.js +464 -0
  65. package/dist/scripts/tenant-state-test-kit.js +92 -0
  66. package/dist/scripts/tenant-state-tests.js +564 -0
  67. package/dist/src/backend/modules/booking-executor.js +61 -6
  68. package/dist/src/backend/modules/session-search.js +136 -39
  69. package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  70. package/dist/src/booking-surface/dsh-planner.js +150 -19
  71. package/dist/src/booking-surface/index.js +1 -0
  72. package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
  73. package/dist/src/booking-surface/server.js +193 -80
  74. package/dist/src/booking-surface/startup.js +153 -0
  75. package/dist/src/booking-surface/tenancy.js +188 -0
  76. package/dist/src/gotry-web-api.js +64 -2
  77. package/dist/src/index.js +70 -18
  78. package/dist/src/llm-price-table.js +148 -0
  79. package/dist/src/tenant-seam/index.js +8 -0
  80. package/dist/src/tenant-seam/llm-broker.js +508 -0
  81. package/dist/src/tenant-seam/process-governor.js +281 -0
  82. package/dist/src/tenant-seam/quotas.js +360 -0
  83. package/dist/src/tenant-seam/seam.js +92 -0
  84. package/dist/src/tenant-state/durable-store.js +158 -0
  85. package/dist/src/tenant-state/epoch-copies.js +422 -0
  86. package/dist/src/tenant-state/errors.js +108 -0
  87. package/dist/src/tenant-state/ids.js +101 -0
  88. package/dist/src/tenant-state/index.js +11 -0
  89. package/dist/src/tenant-state/lease-store.js +203 -0
  90. package/dist/src/tenant-state/tenant-lease.js +150 -0
  91. package/dist/src/tenant-state/tenant-router.js +650 -0
  92. package/extension/manifest.json +2 -2
  93. package/package.json +5 -1
  94. package/ts/capabilities/flyai-config.ts +12 -10
  95. package/ts/capabilities/flyai.ts +91 -50
  96. package/ts/capabilities/hbcli-config.ts +128 -0
  97. package/ts/capabilities/hbcli.ts +30 -3
  98. package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
  99. package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
  100. package/ts/capabilities/session/bridge-ledger.ts +192 -78
  101. package/ts/capabilities/session/ctrip-city.ts +158 -0
  102. package/ts/capabilities/session/extension-bridge.ts +185 -67
  103. package/ts/capabilities/session/gui-flight.ts +1022 -0
  104. package/ts/capabilities/session/gui-hotel.ts +505 -0
  105. package/ts/capabilities/session-search.ts +130 -18
  106. package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  107. package/ts/src/booking-surface/dsh-planner.ts +169 -19
  108. package/ts/src/booking-surface/index.ts +1 -0
  109. package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
  110. package/ts/src/booking-surface/server.ts +215 -84
  111. package/ts/src/booking-surface/startup.ts +155 -0
  112. package/ts/src/booking-surface/tenancy.ts +242 -0
  113. package/ts/src/gotry-web-api.ts +51 -2
  114. package/ts/src/index.ts +67 -19
  115. package/ts/src/llm-price-table.ts +180 -0
  116. package/ts/src/tenant-seam/index.ts +9 -0
  117. package/ts/src/tenant-seam/llm-broker.ts +520 -0
  118. package/ts/src/tenant-seam/process-governor.ts +343 -0
  119. package/ts/src/tenant-seam/quotas.ts +433 -0
  120. package/ts/src/tenant-seam/seam.ts +142 -0
  121. package/ts/src/tenant-state/durable-store.ts +171 -0
  122. package/ts/src/tenant-state/epoch-copies.ts +491 -0
  123. package/ts/src/tenant-state/errors.ts +128 -0
  124. package/ts/src/tenant-state/ids.ts +108 -0
  125. package/ts/src/tenant-state/index.ts +12 -0
  126. package/ts/src/tenant-state/lease-store.ts +266 -0
  127. package/ts/src/tenant-state/tenant-lease.ts +175 -0
  128. package/ts/src/tenant-state/tenant-router.ts +743 -0
@@ -29,11 +29,16 @@ else process.stdout.write(JSON.stringify({data:{itemList:[]}}));
29
29
  });
30
30
  const status = () => handlers.flyai(new Request('http://localhost/api/gotry/flyai')).then((r) => r.json());
31
31
  assert.deepEqual(await status(), { ok: true, configured: false, verified: false, writable: true, source: 'none', endpoint: 'https://flyai.open.fliggy.com/mcp', endpointDebug: false });
32
+ mkdirSync(join(home, '.gotry'), { mode: 0o755 });
33
+ chmodSync(join(home, '.gotry'), 0o755);
34
+ writeFileSync(join(home, '.gotry', 'doctor-report.md'), 'existing report');
32
35
  let response = await handlers.flyai(request('save', 'sk-good-candidate-1234'));
33
36
  assert.equal(response.status, 200);
34
37
  let body = await response.json();
35
38
  assert.equal(body.ok, true);
36
39
  assert.equal(body.verified, true);
40
+ assert.equal(statSync(join(home, '.gotry')).mode & 0o777, 0o700);
41
+ assert.equal(readFileSync(join(home, '.gotry', 'doctor-report.md'), 'utf8'), 'existing report');
37
42
  assert.equal(body.source, 'config');
38
43
  assert.ok(!JSON.stringify(body).includes('sk-good-candidate'));
39
44
  const config = join(home, '.flyai/config.json'), receipt = join(home, '.gotry/flyai-verification.json');
@@ -6,7 +6,8 @@ import vm from 'node:vm';
6
6
  const registrations = [], slots = [], tabs = [];
7
7
  const source = readFileSync(join(import.meta.dirname, '../../client/client.js'), 'utf8');
8
8
  vm.runInNewContext(source, { window: { __ModuleLoader__: { load: (r) => registrations.push(r) } }, console, fetch, AbortController });
9
- const react = { createElement: (type, props, ...children) => ({ type, props, children }) };
9
+ const react = { createElement: (type, props, ...children) => ({ type, props, children }),
10
+ useState: (value) => [value, () => { }], useRef: (value) => ({ current: value }), useEffect: () => { } };
10
11
  const primitives = { SettingsForm: () => { }, SettingsSecretField: () => { }, Button: () => { }, Input: () => { }, Tag: () => { }, FileTypeIcon: () => { } };
11
12
  const plugin = registrations[0].factory((name) => name === 'react' ? react : primitives);
12
13
  const context = {
@@ -19,6 +20,24 @@ const context = {
19
20
  };
20
21
  plugin.apply(context);
21
22
  assert.ok(slots.some(s => s.definition.name === 'plugins.item' && s.definition.id === 'gotry'), 'Plugins must contain a directly visible GoTry configuration entry');
23
+ const settings = slots.find(s => s.definition.name === 'plugins.item' && s.definition.id === 'gotry');
24
+ const nodes = [];
25
+ function visit(node) {
26
+ if (!node || typeof node !== 'object')
27
+ return;
28
+ nodes.push(node);
29
+ if (typeof node.type === 'function')
30
+ visit(node.type(node.props));
31
+ for (const child of node.children ?? [])
32
+ visit(child);
33
+ }
34
+ visit(settings.component({ t: (key) => key }));
35
+ assert.ok(nodes.some(n => n.props?.['data-gotry-flyai-settings'] === ''), 'existing FlyAI configuration remains available');
36
+ assert.ok(nodes.some(n => n.props?.['data-gotry-hbcli-settings'] === ''), 'HotelByte configuration must be visible in the same page');
37
+ for (const id of ['gotry-flyai-key', 'gotry-hbcli-key', 'gotry-hbcli-secret']) {
38
+ assert.ok(nodes.some(n => n.type === primitives.SettingsSecretField && n.props?.id === id && n.props?.text === ''), `${id} must use an empty confidential field`);
39
+ }
40
+ assert.ok(nodes.some(n => n.type === 'a' && n.props?.href === 'https://hotelbyte.com/zh/guides/sandbox-verification'));
22
41
  assert.ok(tabs.some(t => t.kind === 'gotry-artifacts' && t.guide?.length), 'right Sidebar must contain an always discoverable GoTry artifacts page');
23
42
  assert.ok(slots.some(s => s.definition.name === 'sidebar.right.pane.tab' && s.definition.key === '@danceiny/gotry:artifacts'));
24
43
  const pkg = JSON.parse(readFileSync(join(import.meta.dirname, '../../package.json'), 'utf8'));
@@ -0,0 +1,27 @@
1
+ /**
2
+ * GUI 车道 live 验证入口(GOTRY_GUI_LIVE=1 显式 opt-in;默认 SKIP 不开窗口——
3
+ * 与 session-tests 的 GOTRY_SESSION_LIVE 测试纪律同构)。
4
+ * 匿名只读:gotry 自己的隔离 Chrome 窗口驱动携程 PC 表单,不登录、不下单。
5
+ */
6
+ import { sessionFlightGuiSearch } from '../capabilities/session-search.js';
7
+ const roll = new Date(Date.now() + 30 * 24 * 3600 * 1000);
8
+ const dep = `${roll.getFullYear()}-${String(roll.getMonth() + 1).padStart(2, '0')}-${String(roll.getDate()).padStart(2, '0')}`;
9
+ const rollBack = new Date(Date.now() + 34 * 24 * 3600 * 1000);
10
+ const back = `${rollBack.getFullYear()}-${String(rollBack.getMonth() + 1).padStart(2, '0')}-${String(rollBack.getDate()).padStart(2, '0')}`;
11
+ if (process.env.GOTRY_GUI_LIVE !== '1') {
12
+ console.log('SKIP - GOTRY_GUI_LIVE!=1(GUI 车道 live 验证未开;离线契约见 session-tests §L)');
13
+ process.exit(0);
14
+ }
15
+ console.log(`live #1 单程 上海→丽江 ${dep}`);
16
+ const r1 = await sessionFlightGuiSearch({ from: '上海', to: '丽江', date: dep, timeoutMs: 120_000 });
17
+ console.log(JSON.stringify({ ok: r1.ok, verdict: r1.verdict, options: r1.options?.length, calendar: r1.calendar?.length, calendarHead: r1.calendar?.slice(0, 5), listUrl: r1.listUrl, evidence: r1.evidence, error: r1.error?.slice(0, 900) }, null, 2));
18
+ if (r1.verdict === 'hit')
19
+ console.log(' 首条:', JSON.stringify(r1.options?.[0]));
20
+ if (process.argv[2] === 'oneway')
21
+ process.exit(0);
22
+ console.log(`live #2 往返 香港→迪拜 ${dep} / ${back}(等 31s 节律)`);
23
+ await new Promise(r => setTimeout(r, 31_000));
24
+ const r2 = await sessionFlightGuiSearch({ from: '香港', to: '迪拜', date: dep, backDate: back, timeoutMs: 120_000 });
25
+ console.log(JSON.stringify({ ok: r2.ok, verdict: r2.verdict, options: r2.options?.length, calendar: r2.calendar?.length, calendarHead: r2.calendar?.slice(0, 5), listUrl: r2.listUrl, evidence: r2.evidence, error: r2.error?.slice(0, 900) }, null, 2));
26
+ if (r2.verdict === 'hit')
27
+ console.log(' 首条:', JSON.stringify(r2.options?.[0]));
@@ -44,7 +44,7 @@ await writeFile(fallback, JSON.stringify({ meta: 'fake', stays: [{ id: 's1', not
44
44
  {
45
45
  const { writeFileSync, chmodSync } = await import('node:fs');
46
46
  const echoBin = join(tmp, 'hbcli-echo');
47
- writeFileSync(echoBin, '#!/bin/sh\necho "ARGS:$@"; echo \'{}\' ');
47
+ writeFileSync(echoBin, '#!/bin/sh\necho "ARGS:$@"; echo \'{"list":[]}\' ');
48
48
  chmodSync(echoBin, 0o755);
49
49
  const re = await searchHotels({ destination: '普吉', adults: 2 }, { hbcliBin: echoBin });
50
50
  const argLine = JSON.stringify(re);
@@ -0,0 +1,107 @@
1
+ /** hbcli dependency baseline and installer upgrade checks. All homes/binaries are isolated. */
2
+ import assert from 'node:assert/strict';
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { runDoctorChecks } from '../capabilities/doctor.js';
8
+ const root = mkdtempSync(join(tmpdir(), 'gotry-hbcli-upgrade-test-'));
9
+ const home = join(root, 'home');
10
+ const bin = join(home, '.local/bin/hbcli');
11
+ const credentials = join(home, '.staicli/credentials.json');
12
+ const originalCredentials = '{"openapi:uat":{"appKey":"fixture","appSecret":"fixture"},"portal:prod":{"ticket":"preserve"}}';
13
+ mkdirSync(join(home, '.local/bin'), { recursive: true });
14
+ mkdirSync(join(home, '.staicli'), { recursive: true });
15
+ mkdirSync(join(root, 'empty-path'));
16
+ writeFileSync(credentials, originalCredentials, { mode: 0o600 });
17
+ const writeVersion = (version) => writeFileSync(bin, `#!/bin/sh\ncase "$*" in\n *version*) echo '${version}';;\n *whoami*) echo '{"api_key":{"configured":true}}';;\nesac\n`, { mode: 0o755 });
18
+ const previousPath = process.env.PATH;
19
+ try {
20
+ process.env.PATH = join(root, 'empty-path');
21
+ writeVersion('0.0.3');
22
+ const old = (await runDoctorChecks({ homeDir: home, repoRoot: root, env: {}, extensionPorts: [] })).items.find(i => i.id === 'hbcli');
23
+ assert.equal(old.status, 'missing', '0.0.3 must be upgraded even when credentials are configured');
24
+ assert.match(old.detail, /0\.0\.4/);
25
+ assert.match(old.fix ?? '', /staicli-v0\.0\.4\/install\.sh/);
26
+ assert.match(old.fix ?? '', /--version 0\.0\.4/);
27
+ assert.doesNotMatch(old.detail, /会 401/, 'an old dependency version alone does not prove an authentication failure');
28
+ console.log('1. doctor rejects the old baseline and gives the pinned official upgrade');
29
+ const { setupHbcli } = await import(new URL('../../bin/gotry-bootstrap.js', import.meta.url).href);
30
+ let installs = 0;
31
+ const install = async (command) => {
32
+ installs++;
33
+ assert.match(command, /staicli-v0\.0\.4\/install\.sh/);
34
+ assert.match(command, /--version 0\.0\.4/);
35
+ writeVersion('0.0.4');
36
+ return { ok: true };
37
+ };
38
+ assert.equal((await setupHbcli({ homeDir: home, attemptInstall: install })).ok, true);
39
+ assert.equal(installs, 1, 'old binary must enter the installer instead of returning an upgrade hint');
40
+ assert.equal(readFileSync(credentials, 'utf8'), originalCredentials, 'upgrading does not rewrite credentials');
41
+ const current = (await runDoctorChecks({ homeDir: home, repoRoot: root, env: {}, extensionPorts: [] })).items.find(i => i.id === 'hbcli');
42
+ assert.equal(current.status, 'ok');
43
+ console.log('2. old binary upgrades and is rechecked; existing credentials are preserved');
44
+ assert.equal((await setupHbcli({ homeDir: home, attemptInstall: install })).ok, true);
45
+ writeVersion('0.0.5');
46
+ assert.equal((await setupHbcli({ homeDir: home, attemptInstall: install })).ok, true);
47
+ assert.equal(installs, 1, 'current and newer versions must not be reinstalled or downgraded');
48
+ console.log('3. current/newer binaries remain installed without downgrade');
49
+ writeVersion('0.0.3');
50
+ await setupHbcli({ homeDir: home, checkOnly: true, attemptInstall: install });
51
+ assert.equal(installs, 1, 'check-only cannot run an installer');
52
+ assert.equal((await setupHbcli({ homeDir: home, attemptInstall: async () => ({ ok: false, error: 'fixture failure' }) })).ok, false);
53
+ assert.equal(readFileSync(credentials, 'utf8'), originalCredentials);
54
+ console.log('4. check-only and failed installation preserve the existing configuration');
55
+ assert.equal((await setupHbcli({ homeDir: home, attemptInstall: async () => ({ ok: true }) })).ok, false, 'installer exit success without the required binary is not a successful upgrade');
56
+ rmSync(bin);
57
+ assert.equal((await setupHbcli({ homeDir: home, attemptInstall: install })).ok, true, 'missing binary uses the same pinned installer');
58
+ assert.equal(installs, 2);
59
+ console.log('5. post-install version is required; missing binary uses the same installer');
60
+ const shadowBin = join(root, 'empty-path/hbcli');
61
+ writeFileSync(shadowBin, '#!/bin/sh\necho 0.0.3\n', { mode: 0o755 });
62
+ const shadowed = await setupHbcli({ homeDir: home, attemptInstall: async () => ({ ok: true }) });
63
+ assert.equal(shadowed.ok, false, 'a newer fallback binary cannot hide an older binary selected by PATH');
64
+ assert.match(shadowed.error ?? '', /PATH/);
65
+ rmSync(shadowBin);
66
+ console.log('5b. PATH precedence is rechecked and shadowing is reported');
67
+ // Optional real release artifact proof: no supplier request and no ambient credentials.
68
+ const binary = process.env.HBCLI_RELEASE_BINARY;
69
+ if (binary) {
70
+ const { spawnSync } = await import('node:child_process');
71
+ const digests = {
72
+ 'darwin-arm64': '0fe14ddeafda8287ac0cd5ab7e1ee88a180c1d46b75c120d2002c7f74f6d5b54',
73
+ 'darwin-x64': '6a389403b170a5c68a6d73ad970bc75dfbd69a533d84d5a9a7ab76b4450a70ba',
74
+ 'linux-arm64': '8fc87bec736d50ab63488c4c8fef62630ec147ac2ea7a05cc6e12e190bf2182c',
75
+ 'linux-x64': '7c3f297e20131bc163a2c0040a8c4e438fc1cbd9179f80f48b81b3a6d4ee0b11',
76
+ };
77
+ assert.equal(createHash('sha256').update(readFileSync(binary)).digest('hex'), digests[`${process.platform}-${process.arch}`], 'binary must match the official staicli-v0.0.4 release asset digest');
78
+ const releaseHome = join(root, 'release-home');
79
+ mkdirSync(releaseHome);
80
+ const releaseEnv = { HOME: releaseHome, STAICLI_HOME: join(releaseHome, '.staicli'), PATH: previousPath ?? '' };
81
+ const run = (args) => {
82
+ const r = spawnSync(binary, args, { cwd: releaseHome, env: releaseEnv, encoding: 'utf8', timeout: 10_000 });
83
+ assert.equal(r.error, undefined);
84
+ assert.equal(r.status, 0, `release CLI command failed: ${args.join(' ')}`);
85
+ return r.stdout;
86
+ };
87
+ assert.equal(run(['--version']).trim(), '0.0.4');
88
+ const hotelHelp = run(['search', 'hotel-list', '--help']);
89
+ for (const flag of ['--destination-name', '--check-in', '--check-out', '--room-occupancies', '--page-size'])
90
+ assert.ok(hotelHelp.includes(flag));
91
+ for (const command of ['customer-send-code', 'customer-login', 'register'])
92
+ assert.ok(run(['auth', '--help']).includes(command));
93
+ run(['--json', '--env', 'uat', 'auth', 'set-credentials', '--app-key', 'fixture', '--app-secret', 'fixture']);
94
+ const stored = JSON.parse(readFileSync(join(releaseEnv.STAICLI_HOME, 'credentials.json'), 'utf8'));
95
+ assert.deepEqual(stored['openapi:uat'], { appKey: 'fixture', appSecret: 'fixture' }, 'GUI credential format matches the release CLI');
96
+ assert.equal(JSON.parse(run(['--json', '--env', 'uat', 'auth', 'whoami'])).api_key.configured, true);
97
+ assert.ok(!existsSync(join(home, '.staicli/versions')), 'real artifact proof stays in its isolated home');
98
+ console.log('6. official release digest/version, hotel flags, auth commands and GUI credential format verified offline');
99
+ }
100
+ else
101
+ console.log('6. SKIP real artifact proof: set HBCLI_RELEASE_BINARY to the downloaded official 0.0.4 binary');
102
+ }
103
+ finally {
104
+ process.env.PATH = previousPath;
105
+ rmSync(root, { recursive: true, force: true });
106
+ }
107
+ console.log('HBCLI UPGRADE TESTS: OK');
@@ -0,0 +1,114 @@
1
+ /** Native HotelByte configuration: isolated official credential format and a fixture CLI. */
2
+ import assert from 'node:assert/strict';
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { createGotryWebHandlers } from '../src/gotry-web-api.js';
7
+ const root = mkdtempSync(join(tmpdir(), 'gotry-hbcli-web-'));
8
+ try {
9
+ const home = join(root, 'home'), cli = join(root, 'hbcli-fixture');
10
+ mkdirSync(home);
11
+ writeFileSync(cli, `#!${process.execPath}
12
+ const fs = require('node:fs'), path = require('node:path');
13
+ const credentials = JSON.parse(fs.readFileSync(path.join(process.env.STAICLI_HOME, 'credentials.json')));
14
+ const candidate = credentials['openapi:uat'];
15
+ fs.writeFileSync(process.env.HBCLI_CAPTURE, JSON.stringify({ argv: process.argv.slice(2), root: process.env.STAICLI_HOME }));
16
+ const reply = () => {
17
+ if (candidate.appKey === 'hbk-invalid') { process.stderr.write(candidate.appSecret); process.stdout.write(JSON.stringify({code:401,msg:candidate.appSecret})); }
18
+ else if (candidate.appKey === 'hbk-no-suppliers') process.stdout.write(JSON.stringify({code:300010002,msg:'no available suppliers configured'}));
19
+ else if (candidate.appKey === 'hbk-malformed') process.stdout.write('{}');
20
+ else process.stdout.write(JSON.stringify({list:[{id:123,name:{zh:'Fixture Hotel'},minPrice:{amount:680,currency:'CNY'}}]}));
21
+ };
22
+ if (candidate.appKey === 'hbk-pending') setTimeout(reply, 800); else reply();
23
+ `, { mode: 0o700 });
24
+ const env = { PATH: process.env.PATH, HOME: home, GOTRY_HBCLI_BIN: cli, HBCLI_CAPTURE: join(root, 'capture.json') };
25
+ const handlers = createGotryWebHandlers({ homeDir: home, env, stateRoot: root, workspaceForSession: async () => root });
26
+ assert.equal(typeof handlers.hbcli, 'function', 'the native configuration page needs an hbcli handler');
27
+ const request = (action, appKey, signal) => new Request('http://localhost/api/gotry/hbcli/write', {
28
+ method: 'POST', headers: { 'content-type': 'application/json' },
29
+ body: JSON.stringify({ action, ...(appKey ? { appKey, appSecret: 'hbs-fixture-secret' } : {}) }), signal,
30
+ });
31
+ const status = () => handlers.hbcli(new Request('http://localhost/api/gotry/hbcli')).then(r => r.json());
32
+ assert.equal((await status()).configured, false);
33
+ assert.equal(existsSync(env.HBCLI_CAPTURE), false, 'status must not invoke the CLI or contact a supplier');
34
+ const dir = join(home, '.staicli'), file = join(dir, 'credentials.json');
35
+ mkdirSync(dir, { mode: 0o755 });
36
+ writeFileSync(file, JSON.stringify({ 'portal:uat': { ticket: 'preserve-other-mode' }, 'openapi:prod': { appKey: 'preserve-prod' } }), { mode: 0o600 });
37
+ let response = await handlers.hbcli(request('save', 'hbk-valid'));
38
+ assert.equal(response.status, 200);
39
+ let body = await response.json();
40
+ assert.equal(body.configured, true);
41
+ assert.equal(body.verified, true);
42
+ assert.equal(body.verdict, 'hit');
43
+ assert.doesNotMatch(JSON.stringify(body), /hbk-valid|hbs-fixture-secret|preserve-other-mode/);
44
+ const saved = JSON.parse(readFileSync(file, 'utf8'));
45
+ assert.deepEqual(saved['openapi:uat'], { appKey: 'hbk-valid', appSecret: 'hbs-fixture-secret' });
46
+ assert.equal(saved['portal:uat'].ticket, 'preserve-other-mode');
47
+ assert.equal(saved['openapi:prod'].appKey, 'preserve-prod');
48
+ assert.equal(statSync(dir).mode & 0o777, 0o700);
49
+ assert.equal(statSync(file).mode & 0o777, 0o600);
50
+ const capture = JSON.parse(readFileSync(env.HBCLI_CAPTURE, 'utf8'));
51
+ assert.notEqual(capture.root, dir, 'verification must use a temporary credential root');
52
+ assert.ok(capture.argv.includes('hotel-list'));
53
+ assert.doesNotMatch(JSON.stringify(capture.argv), /hbk-valid|hbs-fixture-secret/, 'secrets must never be passed in argv');
54
+ const previous = readFileSync(file, 'utf8');
55
+ for (const key of ['hbk-invalid', 'hbk-malformed']) {
56
+ response = await handlers.hbcli(request('save', key));
57
+ assert.equal(response.status, 422);
58
+ assert.doesNotMatch(await response.text(), /hbs-fixture-secret/);
59
+ assert.equal(readFileSync(file, 'utf8'), previous);
60
+ }
61
+ const lockedEnv = { ...env, HOTELBYTE_TOKEN: 'fixture-launch-token' };
62
+ const locked = createGotryWebHandlers({ homeDir: home, env: lockedEnv, stateRoot: root, workspaceForSession: async () => root });
63
+ assert.equal((await (await locked.hbcli(new Request('http://localhost/api/gotry/hbcli'))).json()).writable, false);
64
+ assert.equal((await locked.hbcli(request('save', 'hbk-valid'))).status, 409);
65
+ assert.equal(readFileSync(file, 'utf8'), previous);
66
+ const cancelled = new AbortController();
67
+ const pending = handlers.hbcli(request('save', 'hbk-pending', cancelled.signal));
68
+ setTimeout(() => cancelled.abort(), 100);
69
+ assert.equal((await pending).status, 422);
70
+ assert.equal(readFileSync(file, 'utf8'), previous);
71
+ const concurrent = handlers.hbcli(request('save', 'hbk-pending'));
72
+ setTimeout(() => writeFileSync(file, previous + '\n'), 150);
73
+ assert.equal((await concurrent).status, 409);
74
+ writeFileSync(file, previous);
75
+ const receiptDir = join(home, '.gotry'), receiptBackup = readFileSync(join(receiptDir, 'hbcli-verification.json'), 'utf8');
76
+ rmSync(receiptDir, { recursive: true });
77
+ writeFileSync(receiptDir, 'receipt storage unavailable');
78
+ assert.equal((await handlers.hbcli(request('save', 'hbk-other-valid'))).status, 409);
79
+ assert.equal(readFileSync(file, 'utf8'), previous, 'failed receipt storage must restore the previous credential data');
80
+ rmSync(receiptDir);
81
+ mkdirSync(receiptDir, { mode: 0o700 });
82
+ writeFileSync(join(receiptDir, 'hbcli-verification.json'), receiptBackup, { mode: 0o600 });
83
+ response = await handlers.hbcli(request('save', 'hbk-no-suppliers'));
84
+ body = await response.json();
85
+ assert.equal(response.status, 200);
86
+ assert.equal(body.configured, true);
87
+ assert.equal(body.verified, false, 'no suppliers is not a successful inventory query');
88
+ assert.equal(body.verdict, 'no-suppliers');
89
+ assert.equal((await status()).verdict, 'no-suppliers');
90
+ assert.equal((await handlers.hbcli(request('clear'))).status, 200);
91
+ assert.equal((await status()).configured, false);
92
+ const cleared = JSON.parse(readFileSync(file, 'utf8'));
93
+ assert.equal(cleared['openapi:uat'], undefined);
94
+ assert.equal(cleared['portal:uat'].ticket, 'preserve-other-mode');
95
+ assert.equal(cleared['openapi:prod'].appKey, 'preserve-prod');
96
+ writeFileSync(file, '{broken configuration');
97
+ assert.equal((await handlers.hbcli(request('save', 'hbk-valid'))).status, 409);
98
+ assert.equal(readFileSync(file, 'utf8'), '{broken configuration');
99
+ rmSync(dir, { recursive: true });
100
+ const outside = join(root, 'outside');
101
+ mkdirSync(outside);
102
+ symlinkSync(outside, dir, 'dir');
103
+ assert.equal((await handlers.hbcli(request('save', 'hbk-valid'))).status, 409);
104
+ assert.equal(existsSync(join(outside, 'credentials.json')), false);
105
+ rmSync(dir);
106
+ response = await handlers.hbcli(new Request('http://localhost/api/gotry/hbcli/write', {
107
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'save', appKey: 'x'.repeat(9000), appSecret: 'fixture' }),
108
+ }));
109
+ assert.equal(response.status, 413);
110
+ console.log('hbcli native Web setup: isolated verification, confidential responses, supplier status, cancellation, conflict, preservation, clear and file guards PASS');
111
+ }
112
+ finally {
113
+ rmSync(root, { recursive: true, force: true });
114
+ }
@@ -30,7 +30,7 @@ function installRecordingFixture(tmp, name) {
30
30
  const logPath = join(tmp, `${name}.argv.log`);
31
31
  const fixturePath = join(tmp, name);
32
32
  writeFileSync(logPath, `${NO_SPAWN_SENTINEL}\n`);
33
- writeFileSync(fixturePath, `#!/bin/sh\necho "$@" > '${logPath}'\necho '{}'\nexit 0\n`);
33
+ writeFileSync(fixturePath, `#!/bin/sh\necho "$@" > '${logPath}'\necho '{"list":[]}'\nexit 0\n`);
34
34
  chmodSync(fixturePath, 0o755);
35
35
  return { path: fixturePath, logPath };
36
36
  }
@@ -0,0 +1,169 @@
1
+ /** Hotel retrieval must distinguish supplier/transport failures from an observed empty list.
2
+ * Isolated state and fixture CLIs only; no user profile, provider, or browser is accessed.
3
+ */
4
+ import assert from 'node:assert/strict';
5
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { apply } from '../src/index.js';
9
+ import { __resetEffectBreakersForTest, makeProductionInterpreter } from '../capabilities/effect.js';
10
+ import { callHbcliJson, searchHotels } from '../capabilities/hbcli.js';
11
+ import { sessionHotelSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
12
+ import { buildHotelEntryUrl } from '../capabilities/session/adapters/ctrip-hotel.js';
13
+ import { __setSessionBridgeForTest, createSessionBridge, EXTENSION_ORIGIN } from '../capabilities/session/extension-bridge.js';
14
+ const root = mkdtempSync(join(tmpdir(), 'gotry-hotel-result-'));
15
+ let failed = 0;
16
+ let passed = 0;
17
+ async function check(label, fn) {
18
+ try {
19
+ await fn();
20
+ passed++;
21
+ console.log(`ok - ${label}`);
22
+ }
23
+ catch (e) {
24
+ failed++;
25
+ console.error(`FAIL - ${label}: ${e.message}`);
26
+ }
27
+ }
28
+ function fixture(name, text) {
29
+ const path = join(root, name);
30
+ writeFileSync(path, `#!${process.execPath}\nprocess.stdout.write(${JSON.stringify(text)});\n`, { mode: 0o700 });
31
+ return path;
32
+ }
33
+ const query = { destination: '长春', checkIn: '2026-12-05', checkOut: '2026-12-08', adults: 2 };
34
+ const unavailable = fixture('no-suppliers', '{"code":300010002,"msg":"no available suppliers configured, please contact API seller manager"}');
35
+ try {
36
+ await check('Harbin hotel entry preserves the exact dates and occupancy', async () => {
37
+ const r = buildHotelEntryUrl({ to: '哈尔滨', checkIn: '2026-12-09', checkOut: '2026-12-11', adults: 2 });
38
+ assert.equal(r.ok, true);
39
+ assert.equal(r.url, 'https://hotels.ctrip.com/hotels/list?city=5&checkin=2026-12-09&checkout=2026-12-11&adult=2');
40
+ });
41
+ for (const claimed of [false, true]) {
42
+ await check(`login precheck timeout identifies ${claimed ? 'claimed' : 'queued'} phase`, async () => {
43
+ const created = await createSessionBridge({ ports: [0] });
44
+ assert.equal(created.ok, true);
45
+ if (!created.ok)
46
+ return;
47
+ const bridge = created.bridge;
48
+ try {
49
+ await fetch(`http://127.0.0.1:${bridge.port}/health`, { headers: { origin: EXTENSION_ORIGIN } });
50
+ const pending = bridge.submit({ kind: 'cookie-names', site: 'ctrip-hotel' }, { timeoutMs: 100, extensionWaitMs: 1000 });
51
+ if (claimed) {
52
+ const poll = await fetch(`http://127.0.0.1:${bridge.port}/jobs`, {
53
+ method: 'POST', headers: { origin: EXTENSION_ORIGIN, 'content-type': 'application/json' },
54
+ body: JSON.stringify({ capabilities: ['ctrip-hotel'] }),
55
+ });
56
+ assert.equal((await poll.json()).job.kind, 'cookie-names');
57
+ }
58
+ const result = await pending;
59
+ assert.equal(result.ok, false);
60
+ const failure = result;
61
+ assert.equal(failure.stage, claimed ? 'claimed' : 'queued');
62
+ assert.equal(failure.jobKind, 'cookie-names');
63
+ assert.match(failure.summary, /登录态检查/);
64
+ assert.doesNotMatch(failure.summary, /标签页无嗅探/);
65
+ }
66
+ finally {
67
+ await bridge.close();
68
+ }
69
+ });
70
+ }
71
+ await check('failed FlyAI flights cannot open the hotel circuit', async () => {
72
+ let hotelCalls = 0;
73
+ const interpret = makeProductionInterpreter({ breakers: new Map(), handlers: {
74
+ FLYAI_SEARCH: async (params) => {
75
+ const kind = params.kind;
76
+ if (kind === 'hotel') {
77
+ hotelCalls++;
78
+ return { ok: true, via: 'flyai', verdict: 'hit', hotels: [{ name: 'Fixture Hotel', price: 680 }] };
79
+ }
80
+ return { ok: false, via: 'flyai-error', verdict: 'error', retryable: false, upstreamHealth: 'unhealthy' };
81
+ },
82
+ } });
83
+ for (let i = 0; i < 3; i++)
84
+ await interpret({ effect: 'FLYAI_SEARCH', params: { kind: 'flight' } });
85
+ const blockedFlight = await interpret({ effect: 'FLYAI_SEARCH', params: { kind: 'flight' } });
86
+ assert.equal(blockedFlight.trace.declined, 'circuit-open');
87
+ const hotel = await interpret({ effect: 'FLYAI_SEARCH', params: { kind: 'hotel', destName: '长春', checkInDate: query.checkIn, checkOutDate: query.checkOut } });
88
+ assert.equal(hotel.trace.declined, undefined);
89
+ assert.equal(hotel.result?.verdict, 'hit');
90
+ assert.equal(hotelCalls, 1);
91
+ assert.equal((await interpret({ effect: 'FLYAI_SEARCH', params: { kind: 'flight' } })).trace.declined, 'circuit-open');
92
+ });
93
+ await check('exit 0 business error is not a realtime success', async () => {
94
+ const r = await callHbcliJson(['search', 'hotel-list', '--json'], { hbcliBin: unavailable });
95
+ assert.equal(r.exitCode, 0);
96
+ assert.equal(r.via, 'hbcli-error');
97
+ assert.match(r.error ?? '', /300010002/);
98
+ assert.match(r.evidence, /@error@/);
99
+ });
100
+ await check('supplier configuration error never says realtime zero inventory', async () => {
101
+ const r = await searchHotels(query, { hbcliBin: unavailable });
102
+ assert.equal(r.via, 'hbcli-error');
103
+ assert.match(r.summary, /300010002/);
104
+ assert.doesNotMatch(r.summary, /实时 0 家/);
105
+ assert.equal(r.hotels ?? null, null);
106
+ });
107
+ for (const [name, body] of [['garbage', 'not JSON'], ['missing-list', '{}'], ['bad-list', '{"list":{}}']]) {
108
+ await check(`${name} is an invalid observation, not empty inventory`, async () => {
109
+ const r = await searchHotels(query, { hbcliBin: fixture(name, body) });
110
+ assert.equal(r.via, 'hbcli-error');
111
+ assert.doesNotMatch(r.summary, /实时 0 家/);
112
+ });
113
+ }
114
+ await check('a recognized empty list remains a successful zero result', async () => {
115
+ const r = await searchHotels(query, { hbcliBin: fixture('empty', '{"list":[],"basic":{"sessionId":"fixture-session"}}') });
116
+ assert.equal(r.via, 'hbcli-realtime');
117
+ assert.match(r.summary, /实时 0 家/);
118
+ });
119
+ await check('registered hotel tool exposes failure and preserves error evidence', async () => {
120
+ __resetEffectBreakersForTest();
121
+ const tools = [];
122
+ const ctx = { tools: { register: (t) => tools.push(t) }, systemPrompt: { variable: () => () => '' }, on: () => () => { } };
123
+ apply(ctx, { stateRoot: root, timeoutMs: 1000, hbcliBin: unavailable, sessionAccess: 'off', benchmarkEnvironmentConfigPath: '' });
124
+ const tool = tools.find(t => t.name === 'gotry_hotel_search');
125
+ const r = await tool.execute(query, {});
126
+ assert.equal(r.ok, false);
127
+ assert.equal(r.verdict, 'error');
128
+ assert.match(r.evidence, /hbcli@error@/);
129
+ assert.doesNotMatch(r.evidence, /静态包/);
130
+ assert.doesNotMatch(JSON.stringify(tool.presentResult?.(query, r)), /无结果/);
131
+ });
132
+ // Use the actual session collector; replace only its external browser transport.
133
+ for (const [label, result, verdict] of [
134
+ ['sniff timeout', { ok: false, timeout: true, title: '酒店列表' }, 'error'],
135
+ ['invalid JSON', { ok: true, body: 'not JSON', title: '酒店列表' }, 'error'],
136
+ ['unrecognized JSON', { ok: true, body: '{"message":"upstream failed"}', title: '酒店列表' }, 'error'],
137
+ ['failed empty envelope', { ok: true, body: '{"code":500,"data":{"hotelList":[]}}', title: '酒店列表' }, 'error'],
138
+ ['failed Ctrip status', { ok: true, body: '{"ResponseStatus":{"Ack":"Failure"},"data":{"hotelList":[]}}', title: '酒店列表' }, 'error'],
139
+ ['invalid nonempty list', { ok: true, body: '{"data":{"hotelList":[{"unexpected":true}],"hotelMatchInfos":[]}}', title: '酒店列表' }, 'error'],
140
+ ['explicit empty list', { ok: true, body: '{"data":{"hotelList":[]}}', title: '酒店列表' }, 'miss'],
141
+ ['priced hotel', { ok: true, body: '{"data":{"hotelList":[{"hotelId":123,"hotelName":"Fixture Hotel","price":680}]}}', title: '酒店列表' }, 'hit'],
142
+ ['page challenge', { ok: false, timeout: true, challenge: true, title: '酒店列表' }, 'challenged'],
143
+ ]) {
144
+ await check(`Ctrip ${label} is classified without inventing empty inventory`, async () => {
145
+ __resetRateLimiterForTest();
146
+ const bridge = {
147
+ port: 0, extensionConnected: () => true, close: async () => { },
148
+ submit: async (job) => ({ ok: true, result: job.kind === 'cookie-names' ? { ok: true, names: ['cticket'] } : result }),
149
+ };
150
+ __setSessionBridgeForTest(bridge);
151
+ const r = await sessionHotelSearch({ to: '长春', cityId: 158, checkIn: query.checkIn, checkOut: query.checkOut });
152
+ assert.equal(r.verdict, verdict);
153
+ assert.equal(r.ok, verdict === 'hit' || verdict === 'miss');
154
+ if (verdict === 'error' || verdict === 'challenged')
155
+ assert.equal(r.hotels, undefined);
156
+ if (verdict === 'hit')
157
+ assert.equal(r.hotels?.[0]?.price, 680);
158
+ });
159
+ }
160
+ }
161
+ finally {
162
+ __setSessionBridgeForTest(null);
163
+ __resetRateLimiterForTest();
164
+ __resetEffectBreakersForTest();
165
+ rmSync(root, { recursive: true, force: true });
166
+ }
167
+ console.log(`hotel result tests: ${passed} passed, ${failed} failed`);
168
+ if (failed)
169
+ process.exitCode = 1;
@@ -302,7 +302,7 @@ function writeProductFixture(root, options = { writeTerminalOutput: false, mode:
302
302
  mkdirSync(presentDir, { recursive: true });
303
303
  writeFileSync(join(presentDir, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-tool-present', main: 'index.js' }));
304
304
  writeFileSync(join(presentDir, 'index.js'), 'module.exports = {}\n');
305
- for (const file of ['gotry-inner.js', 'gotry-bootstrap.js', 'gotry-flyai-verification.js', 'gotry-extension-status.js', 'gotry-runtime-resolution.js', 'gotry-process-liveness.js']) {
305
+ for (const file of ['gotry-inner.js', 'gotry-bootstrap.js', 'gotry-hbcli-release.js', 'gotry-flyai-verification.js', 'gotry-extension-status.js', 'gotry-runtime-resolution.js', 'gotry-process-liveness.js']) {
306
306
  copyFileSync(join(repoRoot, 'bin', file), join(binDir, file));
307
307
  }
308
308
  copyFileSync(join(repoRoot, 'ts', 'capabilities', 'incident-log.ts'), join(tsCapabilitiesDir, 'incident-log.ts'));