@danceiny/gotry 0.2.0-rc.30 → 0.2.0-rc.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +119 -136
- package/bin/gotry-extension-status.d.ts +12 -0
- package/bin/gotry-extension-status.js +113 -0
- package/bin/gotry-flyai-verification.d.ts +9 -0
- package/bin/gotry-flyai-verification.js +63 -0
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/bin/gotry-inner.js +2 -1
- package/client/client.js +228 -0
- package/dist/capabilities/doctor.js +14 -23
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +8 -1
- package/dist/capabilities/session/gui-flight.js +737 -0
- package/dist/capabilities/session/gui-hotel.js +413 -0
- package/dist/capabilities/session-search.js +98 -11
- package/dist/scripts/artifact-client-contract-tests.js +4 -3
- package/dist/scripts/artifact-delivery-tests.js +31 -0
- package/dist/scripts/bootstrap-tests.js +37 -10
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/doctor-tests.js +36 -7
- package/dist/scripts/dsh-artifact-web-e2e.js +204 -43
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +93 -5
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +119 -0
- package/dist/scripts/gotry-web-client-tests.js +47 -0
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +3 -1
- package/dist/scripts/session-tests.js +177 -6
- package/dist/src/artifact-delivery.js +26 -0
- package/dist/src/gotry-web-api.js +219 -0
- package/dist/src/index.js +45 -17
- package/extension/manifest.json +2 -2
- package/package.json +8 -2
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +9 -2
- package/ts/capabilities/session/gui-flight.ts +792 -0
- package/ts/capabilities/session/gui-hotel.ts +451 -0
- package/ts/capabilities/session-search.ts +115 -15
- package/ts/src/artifact-delivery.ts +25 -0
- package/ts/src/gotry-web-api.ts +189 -0
- package/ts/src/index.ts +46 -18
|
@@ -21,9 +21,14 @@ import { EventEmitter } from 'node:events';
|
|
|
21
21
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
23
|
import { join, dirname, delimiter } from 'node:path';
|
|
24
|
-
import {
|
|
24
|
+
import { pathToFileURL } from 'node:url';
|
|
25
|
+
import { runDoctorChecks as diagnose } from '../capabilities/doctor.js';
|
|
25
26
|
const repoRoot = join(import.meta.dirname, '..', '..');
|
|
26
27
|
const bootstrap = join(repoRoot, 'bin', 'gotry-bootstrap.js');
|
|
28
|
+
const offlinePreload = pathToFileURL(join(repoRoot, 'scripts', 'doctor-test-isolation.mjs')).href;
|
|
29
|
+
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --import=${offlinePreload}`.trim();
|
|
30
|
+
process.env.GOTRY_TEST_DOCTOR_PORT = '0';
|
|
31
|
+
const runDoctorChecks = (opts = {}) => diagnose({ ...opts, extensionPorts: [] });
|
|
27
32
|
function runBootstrap(extraArgs, extraEnv, bootstrapPath = bootstrap) {
|
|
28
33
|
try {
|
|
29
34
|
const out = execFileSync('node', [bootstrapPath, ...extraArgs], {
|
|
@@ -98,12 +103,22 @@ assert.ok(!c7b.out.includes('下载通道'), '非法值不应进入 github 通
|
|
|
98
103
|
console.log('7. 扩展分发 github 通道(不可达基址即时降级 + 非法值回落 bundled)OK');
|
|
99
104
|
// 8. doctor 子命令(2026-09-02 迪拜 session 复盘:可选依赖不撒手——doctor 统一
|
|
100
105
|
// 显示状态 + 精确补装指引;LLM key 显式让渡;报告落 gotry-state/doctor-report.md)
|
|
101
|
-
const
|
|
106
|
+
const doctorHome = mkdtempSync(join(tmpdir(), 'gotry-doctor-home-'));
|
|
107
|
+
let c8;
|
|
108
|
+
try {
|
|
109
|
+
c8 = runBootstrap(['doctor'], { HOME: doctorHome, FLYAI_API_KEY: '', DEBUG_FLYAI_API_KEY: '', STAICLI_HOME: join(doctorHome, '.staicli'), HOTELBYTE_TOKEN: '' });
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
rmSync(doctorHome, { recursive: true, force: true });
|
|
113
|
+
}
|
|
102
114
|
assert.ok(c8.out.includes('[gotry-doctor]'), '应输出 [gotry-doctor] 标签');
|
|
103
115
|
assert.ok(c8.out.includes('Agent Reach'), '体检应含 agent-reach 项');
|
|
104
116
|
assert.ok(c8.out.includes('hbcli'), '体检应含 hbcli 项');
|
|
105
117
|
assert.ok(c8.out.includes('FlyAI(飞猪官方检索'), '体检应含 flyai 项');
|
|
106
|
-
|
|
118
|
+
// FlyAI 面三合法态(doctor.ts #521:configured≠verified):匿名试用 / 已配置,未验证 / 已验证。
|
|
119
|
+
// 机器态依赖断言:本机配置并验证过 key 的环境(如 founder 机)doctor 输出「已验证 …」,
|
|
120
|
+
// 两态断言会把健康态误判为红(2026-10-14 发版门禁实测)。
|
|
121
|
+
assert.ok(c8.out.includes('匿名试用') || c8.out.includes('已配置,未验证') || c8.out.includes('已验证'), '体检应明确 FlyAI 当前状态(匿名试用/已配置,未验证/已验证)');
|
|
107
122
|
assert.ok(c8.out.includes('LLM key'), '体检应含 LLM key 让渡说明(doctor 不管 key)');
|
|
108
123
|
assert.ok(c8.out.includes('dsh-map-tools'), '体检应含 dsh-map-tools 项(#139;与 ts/capabilities/doctor.ts 两面成对)');
|
|
109
124
|
assert.ok(c8.out.includes('dsh-tool-ask-user'), '体检应含 dsh-tool-ask-user 项(与 ts/capabilities/doctor.ts 两面成对)');
|
|
@@ -156,7 +171,7 @@ console.log('9. calendar 子命令(setup 状态面 on/off/status + doctor 三态
|
|
|
156
171
|
assert.ok(!r.stdout.includes('[gotry-doctor]'), 'summary 模式零 header(stdout 静默)');
|
|
157
172
|
assert.match(r.stderr, /\[gotry\] doctor: \d+ 项待处理/, '隔离 HOME 有缺失项 → 一行摘要进 stderr');
|
|
158
173
|
assert.match(r.stderr, /gotry_doctor/, '摘要带对话内指路');
|
|
159
|
-
assert.match(r.stderr,
|
|
174
|
+
assert.match(r.stderr, /扩展=待确认/, '没有安装或连接证据时,摘要显示待确认');
|
|
160
175
|
}
|
|
161
176
|
console.log('10. 启动一次性 doctor 摘要(--summary:stderr 一行/零写盘/恒 exit 0)OK');
|
|
162
177
|
// 11. setupSidebar 落盘状态复核(2026-09-08 安装链修复):pnpm ≥10.5 在 profile 目录
|
|
@@ -388,6 +403,10 @@ console.log('18. onboarding CLI 跳过(非 TTY / CI / GOTRY_SETUP_SKIP / GOTRY_O
|
|
|
388
403
|
const fixture = mkdtempSync(join(tmpdir(), 'gotry-onboard-pkg-'));
|
|
389
404
|
mkdirSync(join(fixture, 'bin'), { recursive: true });
|
|
390
405
|
copyFileSync(bootstrap, join(fixture, 'bin', 'gotry-bootstrap.js'));
|
|
406
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(fixture, 'bin', 'gotry-flyai-verification.js'));
|
|
407
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(fixture, 'bin', 'gotry-process-liveness.js'));
|
|
408
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(fixture, 'bin', 'gotry-extension-status.js'));
|
|
409
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-hbcli-release.js'), join(fixture, 'bin', 'gotry-hbcli-release.js'));
|
|
391
410
|
copyFileSync(join(repoRoot, 'package.json'), join(fixture, 'package.json'));
|
|
392
411
|
const vendorDir = join(fixture, 'ts', 'dsh-runtime', 'vendor');
|
|
393
412
|
mkdirSync(vendorDir, { recursive: true });
|
|
@@ -637,6 +656,9 @@ function buildPkgFixture(tmpRoot) {
|
|
|
637
656
|
writeFileSync(join(pkgRoot, 'package.json'), `${JSON.stringify({ name: 'gotry', version: '0.0.1-test', type: 'module' })}\n`);
|
|
638
657
|
copyFileSync(join(repoRoot, 'bin', 'gotry-inner.js'), join(pkgBin, 'gotry-inner.js'));
|
|
639
658
|
copyFileSync(join(repoRoot, 'bin', 'gotry-bootstrap.js'), join(pkgBin, 'gotry-bootstrap.js'));
|
|
659
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(pkgBin, 'gotry-flyai-verification.js'));
|
|
660
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(pkgBin, 'gotry-extension-status.js'));
|
|
661
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-hbcli-release.js'), join(pkgBin, 'gotry-hbcli-release.js'));
|
|
640
662
|
copyFileSync(join(repoRoot, 'bin', 'gotry-runtime-resolution.js'), join(pkgBin, 'gotry-runtime-resolution.js'));
|
|
641
663
|
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(pkgBin, 'gotry-process-liveness.js'));
|
|
642
664
|
copyFileSync(join(repoRoot, 'cordis.gotry-patch.yml'), join(pkgRoot, 'cordis.gotry-patch.yml'));
|
|
@@ -699,8 +721,8 @@ echo "bash-finished $$" >> "$GOTRY_TEST_INSTALLER_MARKER"
|
|
|
699
721
|
}
|
|
700
722
|
const closedFixtures = new WeakSet();
|
|
701
723
|
function spawnInner(fixture, opts) {
|
|
702
|
-
//
|
|
703
|
-
const nodeOptions = opts.tty ?
|
|
724
|
+
// 保留离线诊断 preload,但移除父进程 tsx loader;临时包没有 tsx。
|
|
725
|
+
const nodeOptions = `--import=${offlinePreload}${opts.tty ? ` --require ${fixture.preloadPath}` : ''}`;
|
|
704
726
|
const child = spawn(process.execPath, [join(fixture.pkgBin, 'gotry-inner.js'), 'web', '--no-open'], {
|
|
705
727
|
env: {
|
|
706
728
|
...process.env,
|
|
@@ -1498,8 +1520,9 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1498
1520
|
assert.deepEqual(unit('not json'), { ok: true, customer: false }, '非 JSON(超旧版/异常)保守视为有效,与工具层同语义');
|
|
1499
1521
|
assert.deepEqual(unit(''), { ok: true, customer: false }, '空输出同属不可解析,保守视为有效');
|
|
1500
1522
|
assert.deepEqual(unit(null), { ok: false, customer: false }, '取不到输出(非零退出/超时/spawn 失败)→ 未确认');
|
|
1501
|
-
const fakeHbcli = (whoamiArm) => `#!/bin/sh\ncase "$*" in\n *--version*) echo '
|
|
1523
|
+
const fakeHbcli = (whoamiArm, version = '0.0.4') => `#!/bin/sh\ncase "$*" in\n *--version*) echo '${version}';;\n *version*) echo '${version}';;\n *whoami*) ${whoamiArm};;\nesac\n`;
|
|
1502
1524
|
const cases = [
|
|
1525
|
+
{ name: 'old-version', version: '0.0.3', arm: `echo '${allFalse.replace('"api_key":{"configured":false}', '"api_key":{"configured":true}')}'`, expect: 'missing' },
|
|
1503
1526
|
{ name: 'allfalse', arm: `echo '${allFalse}'`, expect: 'degraded' },
|
|
1504
1527
|
{ name: 'customer', arm: `echo '${allFalse.replace('"customer":{"configured":false}', '"customer":{"configured":true}')}'`, expect: 'ok', customer: true },
|
|
1505
1528
|
{ name: 'apikey', arm: `echo '${allFalse.replace('"api_key":{"configured":false}', '"api_key":{"configured":true}')}'`, expect: 'ok' },
|
|
@@ -1517,12 +1540,12 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1517
1540
|
mkdirSync(binDir);
|
|
1518
1541
|
mkdirSync(home);
|
|
1519
1542
|
symlinkSync(process.execPath, join(binDir, 'node'));
|
|
1520
|
-
writeFileSync(join(binDir, 'hbcli'), fakeHbcli(c.arm), { mode: 0o755 });
|
|
1543
|
+
writeFileSync(join(binDir, 'hbcli'), fakeHbcli(c.arm, c.version), { mode: 0o755 });
|
|
1521
1544
|
// CLI 面
|
|
1522
1545
|
const cli = runBootstrap(['doctor'], { HOME: home, PATH: binDir, FLYAI_API_KEY: '' });
|
|
1523
1546
|
const line = cli.out.split('\n').find((l) => l.includes('hbcli(酒店实时源)'));
|
|
1524
1547
|
assert.ok(line, `${c.name}: CLI doctor 应输出 hbcli 行\n${cli.out}`);
|
|
1525
|
-
const cliLevel = line.includes('已安装且凭证有效') ? 'ok' : line.includes('二进制在,但凭证未配置/失效') ? 'degraded' : 'other';
|
|
1548
|
+
const cliLevel = line.includes('已安装且凭证有效') ? 'ok' : line.includes('二进制在,但凭证未配置/失效') ? 'degraded' : line.includes('版本过旧') ? 'missing' : 'other';
|
|
1526
1549
|
assert.equal(cliLevel, c.expect, `${c.name}: CLI doctor 的 hbcli 结论应为 ${c.expect}\n${line}`);
|
|
1527
1550
|
if (c.customer)
|
|
1528
1551
|
assert.ok(line.includes('customer 档在用'), `${c.name}: CLI 应标注 customer 档在用\n${line}`);
|
|
@@ -1532,6 +1555,10 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1532
1555
|
process.env.PATH = prevPath;
|
|
1533
1556
|
assert.equal(tool.status, c.expect, `${c.name}: 工具层 doctor 的 hbcli 结论应为 ${c.expect},实际 ${tool.status}`);
|
|
1534
1557
|
assert.equal(cliLevel, tool.status, `${c.name}: CLI 与工具层必须同结论(两份实现不得漂移)`);
|
|
1558
|
+
if (c.expect === 'missing') {
|
|
1559
|
+
assert.match(cli.out, /staicli-v0\.0\.4\/install\.sh/, 'CLI 应提供当前官方版本的安装入口');
|
|
1560
|
+
assert.match(tool.fix ?? '', /staicli-v0\.0\.4\/install\.sh/, '工具层应提供相同升级入口');
|
|
1561
|
+
}
|
|
1535
1562
|
}
|
|
1536
1563
|
finally {
|
|
1537
1564
|
process.env.PATH = prevPath;
|
|
@@ -1550,5 +1577,5 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1550
1577
|
catch { /* ignore */ }
|
|
1551
1578
|
}
|
|
1552
1579
|
}
|
|
1553
|
-
console.log('24. hbcli
|
|
1580
|
+
console.log('24. hbcli 版本与凭证判定(CLI↔工具层 6 夹具同结论:旧版=missing / 全 false=degraded / customer·api_key=ok / 非 JSON=保守 ok / 非零退出=degraded)OK');
|
|
1554
1581
|
console.log('BOOTSTRAP TESTS: 26/26 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / doctor 体检面 / calendar setup 状态面 / 显式跳过 + auto 跳过 + 单项跳过 / 启动摘要 / sidebar 落盘状态复核 / onboarding 分类+计划+env opt-out+跳过原因+yes+partial-failure+幂等+prompt+CLI 跳过+--scan / 编排缝 orchestrateWebLaunch × 6 / prompt waiter failure deterministic process-group reap / waiter 原始错误 + cleanup timeout/failure handled / 临时安装包 spawned inner 21a TTY-eligible 真实 prompt→n→web + 摘要抑制 + 无残留 / 21b non-TTY 零 prompt / 21c POSIX 信号清理 SIGTERM→清 result+patch 目录 + 无残留子进程 / 21f yes-path installer 子树信号清理 / 21g yes-path timeout installer 子树清理 / 21d timeout 诊断 + 无残留 / 21e dsh-lifecycle 信号转发 / win32 平台边界 / result 通道排他写入 / hbcli 凭证判定 CLI↔工具层对拍)');
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Supplier metadata fixtures; no vendor requests, credentials or browser session. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseCtripHotelCities, resolveCtripHotelCity, parseCtripCityCatalog, parseCtripCitySuggestions, matchCtripCity, resolveCtripFlightCity } from '../capabilities/session/ctrip-city.js';
|
|
4
|
+
import { resolveFlightEntryUrl } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
5
|
+
const catalog = { ResponseStatus: { Ack: 'Success' }, responseHead: { errorCode: '0' }, recommendGroupList: [{ indexedCity: { cityList: [
|
|
6
|
+
{ id: 2, code: 'SHA', name: '上海', eName: 'Shanghai', airportlst: [{ code: 'PVG', name: '上海浦东国际机场' }] },
|
|
7
|
+
{ id: 58, code: 'HKG', name: '中国香港', nameEn: 'Hong Kong' },
|
|
8
|
+
] }, geographicalRegionList: [{ hotCityList: [{ cityList: [{ id: 220, code: 'DXB', name: '迪拜', eName: 'Dubai', specName: '迪拜(阿联酋)', sameSpecCity: 1 }] }] }] }] };
|
|
9
|
+
const cities = parseCtripCityCatalog(catalog);
|
|
10
|
+
for (const [name, code] of [['迪拜(阿联酋)', 'DXB'], ['DXB', 'DXB'], ['香港', 'HKG'], ['Hong Kong', 'HKG'], ['Shanghai', 'SHA'], ['上海浦东国际机场', 'PVG']]) {
|
|
11
|
+
const r = matchCtripCity(name, cities);
|
|
12
|
+
assert.ok(r.ok, name);
|
|
13
|
+
assert.equal(r.city.code, code);
|
|
14
|
+
}
|
|
15
|
+
assert.equal(matchCtripCity('Dubai', cities).ok, false, 'supplier-marked same-name cities require qualification');
|
|
16
|
+
assert.equal(matchCtripCity('迪', cities).ok, false, 'no prefix or nearby-city substitution');
|
|
17
|
+
assert.equal(matchCtripCity('同名城', [{ name: '同名城', code: 'AAA', aliases: ['同名城'], context: '甲国' }, { name: '同名城', code: 'BBB', aliases: ['同名城'], context: '乙国' }]).ok, false);
|
|
18
|
+
assert.throws(() => parseCtripCityCatalog({ ...catalog, responseHead: { errorCode: '1' } }));
|
|
19
|
+
const suggestions = { Data: [{ Type: 4, Name: '未缓存城市', Code: 'NEW', EName: 'New City', Country: '示例国', Nearby: [{ Type: 4, Name: '邻近城市', Code: 'NEA' }] }] };
|
|
20
|
+
assert.equal(parseCtripCitySuggestions(suggestions).length, 1);
|
|
21
|
+
let requests = [];
|
|
22
|
+
const fakeFetch = async (url) => {
|
|
23
|
+
requests.push(String(url));
|
|
24
|
+
return Response.json(requests.length === 1 ? catalog : suggestions);
|
|
25
|
+
};
|
|
26
|
+
const resolved = await resolveCtripFlightCity('未缓存城市', fakeFetch);
|
|
27
|
+
assert.ok(resolved.ok);
|
|
28
|
+
assert.equal(resolved.city.code, 'NEW');
|
|
29
|
+
assert.equal(requests.length, 2, 'catalog miss queries supplier autocomplete');
|
|
30
|
+
const blocked = await resolveCtripFlightCity('未知城市', async () => new Response('whaleguard block'));
|
|
31
|
+
assert.ok(!blocked.ok && blocked.reason === 'unavailable', 'challenge is unavailable, not unsupported/no inventory');
|
|
32
|
+
const oversized = await resolveCtripFlightCity('未知城市', async () => new Response('x'.repeat(512001)));
|
|
33
|
+
assert.ok(!oversized.ok && oversized.reason === 'unavailable');
|
|
34
|
+
const entry = await resolveFlightEntryUrl('迪拜(阿联酋)', '香港', '2026-12-01', {}, async (q) => matchCtripCity(q, cities));
|
|
35
|
+
assert.ok(entry.ok && entry.url?.includes('oneway-dxb-hkg'));
|
|
36
|
+
let lookups = 0;
|
|
37
|
+
assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', 'bad-date', {}, async () => { lookups++; throw Error('should not run'); })).ok);
|
|
38
|
+
assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', '2026-12-01', { fromCode: '../dxb' }, async () => { lookups++; throw Error('should not run'); })).ok);
|
|
39
|
+
assert.equal(lookups, 0);
|
|
40
|
+
console.log('Ctrip city metadata: international names/airports, ambiguity, autocomplete fallback, challenge/bounds and exact URL PASS');
|
|
41
|
+
const hotelRecord = (id, name, english, country) => ({ keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: String(id), displayTexts: [{ key: 'MAIN_TITLE', value: english }] } }, controlInfo: { regionInfo: { basicCityModel: { cityId: id }, displayCityModel: { cityName: name, countryName: country, destinationNameEN: english } } } });
|
|
42
|
+
const hotels = { ResponseStatus: { Ack: 'Success' }, data: { mainKeywordList: { keywords: [hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), hotelRecord(596578, '迪拜', 'Dubai', '印度')] } } };
|
|
43
|
+
const hotelFetch = async () => Response.json(hotels);
|
|
44
|
+
const ambiguous = await resolveCtripHotelCity('迪拜', undefined, hotelFetch);
|
|
45
|
+
assert.ok(!ambiguous.ok && ambiguous.reason === 'ambiguous');
|
|
46
|
+
assert.equal(ambiguous.candidates.length, 2);
|
|
47
|
+
const qualified = await resolveCtripHotelCity('迪拜', '阿联酋', hotelFetch);
|
|
48
|
+
assert.ok(qualified.ok && qualified.city.id === '220');
|
|
49
|
+
const rejected = await resolveCtripHotelCity('迪拜', undefined, async () => Response.json({ ...hotels, data: { ...hotels.data, htlSpiderActionErrorCode: 'captcha' } }));
|
|
50
|
+
assert.ok(!rejected.ok && rejected.reason === 'unavailable');
|
|
51
|
+
assert.throws(() => parseCtripHotelCities({ ...hotels, data: { mainKeywordList: { keywords: [{ ...hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: '58' } } }] } } }));
|
|
52
|
+
console.log('Hotel destination namespace: same-name city ambiguity, country confirmation, challenge and ID conflict PASS');
|
|
53
|
+
// Exercise the real registered schema and tool dispatch, using an isolated state root.
|
|
54
|
+
const { apply } = await import('../src/index.js');
|
|
55
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
56
|
+
const { tmpdir } = await import('node:os');
|
|
57
|
+
const { join } = await import('node:path');
|
|
58
|
+
const root = mkdtempSync(join(tmpdir(), 'gotry-city-tool-'));
|
|
59
|
+
const tools = [];
|
|
60
|
+
const effects = [];
|
|
61
|
+
try {
|
|
62
|
+
apply({ tools: { register: (t) => tools.push(t) }, systemPrompt: { variable: () => { } }, get: () => undefined, on: () => () => { } }, { stateRoot: root, timeoutMs: 1000, hbcliBin: 'unused', sessionAccess: 'allow' }, { effect: (async (fx) => { effects.push(fx); return { result: { ok: false, verdict: 'error', error: 'fixture only', evidence: '[fixture:city-tool]' }, trace: { effect: fx.effect, channel: 'browser', attempts: 1, backoffMs: 0, breaker: 'off', evidence: [] } }; }) });
|
|
63
|
+
const session = tools.find(t => t.name === 'gotry_session_search');
|
|
64
|
+
const hotel = tools.find(t => t.name === 'gotry_hotel_search');
|
|
65
|
+
const parameters = session.parameters?.properties ?? session.parameters;
|
|
66
|
+
assert.ok(parameters.country && parameters.fromCityCode && parameters.toCityCode, 'model must see disambiguation parameters on session search');
|
|
67
|
+
const hotelParameters = hotel.parameters?.properties ?? hotel.parameters;
|
|
68
|
+
assert.ok(!hotelParameters.country, 'HBCLI hotel tool must not advertise an ignored session parameter');
|
|
69
|
+
await session.execute({ kind: 'hotel', to: '迪拜', country: '阿联酋' }, {});
|
|
70
|
+
assert.equal(effects.at(-1).effect, 'SESSION_HOTEL_SEARCH');
|
|
71
|
+
assert.equal(effects.at(-1).params.country, '阿联酋');
|
|
72
|
+
await session.execute({ kind: 'flight', from: '迪拜', to: '香港', date: '2099-12-01', fromCityCode: 'DXB', toCityCode: 'HKG' }, {});
|
|
73
|
+
assert.equal(effects.at(-1).effect, 'SESSION_FLIGHT_SEARCH');
|
|
74
|
+
assert.equal(effects.at(-1).params.fromCityCode, 'DXB');
|
|
75
|
+
assert.equal(effects.at(-1).params.toCityCode, 'HKG');
|
|
76
|
+
console.log('Registered session tool: country/code schema and effect dispatch PASS');
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* doctor 能力层测试(离线,注入 repoRoot/homeDir/env,零安装零网络写路径):
|
|
3
|
-
* 1. 空 tmp 环境 → agent-reach=missing / 扩展=
|
|
3
|
+
* 1. 空 tmp 环境 → agent-reach=missing / 扩展=degraded(连接未确认) / flyai=degraded(无 key)/ LLM key 恒 ok 且不进 broken
|
|
4
4
|
* 2. 补齐假 .venv 双文件 → agent-reach=ok;只补 python → degraded(半可用态被显式区分)
|
|
5
5
|
* 3. FLYAI_API_KEY 仅注入 → flyai=degraded(未验证);匹配验证回执 → flyai=ok
|
|
6
6
|
* 4. nodeOk 边界(22.14/22.15/23.0)
|
|
@@ -25,9 +25,13 @@ import { existsSync } from 'node:fs';
|
|
|
25
25
|
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
26
26
|
import { tmpdir } from 'node:os';
|
|
27
27
|
import { dirname, join } from 'node:path';
|
|
28
|
-
import { runDoctorChecks, renderDoctorReportMd, nodeOk, probeStdout, scopeKeyFor, createRepairApprovalGate, runDoctorRepair } from '../capabilities/doctor.js';
|
|
28
|
+
import { runDoctorChecks as diagnose, renderDoctorReportMd, nodeOk, probeStdout, scopeKeyFor, createRepairApprovalGate, runDoctorRepair } from '../capabilities/doctor.js';
|
|
29
29
|
import { endpointFingerprint, maskFlyaiKey, resolveFlyaiEndpoint, sha256Hex, writeFlyaiVerification } from '../capabilities/flyai-config.js';
|
|
30
30
|
import { apply } from '../src/index.js';
|
|
31
|
+
const runDoctorChecks = (opts = {}) => diagnose({ ...opts, extensionPorts: [] });
|
|
32
|
+
// The registered model tool calls diagnose internally rather than this wrapper.
|
|
33
|
+
// Isolate its HTTP diagnostic seam too, without changing the product config.
|
|
34
|
+
await import(new URL('../../scripts/doctor-test-isolation.mjs', import.meta.url).href);
|
|
31
35
|
const tmp = await mkdtemp(join(tmpdir(), 'gotry-doctor-test-'));
|
|
32
36
|
const emptyRepo = join(tmp, 'repo-empty');
|
|
33
37
|
const emptyHome = join(tmp, 'home-empty');
|
|
@@ -39,11 +43,10 @@ const byId = (id) => r1.items.find(i => i.id === id);
|
|
|
39
43
|
assert.ok(byId('agent-reach'), 'agent-reach 项存在');
|
|
40
44
|
assert.equal(byId('agent-reach').status, 'missing', `空仓 agent-reach=missing,实际 ${byId('agent-reach').status}`);
|
|
41
45
|
assert.match(byId('agent-reach').fix ?? '', /npx @danceiny\/gotry doctor --fix/, 'missing 项带精确补装指引');
|
|
42
|
-
assert.equal(byId('extension').status, '
|
|
43
|
-
|
|
44
|
-
assert.match(byId('extension').
|
|
45
|
-
assert.match(byId('extension').detail,
|
|
46
|
-
assert.match(byId('extension').detail, /其它工具/, '扩展 missing detail 必显「其它不受影响」');
|
|
46
|
+
assert.equal(byId('extension').status, 'degraded', '空 home 无法断言扩展未安装');
|
|
47
|
+
assert.match(byId('extension').detail, /影响面/, '扩展 detail 必说明影响面');
|
|
48
|
+
assert.match(byId('extension').fix ?? '', /chrome:\/\/extensions/, '扩展未确认时可检查浏览器安装/启用状态');
|
|
49
|
+
assert.match(byId('extension').detail, /其它工具/, '扩展 detail 必说明其它工具不受影响');
|
|
47
50
|
assert.equal(byId('flyai').status, 'degraded', '无 key flyai=degraded(有共享试用,非缺失)');
|
|
48
51
|
// #559 B 步:无 key flyai 必须显式说明「匿名共享额度」影响面
|
|
49
52
|
assert.match(byId('flyai').detail, /影响面/, 'flyai 无 key detail 必带影响面行(issue #559 B 步)');
|
|
@@ -155,6 +158,32 @@ console.log('5b. npm 提升布局解析链(map-tools/ask-user 误报修复)OK');
|
|
|
155
158
|
await rm(vend, { recursive: true, force: true });
|
|
156
159
|
}
|
|
157
160
|
console.log('5c. 随包 vendor 布局(map-tools tarball 分发面)OK');
|
|
161
|
+
// #651: the model-facing diagnostic must recognize the same Chrome Store
|
|
162
|
+
// installation as the CLI without requiring the local unpacked directory.
|
|
163
|
+
{
|
|
164
|
+
const home = join(tmp, 'chrome-store-home');
|
|
165
|
+
const browserRoot = process.platform === 'darwin'
|
|
166
|
+
? join(home, 'Library', 'Application Support', 'Google', 'Chrome')
|
|
167
|
+
: process.platform === 'win32'
|
|
168
|
+
? join(home, 'AppData', 'Local', 'Google', 'Chrome', 'User Data')
|
|
169
|
+
: join(home, '.config', 'google-chrome');
|
|
170
|
+
const profile = join(browserRoot, 'Profile 3');
|
|
171
|
+
const id = 'oeajpiccmonococjcegddlooeeohlbgd';
|
|
172
|
+
const manifest = join(profile, 'Extensions', id, '0.2.0.27_0', 'manifest.json');
|
|
173
|
+
await mkdir(dirname(manifest), { recursive: true });
|
|
174
|
+
await writeFile(manifest, JSON.stringify({ manifest_version: 3, version: '0.2.0.27', name: 'Stai Travel Bridge' }));
|
|
175
|
+
const prefs = join(profile, 'Secure Preferences');
|
|
176
|
+
await writeFile(prefs, JSON.stringify({ extensions: { settings: { [id]: { disable_reasons: [] } } } }));
|
|
177
|
+
const report = await runDoctorChecks({ repoRoot: emptyRepo, homeDir: home, env: {} });
|
|
178
|
+
const extension = report.items.find(item => item.id === 'extension');
|
|
179
|
+
assert.equal(extension.status, 'degraded');
|
|
180
|
+
assert.match(extension.detail, /已安装/);
|
|
181
|
+
assert.match(report.summary, /Stai Travel Bridge 扩展\(待确认\)/);
|
|
182
|
+
await writeFile(prefs, JSON.stringify({ extensions: { settings: { [id]: { state: 0 } } } }));
|
|
183
|
+
const disabled = (await runDoctorChecks({ repoRoot: emptyRepo, homeDir: home, env: {} })).items.find(item => item.id === 'extension');
|
|
184
|
+
assert.match(disabled.detail, /已安装.*停用/);
|
|
185
|
+
}
|
|
186
|
+
console.log('5d. Chrome 商店安装与停用状态,工具层不误报 missing/ready OK');
|
|
158
187
|
// 6. MCP 工具面:gotry_doctor 注册 + isolated stateRoot 报告落盘
|
|
159
188
|
const smokeRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-state-'));
|
|
160
189
|
const registered = [];
|