@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.16

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 (139) hide show
  1. package/README.md +10 -6
  2. package/README.zh-CN.md +10 -6
  3. package/bin/gotry-bootstrap.js +294 -5
  4. package/cordis.gotry-patch.yml +10 -0
  5. package/data/airline-airports.json +39 -0
  6. package/dist/capabilities/agent-reach-deep.js +1 -1
  7. package/dist/capabilities/agent-reach.js +1 -1
  8. package/dist/capabilities/anything.js +1 -1
  9. package/dist/capabilities/artifacts.js +1 -1
  10. package/dist/capabilities/effect.js +293 -0
  11. package/dist/capabilities/fact-log.js +41 -0
  12. package/dist/capabilities/flyai.js +1 -1
  13. package/dist/capabilities/hbcli.js +43 -1
  14. package/dist/capabilities/incident-log.js +1 -1
  15. package/dist/capabilities/opensky.js +1 -1
  16. package/dist/capabilities/resilience.js +90 -0
  17. package/dist/capabilities/session/action-cache.js +1 -1
  18. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  19. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  20. package/dist/capabilities/session/benchmark.js +2 -1
  21. package/dist/capabilities/session/extension-bridge.js +345 -0
  22. package/dist/capabilities/session/extension-channel.js +93 -0
  23. package/dist/capabilities/session/extract.js +1 -1
  24. package/dist/capabilities/session/health-watch.js +216 -0
  25. package/dist/capabilities/session/read-guard.js +1 -1
  26. package/dist/capabilities/session/transport.js +1 -1
  27. package/dist/capabilities/session/wizard.js +377 -0
  28. package/dist/capabilities/session-consent.js +1 -1
  29. package/dist/capabilities/session-login.js +77 -1
  30. package/dist/capabilities/session-search.js +69 -2
  31. package/dist/capabilities/weather.js +1 -1
  32. package/dist/scripts/action-cache-tests.js +1 -1
  33. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  34. package/dist/scripts/agent-reach-tests.js +1 -1
  35. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  36. package/dist/scripts/anything-tests.js +1 -1
  37. package/dist/scripts/async-collect.js +1 -1
  38. package/dist/scripts/booking-saga-tests.js +1 -1
  39. package/dist/scripts/bootstrap-tests.js +39 -3
  40. package/dist/scripts/build-changelog.js +279 -0
  41. package/dist/scripts/changelog-tests.js +165 -0
  42. package/dist/scripts/companion-tests.js +1 -1
  43. package/dist/scripts/diff-test.js +1 -1
  44. package/dist/scripts/effect-tests.js +474 -0
  45. package/dist/scripts/engine-run.js +1 -1
  46. package/dist/scripts/engine-tests.js +1 -1
  47. package/dist/scripts/extension-tests.js +468 -0
  48. package/dist/scripts/fact-gate-tests.js +318 -0
  49. package/dist/scripts/flyai-tests.js +1 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +180 -0
  51. package/dist/scripts/hbcli-tests.js +1 -1
  52. package/dist/scripts/health-watch-cli.js +51 -0
  53. package/dist/scripts/i18n-tests.js +1 -1
  54. package/dist/scripts/incident-tests.js +1 -1
  55. package/dist/scripts/journey-tests.js +1 -1
  56. package/dist/scripts/ledger-tests.js +1 -1
  57. package/dist/scripts/ledger-workflow-crash.js +1 -1
  58. package/dist/scripts/memory-capture-tests.js +1 -1
  59. package/dist/scripts/memory-decay-tests.js +1 -1
  60. package/dist/scripts/memory-metrics.js +1 -1
  61. package/dist/scripts/memory-value-report.js +1 -1
  62. package/dist/scripts/nightly-evidence-tests.js +67 -4
  63. package/dist/scripts/nightly-evidence.js +59 -3
  64. package/dist/scripts/nudge-digest.js +1 -1
  65. package/dist/scripts/onboarding-tests.js +218 -0
  66. package/dist/scripts/opensky-check.js +1 -1
  67. package/dist/scripts/opensky-tests.js +1 -1
  68. package/dist/scripts/price-drift-tests.js +441 -0
  69. package/dist/scripts/price-drift-watch.js +491 -0
  70. package/dist/scripts/probe-poi-tests.js +1 -1
  71. package/dist/scripts/product-metrics.js +1 -1
  72. package/dist/scripts/publish-preverify.js +1 -1
  73. package/dist/scripts/realtime-pricing-tests.js +1 -1
  74. package/dist/scripts/replay-async.js +1 -1
  75. package/dist/scripts/replay-real.js +1 -1
  76. package/dist/scripts/replay.js +1 -1
  77. package/dist/scripts/session-attach-diagnose.js +1 -1
  78. package/dist/scripts/session-attach-poc.js +1 -1
  79. package/dist/scripts/session-benchmark.js +1 -1
  80. package/dist/scripts/session-extract-tests.js +1 -1
  81. package/dist/scripts/session-login.js +1 -1
  82. package/dist/scripts/session-tests.js +1 -1
  83. package/dist/scripts/skeleton-check.js +1 -1
  84. package/dist/scripts/skeleton-integration-test.js +1 -1
  85. package/dist/scripts/skills-contract-tests.js +1 -1
  86. package/dist/scripts/smoke.js +76 -1
  87. package/dist/scripts/state-cli-tests.js +1 -1
  88. package/dist/scripts/state-cli.js +1 -1
  89. package/dist/scripts/time-eval-tests.js +1 -1
  90. package/dist/scripts/travel-timeline-tests.js +1 -1
  91. package/dist/scripts/unified-tests.js +1 -1
  92. package/dist/scripts/weather-tests.js +1 -1
  93. package/dist/scripts/wizard-bootstrap.js +32 -0
  94. package/dist/scripts/z3-race-tests.js +1 -1
  95. package/dist/src/artifact-gate.js +310 -0
  96. package/dist/src/bookable-facts.js +349 -0
  97. package/dist/src/booking-saga.js +1 -1
  98. package/dist/src/bridge.js +1 -1
  99. package/dist/src/companions.js +1 -1
  100. package/dist/src/contracts.js +1 -1
  101. package/dist/src/dsh-llm.js +1 -1
  102. package/dist/src/engine.js +1 -1
  103. package/dist/src/i18n.js +1 -1
  104. package/dist/src/index.js +189 -46
  105. package/dist/src/journey.js +1 -1
  106. package/dist/src/loop.js +1 -1
  107. package/dist/src/memory-capture.js +1 -1
  108. package/dist/src/memory-decay.js +1 -1
  109. package/dist/src/memory-utility.js +1 -1
  110. package/dist/src/mock-llm.js +1 -1
  111. package/dist/src/model.js +1 -1
  112. package/dist/src/realtime-pricing.js +14 -7
  113. package/dist/src/slot-spec.js +1 -1
  114. package/dist/src/state-ledger.js +1 -1
  115. package/dist/src/time-anchor.js +1 -1
  116. package/dist/src/tool-packet.js +1 -1
  117. package/dist/src/travel-slots.js +1 -1
  118. package/dist/src/travel-timeline.js +1 -1
  119. package/dist/src/unified.js +1 -1
  120. package/dist/src/wish-pool.js +1 -1
  121. package/dist/src/z3-shared.js +1 -1
  122. package/extension/README.md +26 -0
  123. package/extension/background.js +160 -0
  124. package/extension/content-bridge.js +41 -0
  125. package/extension/content-main.js +62 -0
  126. package/extension/manifest.json +32 -0
  127. package/package.json +6 -2
  128. package/ts/capabilities/fact-log.ts +54 -0
  129. package/ts/capabilities/hbcli.ts +46 -0
  130. package/ts/capabilities/session/benchmark.ts +4 -1
  131. package/ts/capabilities/session/extension-bridge.ts +354 -0
  132. package/ts/capabilities/session/extension-channel.ts +92 -0
  133. package/ts/capabilities/session/health-watch.ts +257 -0
  134. package/ts/capabilities/session/wizard.ts +282 -0
  135. package/ts/capabilities/session-login.ts +53 -2
  136. package/ts/capabilities/session-search.ts +68 -3
  137. package/ts/src/artifact-gate.ts +365 -0
  138. package/ts/src/bookable-facts.ts +557 -0
  139. package/ts/src/index.ts +102 -8
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { existsSync, mkdtempSync, rmSync } from 'node:fs';
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { buildNightlyRunRecord, loadPriceTable, loadPromptSet, priceRunCost, runNightlyEvidence } from './nightly-evidence.js';
@@ -11,10 +11,15 @@ function ok(label) {
11
11
  }
12
12
  const priceTable = loadPriceTable('data/llm-price-table.json');
13
13
  const promptSet = loadPromptSet('data/m3-nightly-prompts.json');
14
- assert.equal(priceTable.schema_version, 'gotry_llm_price_table_v1');
14
+ assert.equal(priceTable.schema_version, 'gotry_llm_price_table_v2');
15
15
  assert.equal(promptSet.schema_version, 'gotry_m3_nightly_prompt_set_v1');
16
16
  assert.ok(promptSet.turns.length >= 1);
17
- ok('封存数据文件 schema 与轮次');
17
+ assert.ok(priceTable.prices['deepseek-v4-flash'], '裸键 deepseek-v4-flash 必须可查');
18
+ assert.ok(priceTable.prices['MiniMax-M3'], '裸键 MiniMax-M3 必须可查(provider 为单一时透明)');
19
+ assert.ok(priceTable.prices['minimax:MiniMax-M3'], 'qualified 键 minimax:MiniMax-M3 必须可查');
20
+ assert.ok(priceTable.aliases['deepseek-chat'], 'deepseek-chat 必须在 aliases 表');
21
+ assert.equal(priceTable.aliases['deepseek-chat'], 'deepseek-v4-flash');
22
+ ok('v2 schema 载入 + 裸键 + qualified 键 + alias 双查找');
18
23
  for (const [name, entry] of Object.entries(priceTable.prices)){
19
24
  assert.ok(entry.input_cache_hit_usd_per_1m_peak <= entry.input_cache_miss_usd_per_1m_peak, `${name} hit<=miss`);
20
25
  assert.ok(entry.output_usd_per_1m_offpeak <= entry.output_usd_per_1m_peak, `${name} offpeak<=peak`);
@@ -38,7 +43,19 @@ const hitUsage = {
38
43
  };
39
44
  assert.equal(priceRunCost('deepseek-chat', hitUsage, priceTable), 1.334);
40
45
  ok('peak hit 换算 0.014+1.32=1.334');
46
+ const m3cost = priceRunCost('MiniMax-M3', baseUsage, priceTable);
47
+ assert.equal(m3cost, 3.00);
48
+ ok('MiniMax-M3 peak miss 换算 0.60+2.40=3.00');
49
+ const m3hit = priceRunCost('MiniMax-M3', hitUsage, priceTable);
50
+ assert.equal(m3hit, 2.52);
51
+ ok('MiniMax-M3 peak hit 换算 0.12+2.40=2.52');
52
+ const m3prov = priceTable.prices['minimax:MiniMax-M3'];
53
+ assert.equal(m3prov.input_cache_miss_usd_per_1m_peak, m3prov.input_cache_miss_usd_per_1m_offpeak, 'MiniMax-M3 flat 纪律:peak==offpeak miss');
54
+ assert.equal(m3prov.input_cache_hit_usd_per_1m_peak, m3prov.input_cache_hit_usd_per_1m_offpeak, 'MiniMax-M3 flat 纪律:peak==offpeak hit');
55
+ assert.equal(m3prov.output_usd_per_1m_peak, m3prov.output_usd_per_1m_offpeak, 'MiniMax-M3 flat 纪律:peak==offpeak output');
56
+ ok('MiniMax flat_no_offpeak 纪律同价守门');
41
57
  assert.throws(()=>priceRunCost('unknown-model-xyz', baseUsage, priceTable), /no entry for model/);
58
+ ok('未知模型名 → fail-closed');
42
59
  const dangling = {
43
60
  ...priceTable,
44
61
  aliases: {
@@ -50,6 +67,52 @@ assert.throws(()=>priceRunCost('deepseek-chat', {
50
67
  ...baseUsage,
51
68
  responsesMissingUsage: 1
52
69
  }, priceTable), /cost unprovable/);
70
+ const tmpSchemaDir = mkdtempSync(join(tmpdir(), 'nightly-bad-schema-'));
71
+ const badSchemaPath = join(tmpSchemaDir, 'bad.json');
72
+ try {
73
+ writeFileSync(badSchemaPath, JSON.stringify({
74
+ schema_version: 'gotry_llm_price_table_v99',
75
+ prices: {}
76
+ }));
77
+ assert.throws(()=>loadPriceTable(badSchemaPath), /schema_version must be/);
78
+ ok('非法 schema_version 拒绝');
79
+ } finally{
80
+ rmSync(tmpSchemaDir, {
81
+ recursive: true,
82
+ force: true
83
+ });
84
+ }
85
+ const tmpStrategyDir = mkdtempSync(join(tmpdir(), 'nightly-bad-strategy-'));
86
+ const badStrategyPath = join(tmpStrategyDir, 'bad.json');
87
+ try {
88
+ writeFileSync(badStrategyPath, JSON.stringify({
89
+ schema_version: 'gotry_llm_price_table_v2',
90
+ providers: {
91
+ bad: {
92
+ family: 'x',
93
+ price_strategy: 'mystery_strategy',
94
+ aliases: {},
95
+ models: {
96
+ 'm': {
97
+ input_cache_miss_usd_per_1m_peak: 0.1,
98
+ input_cache_hit_usd_per_1m_peak: 0.01,
99
+ output_usd_per_1m_peak: 0.5,
100
+ input_cache_miss_usd_per_1m_offpeak: 0.1,
101
+ input_cache_hit_usd_per_1m_offpeak: 0.01,
102
+ output_usd_per_1m_offpeak: 0.5
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }));
108
+ assert.throws(()=>loadPriceTable(badStrategyPath), /price_strategy must be/);
109
+ ok('v2 非法 price_strategy 拒绝');
110
+ } finally{
111
+ rmSync(tmpStrategyDir, {
112
+ recursive: true,
113
+ force: true
114
+ });
115
+ }
53
116
  ok('未知模型/悬空alias/usage缺失 fail-closed');
54
117
  const executeAt = '2026-08-29T12:00:00.000Z';
55
118
  const recordInput = {
@@ -120,4 +183,4 @@ ok('dry-run mock 全链演练 + real_llm=false + 零落盘');
120
183
  console.log(`nightly-evidence tests: ${passed} 组断言全绿(offline,真跑不在 CI)`);
121
184
 
122
185
 
123
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nightly-evidence-tests.ts
186
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/nightly-evidence-tests.ts
@@ -55,9 +55,16 @@ function assertPriceEntry(label, value) {
55
55
  }
56
56
  export function loadPriceTable(path) {
57
57
  const raw = JSON.parse(readFileSync(path, 'utf8'));
58
- if (raw['schema_version'] !== 'gotry_llm_price_table_v1') {
59
- throw new Error(`price table schema_version must be gotry_llm_price_table_v1, got: ${String(raw['schema_version'])}`);
58
+ const schemaVersion = raw['schema_version'];
59
+ if (schemaVersion === 'gotry_llm_price_table_v2') {
60
+ return loadPriceTableV2(raw);
60
61
  }
62
+ if (schemaVersion === 'gotry_llm_price_table_v1') {
63
+ return loadPriceTableV1(raw);
64
+ }
65
+ throw new Error(`price table schema_version must be gotry_llm_price_table_v1 or _v2, got: ${String(schemaVersion)}`);
66
+ }
67
+ function loadPriceTableV1(raw) {
61
68
  const aliases = typeof raw['aliases'] === 'object' && raw['aliases'] !== null ? raw['aliases'] : {};
62
69
  for (const [from, to] of Object.entries(aliases)){
63
70
  if (typeof to !== 'string') throw new Error(`price table alias ${from} must map to a model name`);
@@ -72,6 +79,55 @@ export function loadPriceTable(path) {
72
79
  prices
73
80
  };
74
81
  }
82
+ function loadPriceTableV2(raw) {
83
+ const providersObj = typeof raw['providers'] === 'object' && raw['providers'] !== null ? raw['providers'] : null;
84
+ if (!providersObj) throw new Error('price table v2 requires providers object');
85
+ const aliases = {};
86
+ const prices = {};
87
+ for (const [providerId, rawProvider] of Object.entries(providersObj)){
88
+ const provider = assertProviderEntry(`price table provider ${providerId}`, rawProvider);
89
+ for (const [aliasName, target] of Object.entries(provider.aliases)){
90
+ if (typeof target !== 'string') throw new Error(`provider ${providerId} alias ${aliasName} must map to a model name`);
91
+ if (!provider.models[target]) {
92
+ throw new Error(`provider ${providerId} alias ${aliasName} -> ${target} has no entry; update data/llm-price-table.json via PR`);
93
+ }
94
+ aliases[aliasName] = target;
95
+ }
96
+ for (const [modelName, entry] of Object.entries(provider.models)){
97
+ const qualified = `${providerId}:${modelName}`;
98
+ const validated = assertPriceEntry(`price table provider ${providerId} model ${modelName}`, entry);
99
+ if (prices[qualified]) throw new Error(`duplicate price table key ${qualified}`);
100
+ prices[qualified] = validated;
101
+ if (prices[modelName]) throw new Error(`duplicate price table key ${modelName} (across providers, disambiguate via aliases or qualified lookup)`);
102
+ prices[modelName] = validated;
103
+ }
104
+ }
105
+ return {
106
+ schema_version: 'gotry_llm_price_table_v2',
107
+ aliases,
108
+ prices
109
+ };
110
+ }
111
+ function assertProviderEntry(label, value) {
112
+ const entry = typeof value === 'object' && value !== null ? value : null;
113
+ if (!entry) throw new Error(`${label} must be an object`);
114
+ const family = entry['family'];
115
+ if (typeof family !== 'string' || family.length === 0) throw new Error(`${label}.family must be a non-empty string`);
116
+ const strategy = entry['price_strategy'];
117
+ if (strategy !== 'tiered_peak_offpeak' && strategy !== 'flat_no_offpeak') {
118
+ throw new Error(`${label}.price_strategy must be 'tiered_peak_offpeak' or 'flat_no_offpeak', got: ${String(strategy)}`);
119
+ }
120
+ const aliases = typeof entry['aliases'] === 'object' && entry['aliases'] !== null ? entry['aliases'] : {};
121
+ const models = typeof entry['models'] === 'object' && entry['models'] !== null ? entry['models'] : {};
122
+ return {
123
+ family,
124
+ price_strategy: strategy,
125
+ api_base: typeof entry['api_base'] === 'string' ? entry['api_base'] : undefined,
126
+ source_url: typeof entry['source_url'] === 'string' ? entry['source_url'] : undefined,
127
+ aliases,
128
+ models
129
+ };
130
+ }
75
131
  export function priceRunCost(model, usage, table) {
76
132
  if (usage.responsesMissingUsage > 0) {
77
133
  throw new Error(`usage provider omitted usage on ${usage.responsesMissingUsage} response(s) — cost unprovable, refusing to price (fail-closed)`);
@@ -230,4 +286,4 @@ if (process.argv[1]?.endsWith('nightly-evidence.ts')) {
230
286
  }
231
287
 
232
288
 
233
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nightly-evidence.ts
289
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/nightly-evidence.ts
@@ -80,4 +80,4 @@ if (channel === 'file') {
80
80
  }
81
81
 
82
82
 
83
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nudge-digest.ts
83
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/nudge-digest.ts
@@ -0,0 +1,218 @@
1
+ import assert from 'node:assert/strict';
2
+ import { runOnboardingWizard, defaultExtensionDir, describeExtensionDir } from '../capabilities/session/wizard.js';
3
+ import { startExtensionHealthWatch, withAutoRetryOnExtension, HEALTH_WATCH_INTERVAL_FLOOR_MS, HEALTH_WATCH_INTERVAL_CEIL_MS } from '../capabilities/session/health-watch.js';
4
+ let passed = 0;
5
+ function check(label, fn) {
6
+ return Promise.resolve().then(fn).then(()=>{
7
+ passed += 1;
8
+ console.log(` ok - ${label}`);
9
+ });
10
+ }
11
+ function noopSpawn() {
12
+ const { EventEmitter } = require('node:events');
13
+ const child = new EventEmitter();
14
+ const stream = new EventEmitter();
15
+ child.stdin = stream;
16
+ child.stdout = null;
17
+ child.stderr = null;
18
+ child.unref = ()=>undefined;
19
+ return child;
20
+ }
21
+ function captureSpawn() {
22
+ const calls = [];
23
+ const spawn = (bin, args = [])=>{
24
+ calls.push({
25
+ bin,
26
+ args
27
+ });
28
+ return noopSpawn();
29
+ };
30
+ return {
31
+ spawn,
32
+ calls
33
+ };
34
+ }
35
+ async function asyncSleep(ms) {
36
+ if (ms <= 0) return;
37
+ await new Promise((resolve)=>setTimeout(resolve, ms));
38
+ }
39
+ console.log('ONBOARDING UX (§40)');
40
+ await check('1. wizard --dry-run 零网络零浏览器,exit 0,5 步齐全', async ()=>{
41
+ const { spawn } = captureSpawn();
42
+ const result = await runOnboardingWizard({
43
+ dryRun: true,
44
+ forcePlatform: 'darwin',
45
+ spawn
46
+ });
47
+ assert.equal(result.ok, true, `wizard ok 应 true,实际 ${JSON.stringify(result.steps)}`);
48
+ assert.equal(result.steps.length, 5, `应有 5 步,实际 ${result.steps.length}`);
49
+ const expected = [
50
+ 'ensure-extension-files',
51
+ 'open-chrome-extensions',
52
+ 'clipboard-extension-path',
53
+ 'panel-guide',
54
+ 'watch-extension-ready'
55
+ ];
56
+ for(let i = 0; i < expected.length; i++){
57
+ assert.equal(result.steps[i].step, expected[i]);
58
+ assert.equal(result.steps[i].status, 'skip', `dry-run 每步应为 skip,${result.steps[i].step} 实为 ${result.steps[i].status}`);
59
+ }
60
+ assert.equal(result.platform, 'darwin');
61
+ });
62
+ await check('2. health-watch 三时序:0ms / 中途 / 120s+1ms', async ()=>{
63
+ const w0 = startExtensionHealthWatch({
64
+ timeoutMs: 0,
65
+ intervalMs: 10
66
+ });
67
+ const o0 = await w0.waitReady();
68
+ assert.equal(o0.ready, false);
69
+ assert.equal(o0.reason, 'timeout');
70
+ let probeCalls = 0;
71
+ const wMid = startExtensionHealthWatch({
72
+ timeoutMs: 500,
73
+ intervalMs: 30,
74
+ probe: async ()=>{
75
+ probeCalls += 1;
76
+ return probeCalls >= 2;
77
+ }
78
+ });
79
+ const oMid = await wMid.waitReady();
80
+ assert.equal(oMid.ready, true, `中途 ready 实为 ${JSON.stringify(oMid)}`);
81
+ assert.ok(oMid.attempts >= 2);
82
+ const wTimeout = startExtensionHealthWatch({
83
+ timeoutMs: 60,
84
+ intervalMs: 20,
85
+ probe: async ()=>false
86
+ });
87
+ const oTimeout = await wTimeout.waitReady();
88
+ assert.equal(oTimeout.ready, false);
89
+ assert.equal(oTimeout.reason, 'timeout');
90
+ });
91
+ await check('3. 剪贴板 + GUI 面板跨平台降级', async ()=>{
92
+ const darwin = captureSpawn();
93
+ const rDarwin = await runOnboardingWizard({
94
+ dryRun: false,
95
+ forcePlatform: 'darwin',
96
+ extensionDir: '/tmp/gotry-test-ext',
97
+ spawn: darwin.spawn,
98
+ sourceDir: '/'
99
+ });
100
+ const darwinBins = darwin.calls.map((c)=>c.bin);
101
+ assert.ok(darwinBins.includes('open'), `darwin 应调 open(实调 ${darwinBins.join(',')})`);
102
+ assert.ok(darwinBins.includes('pbcopy'), 'darwin 应调 pbcopy');
103
+ assert.ok(darwinBins.includes('osascript'), 'darwin 应调 osascript');
104
+ const linux = captureSpawn();
105
+ await runOnboardingWizard({
106
+ dryRun: false,
107
+ forcePlatform: 'linux',
108
+ extensionDir: '/tmp/gotry-test-ext',
109
+ spawn: linux.spawn,
110
+ sourceDir: '/'
111
+ });
112
+ const linuxBins = linux.calls.map((c)=>c.bin);
113
+ assert.ok(linuxBins.includes('xdg-open'), `linux 应调 xdg-open(实调 ${linuxBins.join(',')})`);
114
+ assert.ok(linuxBins.includes('xclip'), 'linux 应调 xclip');
115
+ assert.ok(linuxBins.includes('zenity'), 'linux 应调 zenity');
116
+ const headless = captureSpawn();
117
+ const rHeadless = await runOnboardingWizard({
118
+ dryRun: true,
119
+ forcePlatform: 'headless',
120
+ extensionDir: '/tmp/gotry-test-ext',
121
+ spawn: headless.spawn
122
+ });
123
+ assert.equal(headless.calls.length, 0, `headless 不应 spawn 任何命令(实调 ${headless.calls.length})`);
124
+ assert.equal(rHeadless.platform, 'headless');
125
+ });
126
+ await check('4. retry-after-watch:needs-extension → watch ready → 自动重放同 query_id', async ()=>{
127
+ let fetchCalls = 0;
128
+ const queryId = 'sf-01';
129
+ const fetch = async ()=>{
130
+ fetchCalls += 1;
131
+ return {
132
+ query_id: queryId,
133
+ verdict: 'needs-extension',
134
+ needsExt: fetchCalls === 1
135
+ };
136
+ };
137
+ const first = await fetch();
138
+ assert.equal(first.verdict, 'needs-extension');
139
+ let probeCount = 0;
140
+ const watch = startExtensionHealthWatch({
141
+ timeoutMs: 1_000,
142
+ intervalMs: 20,
143
+ probe: async ()=>{
144
+ probeCount += 1;
145
+ return probeCount >= 2;
146
+ }
147
+ });
148
+ const outcome = await watch.waitReady();
149
+ assert.equal(outcome.ready, true);
150
+ const retry = await fetch();
151
+ assert.equal(retry.query_id, queryId, '同 query_id 必须保持');
152
+ assert.equal(fetchCalls, 2, `应调用 fetch 两次(首次 + 重放),实 ${fetchCalls}`);
153
+ });
154
+ await check('5. wizard 无 GUI 走终端降级', async ()=>{
155
+ const headless = captureSpawn();
156
+ const result = await runOnboardingWizard({
157
+ dryRun: true,
158
+ forcePlatform: 'headless',
159
+ extensionDir: '/tmp/gotry-test-ext',
160
+ spawn: headless.spawn
161
+ });
162
+ const clip = result.steps.find((s)=>s.step === 'clipboard-extension-path');
163
+ const panel = result.steps.find((s)=>s.step === 'panel-guide');
164
+ const watch = result.steps.find((s)=>s.step === 'watch-extension-ready');
165
+ assert.equal(clip?.status, 'skip', 'headless 剪贴板应 skip');
166
+ assert.equal(panel?.status, 'skip', 'headless 面板应 skip(终端降级)');
167
+ assert.equal(watch?.status, 'skip', 'dry-run watch 应 skip');
168
+ assert.equal(headless.calls.length, 0, `headless 不应调任何 spawn(实调 ${headless.calls.length})`);
169
+ });
170
+ await check('6. withAutoRetryOnExtension 端到端:首次 needs-ext → 自动重放', async ()=>{
171
+ let callCount = 0;
172
+ let ready = false;
173
+ setTimeout(()=>{
174
+ ready = true;
175
+ }, 30);
176
+ const args = {
177
+ fetch: async ()=>{
178
+ callCount += 1;
179
+ return callCount === 1 ? {
180
+ needsExt: true
181
+ } : {
182
+ needsExt: false,
183
+ hit: true
184
+ };
185
+ },
186
+ isExtensionNeeded: (r)=>r.needsExt === true,
187
+ watch: {
188
+ timeoutMs: 1_000,
189
+ intervalMs: 20,
190
+ probe: async ()=>ready
191
+ }
192
+ };
193
+ const out = await withAutoRetryOnExtension(args);
194
+ assert.equal(out.retried, true, `应触发重放,实 retried=${out.retried}`);
195
+ assert.equal(out.attempts, 2);
196
+ assert.equal(out.result.needsExt, false);
197
+ assert.equal(out.result.hit, true);
198
+ assert.equal(callCount, 2, `withAutoRetry 应调 2 次 fetch,实 ${callCount}`);
199
+ });
200
+ await check('7. health-watch interval 边界常量与 watch 契约对齐', ()=>{
201
+ assert.ok(HEALTH_WATCH_INTERVAL_FLOOR_MS >= 1_000, '下界不应让 watch 风暴');
202
+ assert.ok(HEALTH_WATCH_INTERVAL_CEIL_MS <= 44_000, '上界应 < EXTENSION_CONNECTED_WINDOW_MS');
203
+ });
204
+ await check('8. defaultExtensionDir = ~/.gotry/extension(契约对齐 bootstrap 节)', ()=>{
205
+ const dir = defaultExtensionDir();
206
+ assert.match(dir, /\.gotry[\\/]extension$/, `扩展目录形态应是 ~/.gotry/extension,实 ${dir}`);
207
+ });
208
+ await check('9. describeExtensionDir:不存在的目录不抛错,返回 exists:false', ()=>{
209
+ const desc = describeExtensionDir('/nonexistent/zzz');
210
+ assert.equal(desc.exists, false);
211
+ assert.equal(desc.fileCount, 0);
212
+ assert.equal(desc.hasManifest, false);
213
+ });
214
+ await asyncSleep(0);
215
+ console.log(`ONBOARDING UX (§40): ${passed} pass`);
216
+
217
+
218
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/onboarding-tests.ts
@@ -17,4 +17,4 @@ const target = process.argv[2] ?? 'EK329';
17
17
  console.log(await checkArrival(target, process.argv[3] ?? null));
18
18
 
19
19
 
20
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/opensky-check.ts
20
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/opensky-check.ts
@@ -49,4 +49,4 @@ console.log('3. timeout → unavailable OK');
49
49
  console.log('\nOPEN-SKY TESTS: 3/3 OK(免费匿名 OpenSky API)');
50
50
 
51
51
 
52
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/opensky-tests.ts
52
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/opensky-tests.ts