@danceiny/gotry 0.0.1-rc.5 → 0.0.1-rc.7
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 +1 -1
- package/bin/gotry-inner.js +73 -32
- package/cordis.gotry-patch.yml +2 -2
- package/data/flights_2026.json +215 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +64 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/yunnan-pack.json +201 -0
- package/dist/capabilities/agent-reach-deep.js +172 -0
- package/dist/capabilities/agent-reach.js +204 -0
- package/dist/capabilities/anything.js +139 -0
- package/dist/capabilities/hbcli.js +138 -0
- package/dist/capabilities/incident-log.js +99 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/agent-reach-deep-tests.js +54 -0
- package/dist/scripts/agent-reach-tests.js +38 -0
- package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
- package/dist/scripts/anything-tests.js +95 -0
- package/dist/scripts/async-collect.js +18 -0
- package/dist/scripts/diff-test.js +39 -0
- package/dist/scripts/engine-run.js +14 -0
- package/dist/scripts/engine-tests.js +45 -0
- package/dist/scripts/hbcli-tests.js +85 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +79 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +44 -0
- package/dist/scripts/skeleton-check.js +35 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/smoke.js +90 -0
- package/dist/scripts/unified-tests.js +49 -0
- package/dist/scripts/weather-tests.js +48 -0
- package/dist/src/bridge.js +34 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +154 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +760 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +296 -0
- package/dist/src/mock-llm.js +98 -0
- package/dist/src/model.js +134 -0
- package/dist/src/unified.js +536 -0
- package/package.json +3 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readUrl } from '../capabilities/agent-reach.js';
|
|
3
|
+
{
|
|
4
|
+
const r = await readUrl({
|
|
5
|
+
url: 'example.com'
|
|
6
|
+
});
|
|
7
|
+
assert.equal(r.ok, false);
|
|
8
|
+
assert.equal(r.via, 'r.jina.ai-error');
|
|
9
|
+
assert.ok(r.evidence.includes('error'), 'evidence 带 error');
|
|
10
|
+
console.log('1. 非法 URL 拒绝 OK');
|
|
11
|
+
}{
|
|
12
|
+
const r = await readUrl({
|
|
13
|
+
url: 'https://example.com',
|
|
14
|
+
timeoutMs: 1
|
|
15
|
+
});
|
|
16
|
+
assert.equal(r.ok, false);
|
|
17
|
+
assert.equal(r.via, 'r.jina.ai-error');
|
|
18
|
+
assert.ok(r.latencyMs >= 0, '永不抛错');
|
|
19
|
+
console.log('2. 超时降级 OK');
|
|
20
|
+
}{
|
|
21
|
+
const r = await readUrl({
|
|
22
|
+
url: 'https://example.com',
|
|
23
|
+
timeoutMs: 30_000
|
|
24
|
+
});
|
|
25
|
+
if (r.ok) {
|
|
26
|
+
assert.ok(r.evidence.includes('[agent-reach:web.read@'), '证据链');
|
|
27
|
+
assert.match(r.evidence, /\[agent-reach:web\.read@2\d{3}-\d{2}-\d{2}T/, '证据链带 ISO 时间戳');
|
|
28
|
+
assert.ok(typeof r.content === 'string' && r.content.length > 0, 'content 非空');
|
|
29
|
+
assert.equal(r.title, 'Example Domain', `Title 提取,实际 ${r.title}`);
|
|
30
|
+
console.log(`3. readUrl example.com → ok (title=${r.title}) OK`);
|
|
31
|
+
} else {
|
|
32
|
+
assert.equal(r.via, 'r.jina.ai-error');
|
|
33
|
+
console.log(`3. readUrl example.com → 降级(${r.error?.slice(0, 60)})(网络边界,合法) OK`);
|
|
34
|
+
}
|
|
35
|
+
}console.log('\nAGENT-REACH TESTS: 3/3 OK(readUrl 薄壳;上游真读见 wrapper 套)');
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/agent-reach-tests.ts
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { reach, reachStatus } from '../capabilities/agent-reach.js';
|
|
3
|
+
{
|
|
4
|
+
const st = await reachStatus(90_000);
|
|
5
|
+
assert.equal(st.ok, true);
|
|
6
|
+
assert.equal(st.via, 'agent-reach-cli');
|
|
7
|
+
assert.ok(st.output.length > 50, 'doctor 输出非空(上游原样)');
|
|
8
|
+
console.log(`1. status → doctor 透传 (${st.latencyMs}ms) OK`);
|
|
9
|
+
}{
|
|
10
|
+
const r = await reach({
|
|
11
|
+
channel: 'web',
|
|
12
|
+
method: 'read',
|
|
13
|
+
args: [
|
|
14
|
+
'https://example.com'
|
|
15
|
+
],
|
|
16
|
+
timeoutMs: 30_000
|
|
17
|
+
});
|
|
18
|
+
assert.equal(r.verdict, 'found');
|
|
19
|
+
assert.ok(r.evidence.includes('[agent-reach:web.read@'), '证据链 [agent-reach:web.read@ts]');
|
|
20
|
+
assert.ok(typeof r.data === 'string' && r.data.includes('Example Domain'), 'content 含 Example Domain');
|
|
21
|
+
console.log(`2. web.read example.com → found (${r.latencyMs}ms) OK`);
|
|
22
|
+
}{
|
|
23
|
+
const r = await reach({
|
|
24
|
+
channel: 'v2ex',
|
|
25
|
+
method: 'get_hot_topics',
|
|
26
|
+
timeoutMs: 30_000
|
|
27
|
+
});
|
|
28
|
+
if (r.ok) {
|
|
29
|
+
assert.ok(Array.isArray(r.data), 'hot topics 是数组');
|
|
30
|
+
console.log(`3. v2ex.get_hot_topics → found (${r.data.length} topics) OK`);
|
|
31
|
+
} else {
|
|
32
|
+
assert.ok([
|
|
33
|
+
'error',
|
|
34
|
+
'needs-setup'
|
|
35
|
+
].includes(r.verdict), '降级合法');
|
|
36
|
+
console.log(`3. v2ex.get_hot_topics → ${r.verdict}(网络边界,降级合法) OK`);
|
|
37
|
+
}
|
|
38
|
+
}{
|
|
39
|
+
const r = await reach({
|
|
40
|
+
channel: 'xueqiu',
|
|
41
|
+
method: 'get_stock_quote',
|
|
42
|
+
args: [
|
|
43
|
+
'SH600519'
|
|
44
|
+
],
|
|
45
|
+
timeoutMs: 30_000
|
|
46
|
+
});
|
|
47
|
+
assert.equal(r.verdict, 'needs-setup', `实际 verdict=${r.verdict} error=${r.error}`);
|
|
48
|
+
assert.ok((r.setup ?? '').includes('configure'), 'setup 透传上游 configure 指引(不转述)');
|
|
49
|
+
console.log('4. xueqiu.get_stock_quote → needs-setup(上游原话指引) OK');
|
|
50
|
+
}{
|
|
51
|
+
const r = await reach({
|
|
52
|
+
channel: 'nosuch',
|
|
53
|
+
method: 'foo'
|
|
54
|
+
});
|
|
55
|
+
assert.equal(r.ok, false);
|
|
56
|
+
const channels = r.inventory?.channels ?? {};
|
|
57
|
+
assert.ok(channels.web && channels.xueqiu && channels['exa_search'], 'inventory 带回上游注册表(含 exa_search 真名)');
|
|
58
|
+
console.log(`5. 未知渠道 → 自描述清单(${Object.keys(channels).length} 渠道) OK`);
|
|
59
|
+
}{
|
|
60
|
+
const r = await reach({
|
|
61
|
+
channel: 'web',
|
|
62
|
+
method: 'nosuch'
|
|
63
|
+
});
|
|
64
|
+
assert.equal(r.ok, false);
|
|
65
|
+
const methods = r.inventory?.methods ?? {};
|
|
66
|
+
assert.ok(methods.read, 'inventory.methods 带 read 签名');
|
|
67
|
+
console.log(`6. 未知方法 → 自描述清单(${Object.keys(methods).length} 方法) OK`);
|
|
68
|
+
}{
|
|
69
|
+
const r = await reach({
|
|
70
|
+
channel: '',
|
|
71
|
+
method: ''
|
|
72
|
+
});
|
|
73
|
+
assert.equal(r.ok, false);
|
|
74
|
+
assert.equal(r.verdict, 'error');
|
|
75
|
+
console.log('7. 空 channel/method 降级 OK');
|
|
76
|
+
}console.log('\nAGENT-REACH WRAPPER TESTS: 7/7 OK(反射桥 + doctor 透传 + 自描述错误)');
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/agent-reach-wrapper-tests.ts
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm, writeFile, chmod } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { anythingSearch } from '../capabilities/anything.js';
|
|
6
|
+
const tmp = await mkdtemp(join(tmpdir(), 'anything-test-'));
|
|
7
|
+
try {
|
|
8
|
+
async function fakeBin(name, behaviour) {
|
|
9
|
+
const p = join(tmp, name);
|
|
10
|
+
if (behaviour === 'ok') {
|
|
11
|
+
await writeFile(p, `#!/bin/sh
|
|
12
|
+
cat <<'JSON'
|
|
13
|
+
{"candidates":[
|
|
14
|
+
{"type":"hotel","name":"Park Hyatt","hotel":{"id":"h1","name":"Park Hyatt","latitude":22.31,"longitude":114.16}},
|
|
15
|
+
{"type":"city","name":"大理市","region":{"id":"d1","name":"Dali","latitude":25.58,"longitude":100.21}},
|
|
16
|
+
{"type":"place","name":"洱海","region":{"id":"r1","name":"Erhai Lake","latitude":25.74,"longitude":100.25}}
|
|
17
|
+
]}
|
|
18
|
+
JSON
|
|
19
|
+
`, {
|
|
20
|
+
mode: 0o755
|
|
21
|
+
});
|
|
22
|
+
} else if (behaviour === 'empty') {
|
|
23
|
+
await writeFile(p, `#!/bin/sh
|
|
24
|
+
cat <<'JSON'
|
|
25
|
+
{"candidates":[]}
|
|
26
|
+
JSON
|
|
27
|
+
`, {
|
|
28
|
+
mode: 0o755
|
|
29
|
+
});
|
|
30
|
+
} else if (behaviour === 'fail') {
|
|
31
|
+
await writeFile(p, `#!/bin/sh
|
|
32
|
+
echo 'hotelbe down' >&2
|
|
33
|
+
exit 1
|
|
34
|
+
`, {
|
|
35
|
+
mode: 0o755
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
await writeFile(p, `#!/bin/sh
|
|
39
|
+
echo 'starting' >&2
|
|
40
|
+
while :; do sleep 5; done
|
|
41
|
+
`, {
|
|
42
|
+
mode: 0o755
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
const ok = await fakeBin('hbcli-ok', 'ok');
|
|
48
|
+
const r1 = await anythingSearch({
|
|
49
|
+
keyword: 'Park Hyatt',
|
|
50
|
+
hbcliBin: ok
|
|
51
|
+
});
|
|
52
|
+
assert.equal(r1.verdict, 'hit');
|
|
53
|
+
assert.equal(r1.hits.length, 3);
|
|
54
|
+
assert.ok(r1.evidence.includes('hbcli-anything@'));
|
|
55
|
+
console.log(`1. Park Hyatt → hit (${r1.hits.length} candidates) OK`);
|
|
56
|
+
const empty = await fakeBin('hbcli-empty', 'empty');
|
|
57
|
+
const r2 = await anythingSearch({
|
|
58
|
+
keyword: 'nothing',
|
|
59
|
+
hbcliBin: empty
|
|
60
|
+
});
|
|
61
|
+
assert.equal(r2.verdict, 'miss');
|
|
62
|
+
assert.equal(r2.hits.length, 0);
|
|
63
|
+
console.log('2. nothing → miss OK');
|
|
64
|
+
const fail = await fakeBin('hbcli-fail', 'fail');
|
|
65
|
+
const r3 = await anythingSearch({
|
|
66
|
+
keyword: 'x',
|
|
67
|
+
hbcliBin: fail
|
|
68
|
+
});
|
|
69
|
+
assert.equal(r3.verdict, 'error');
|
|
70
|
+
assert.equal(r3.ok, false);
|
|
71
|
+
assert.match(r3.evidence, /error/);
|
|
72
|
+
console.log('3. fail → error (降级) OK');
|
|
73
|
+
const hang = await fakeBin('hbcli-hang', 'hang');
|
|
74
|
+
const r4 = await anythingSearch({
|
|
75
|
+
keyword: 'x',
|
|
76
|
+
hbcliBin: hang,
|
|
77
|
+
timeoutMs: 800
|
|
78
|
+
});
|
|
79
|
+
assert.equal(r4.verdict, 'error', 'timeout 应判 error');
|
|
80
|
+
console.log(`4. hang/timeout → error OK (latency=${r4.latencyMs}ms)`);
|
|
81
|
+
const r5 = await anythingSearch({
|
|
82
|
+
keyword: ' '
|
|
83
|
+
});
|
|
84
|
+
assert.equal(r5.verdict, 'error');
|
|
85
|
+
console.log('5. empty keyword → error OK');
|
|
86
|
+
console.log('\nANYTHING TESTS: 5/5 OK(hbcli fake + graceful degrade)');
|
|
87
|
+
} finally{
|
|
88
|
+
await rm(tmp, {
|
|
89
|
+
recursive: true,
|
|
90
|
+
force: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/anything-tests.ts
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { loadAsyncTicket, settleAsyncTicket, collectDeepPlanning } from '../src/loop.js';
|
|
2
|
+
import { solveUnified } from '../src/unified.js';
|
|
3
|
+
const ticketId = process.argv[2];
|
|
4
|
+
if (!ticketId) {
|
|
5
|
+
console.error('用法:npx tsx scripts/async-collect.ts <ticketId>(工单在 gotry-state/async/)');
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
const loaded = await loadAsyncTicket(ticketId);
|
|
9
|
+
if (!loaded) {
|
|
10
|
+
console.error(`工单 ${ticketId} 不存在(gotry-state/async/${ticketId}.json)`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const { reply } = await collectDeepPlanning(loaded.state, loaded.ticket, solveUnified);
|
|
14
|
+
const out = await settleAsyncTicket(ticketId, reply);
|
|
15
|
+
console.log(`交付物已落盘:ts/${out}\n\n${reply}`);
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/async-collect.ts
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import * as unifiedModule from '../src/unified.js';
|
|
5
|
+
import { parseCandidate, parseRequest } from '../src/model.js';
|
|
6
|
+
const payload = JSON.parse(await readFile(join('..', 'data', 'golden_erhai.json'), 'utf-8'));
|
|
7
|
+
const req = parseRequest(payload['request']);
|
|
8
|
+
const candidates = payload['candidates'].map(parseCandidate);
|
|
9
|
+
const specA = unifiedModule.segmentsFromCandidate(req, candidates);
|
|
10
|
+
const tsA = unifiedModule.solveChoiceSegment(specA, req);
|
|
11
|
+
const specB = JSON.parse(JSON.stringify(specA));
|
|
12
|
+
const tsB = unifiedModule.solveChoiceSegment(specB, req);
|
|
13
|
+
console.log(`pure-TS 双路径稳定性(同模块, 独立 spec 实例 — 验证幂等)\n`);
|
|
14
|
+
assert.equal(tsA['recommended'], tsB['recommended'], 'recommended 一致');
|
|
15
|
+
const tsV = Object.fromEntries(tsA['verdicts'].map((v)=>[
|
|
16
|
+
v['candidate_id'],
|
|
17
|
+
v
|
|
18
|
+
]));
|
|
19
|
+
const tsVc = Object.fromEntries(tsB['verdicts'].map((v)=>[
|
|
20
|
+
v['candidate_id'],
|
|
21
|
+
v
|
|
22
|
+
]));
|
|
23
|
+
assert.deepEqual(Object.keys(tsV).sort(), Object.keys(tsVc).sort(), '候选集合一致');
|
|
24
|
+
for (const id of Object.keys(tsV)){
|
|
25
|
+
assert.equal(tsV[id]['feasible'], tsVc[id]['feasible'], `${id} feasible 一致`);
|
|
26
|
+
assert.deepEqual(tsV[id]['unsat_core'] ?? [], tsVc[id]['unsat_core'] ?? [], `${id} unsat_core 一致`);
|
|
27
|
+
if (tsV[id]['wish_pool'] && tsVc[id]['wish_pool']) {
|
|
28
|
+
assert.equal(tsV[id]['wish_pool']['conditions']['days'], tsVc[id]['wish_pool']['conditions']['days'], `${id} wish days 一致`);
|
|
29
|
+
assert.equal(tsV[id]['wish_pool']['conditions']['budget_cny'], tsVc[id]['wish_pool']['conditions']['budget_cny'], `${id} wish budget(最优值)一致`);
|
|
30
|
+
}
|
|
31
|
+
if (tsV[id]['true_cost'] && tsVc[id]['true_cost']) {
|
|
32
|
+
const sameWake = tsV[id]['true_cost']['wake'] === tsVc[id]['true_cost']['wake'];
|
|
33
|
+
console.log(`${id}: wake ${tsV[id]['true_cost']['wake']} vs ${tsVc[id]['true_cost']['wake']} ${sameWake ? '(等价选择)' : '(不同选择,均在可行域)'}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
console.log('\nDIFF TEST OK: 双路径判定一致(纯 TS,无 Python 依赖)');
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/diff-test.ts
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parseCandidate, parseRequest } from '../src/model.js';
|
|
4
|
+
import { solve } from '../src/engine.js';
|
|
5
|
+
const payload = JSON.parse(await readFile(join('..', 'data', 'golden_erhai.json'), 'utf-8'));
|
|
6
|
+
const req = parseRequest(payload['request']);
|
|
7
|
+
const candidates = payload['candidates'].map(parseCandidate);
|
|
8
|
+
const t0 = Date.now();
|
|
9
|
+
const result = await solve(req, candidates);
|
|
10
|
+
console.log(`(TS 引擎总耗时含 WASM init: ${Date.now() - t0}ms, recommended=${result['recommended']})\n`);
|
|
11
|
+
console.log(result['answer_md']);
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/engine-run.ts
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parseCandidate, parseRequest, requiredUsableHours } from '../src/model.js';
|
|
5
|
+
import { solve, solveCandidate } from '../src/engine.js';
|
|
6
|
+
const payload = JSON.parse(await readFile(join('..', 'data', 'golden_erhai.json'), 'utf-8'));
|
|
7
|
+
const req = parseRequest(payload['request']);
|
|
8
|
+
const candidates = payload['candidates'].map(parseCandidate);
|
|
9
|
+
const result = await solve(req, candidates);
|
|
10
|
+
const verdicts = Object.fromEntries(result.verdicts.map((v)=>[
|
|
11
|
+
v['candidate_id'],
|
|
12
|
+
v
|
|
13
|
+
]));
|
|
14
|
+
assert.equal(verdicts['dali']['feasible'], false);
|
|
15
|
+
assert.ok(verdicts['dali']['unsat_core'].includes('duration'));
|
|
16
|
+
const wp = verdicts['dali']['wish_pool'];
|
|
17
|
+
assert.ok(wp, 'dali wish pool exists');
|
|
18
|
+
assert.ok(wp['conditions']['days'] >= 5);
|
|
19
|
+
assert.ok('budget_cny' in wp['conditions']);
|
|
20
|
+
assert.ok(wp['conditions']['best_months'].length > 0);
|
|
21
|
+
assert.ok(verdicts['dali']['suggestions'].length > 0);
|
|
22
|
+
assert.ok(verdicts['dali']['suggestions'].some((s)=>s['text'].includes('天')));
|
|
23
|
+
assert.equal(verdicts['qiandao']['feasible'], true);
|
|
24
|
+
assert.equal(result.recommended, 'qiandao');
|
|
25
|
+
const t = verdicts['qiandao']['true_cost'];
|
|
26
|
+
assert.ok(t['money_cny'] <= 3000);
|
|
27
|
+
assert.ok(Number(String(t['wake']).replace(':', '')) >= 630);
|
|
28
|
+
assert.ok(t['energy_arrival_pct'] >= 40);
|
|
29
|
+
assert.ok(t['usable_hours'] >= requiredUsableHours(req.motivation));
|
|
30
|
+
assert.equal(verdicts['taihu']['feasible'], true);
|
|
31
|
+
assert.ok(verdicts['taihu']['true_cost']['money_cny'] <= verdicts['qiandao']['true_cost']['money_cny']);
|
|
32
|
+
assert.ok(result.answer_md.includes('千岛湖'));
|
|
33
|
+
assert.ok(result.answer_md.includes('下一次出发'));
|
|
34
|
+
assert.ok(result.answer_md.includes('待你决定的两个问题'));
|
|
35
|
+
const tight = structuredClone(payload);
|
|
36
|
+
tight['request']['budget_cny'] = 900;
|
|
37
|
+
const tightReq = parseRequest(tight['request']);
|
|
38
|
+
const v = await solveCandidate(candidates.find((c)=>c.id === 'qiandao'), tightReq);
|
|
39
|
+
assert.equal(v.feasible, false);
|
|
40
|
+
assert.ok(v.unsatCore.includes('budget'));
|
|
41
|
+
assert.ok(v.suggestions.some((s)=>s.text.includes('预算提高到')));
|
|
42
|
+
console.log('TS ENGINE TESTS: 8/8 OK');
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/engine-tests.ts
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { writeFile, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { callHbcliJson, searchHotels } from '../capabilities/hbcli.js';
|
|
6
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hbcli-test-'));
|
|
7
|
+
async function fakeBin(name, code, payload) {
|
|
8
|
+
const p = join(tmp, name);
|
|
9
|
+
await writeFile(p, `#!/bin/sh\necho '${payload}'\nexit ${code}\n`, {
|
|
10
|
+
mode: 0o755
|
|
11
|
+
});
|
|
12
|
+
return p;
|
|
13
|
+
}
|
|
14
|
+
const okBin = await fakeBin('hbcli-ok', 0, '{"hotels":[{"id":"h1","name":"x"}]}');
|
|
15
|
+
const failBin = await fakeBin('hbcli-fail', 1, 'certificate has expired');
|
|
16
|
+
const failBin2 = join(tmp, 'hbcli-fail2');
|
|
17
|
+
await writeFile(failBin2, `#!/bin/sh\nexit 1\n`, {
|
|
18
|
+
mode: 0o755
|
|
19
|
+
});
|
|
20
|
+
const r1 = await callHbcliJson([
|
|
21
|
+
'search',
|
|
22
|
+
'hotel-list',
|
|
23
|
+
'--json'
|
|
24
|
+
], {
|
|
25
|
+
hbcliBin: okBin
|
|
26
|
+
});
|
|
27
|
+
assert.equal(r1.via, 'hbcli-realtime', 'happy path');
|
|
28
|
+
assert.deepEqual(r1.result, {
|
|
29
|
+
hotels: [
|
|
30
|
+
{
|
|
31
|
+
id: 'h1',
|
|
32
|
+
name: 'x'
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
});
|
|
36
|
+
assert.match(r1.evidence, /^\[实时API:hbcli@/);
|
|
37
|
+
assert.ok(r1.latencyMs >= 0);
|
|
38
|
+
const r2 = await callHbcliJson([
|
|
39
|
+
'search',
|
|
40
|
+
'hotel-list',
|
|
41
|
+
'--json'
|
|
42
|
+
], {
|
|
43
|
+
hbcliBin: failBin2,
|
|
44
|
+
timeoutMs: 5000
|
|
45
|
+
});
|
|
46
|
+
assert.equal(r2.via, 'hbcli-error', 'error path');
|
|
47
|
+
assert.match(r2.evidence, /\[实时API:hbcli@error@/, `evidence got: ${r2.evidence}`);
|
|
48
|
+
assert.equal(r2.exitCode, 1, '失败时 exitCode 透传');
|
|
49
|
+
assert.ok(r2.error, '有 error 字段');
|
|
50
|
+
const r3 = await callHbcliJson([
|
|
51
|
+
'search',
|
|
52
|
+
'hotel-list',
|
|
53
|
+
'--json'
|
|
54
|
+
], {
|
|
55
|
+
hbcliBin: '/nope/hbcli',
|
|
56
|
+
timeoutMs: 5000
|
|
57
|
+
});
|
|
58
|
+
assert.equal(r3.via, 'hbcli-error', 'no-binary path');
|
|
59
|
+
assert.match(r3.evidence, /spawn_error/, `evidence got: ${r3.evidence}`);
|
|
60
|
+
const fallback = join(tmp, 'hotels-fallback.json');
|
|
61
|
+
await writeFile(fallback, JSON.stringify({
|
|
62
|
+
meta: 'fake',
|
|
63
|
+
stays: [
|
|
64
|
+
{
|
|
65
|
+
id: 's1'
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}));
|
|
69
|
+
const r4 = await searchHotels({
|
|
70
|
+
destination: '普吉'
|
|
71
|
+
}, {
|
|
72
|
+
hbcliBin: failBin2,
|
|
73
|
+
fallbackPath: fallback
|
|
74
|
+
});
|
|
75
|
+
assert.equal(r4.via, 'hbcli-error', 'searchHotels: fails to hbcli → fallback');
|
|
76
|
+
assert.equal(r4.summary.includes('降级到静态包'), true, 'summary 指明降级');
|
|
77
|
+
assert.ok(r4.hotels, 'hotels 字段填上静态包内容');
|
|
78
|
+
await rm(tmp, {
|
|
79
|
+
recursive: true,
|
|
80
|
+
force: true
|
|
81
|
+
});
|
|
82
|
+
console.log('HBCLI TESTS: 4/4 OK (happy path / error path / no-binary / fallback)');
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/hbcli-tests.ts
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname } from 'node:path';
|
|
8
|
+
import { recordIncident, resolveIncidentsPath } from '../capabilities/incident-log.js';
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const tmp = await mkdtemp(join(tmpdir(), 'incident-test-'));
|
|
11
|
+
const path1 = resolveIncidentsPath(tmp);
|
|
12
|
+
recordIncident({
|
|
13
|
+
ts: '2026-08-22T00:00:00Z',
|
|
14
|
+
kind: 'uncaughtException',
|
|
15
|
+
message: 'synthetic',
|
|
16
|
+
stack: 'no-stack'
|
|
17
|
+
}, tmp);
|
|
18
|
+
const content = await readFile(path1, 'utf-8');
|
|
19
|
+
const written = JSON.parse(content.trim().split('\n')[0]);
|
|
20
|
+
assert.equal(written.kind, 'uncaughtException');
|
|
21
|
+
assert.equal(written.message, 'synthetic');
|
|
22
|
+
assert.equal(written.stack, 'no-stack');
|
|
23
|
+
console.log('UNIT 1 OK: recordIncident 同步 fsync 写盘');
|
|
24
|
+
const childCode = `
|
|
25
|
+
import { installProcessGuards } from '${here}/../capabilities/incident-log.ts'
|
|
26
|
+
installProcessGuards('${tmp}', { uncaughtException: 'test-child' })
|
|
27
|
+
setTimeout(() => { throw new Error('boom-from-child') }, 50)
|
|
28
|
+
setTimeout(() => { console.log('CHILD_DONE'); process.exit(0) }, 400)
|
|
29
|
+
`;
|
|
30
|
+
const childPath = join(tmp, 'child.mjs');
|
|
31
|
+
const { writeFile } = await import('node:fs/promises');
|
|
32
|
+
await writeFile(childPath, childCode);
|
|
33
|
+
const result = await new Promise((resolve, reject)=>{
|
|
34
|
+
const child = spawn(process.execPath, [
|
|
35
|
+
'--experimental-strip-types',
|
|
36
|
+
childPath
|
|
37
|
+
], {
|
|
38
|
+
stdio: [
|
|
39
|
+
'ignore',
|
|
40
|
+
'pipe',
|
|
41
|
+
'pipe'
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
let stdout = '', stderr = '';
|
|
45
|
+
child.stdout.on('data', (d)=>{
|
|
46
|
+
stdout += d.toString();
|
|
47
|
+
});
|
|
48
|
+
child.stderr.on('data', (d)=>{
|
|
49
|
+
stderr += d.toString();
|
|
50
|
+
});
|
|
51
|
+
child.on('close', (code)=>resolve({
|
|
52
|
+
stdout,
|
|
53
|
+
stderr,
|
|
54
|
+
code
|
|
55
|
+
}));
|
|
56
|
+
child.on('error', reject);
|
|
57
|
+
});
|
|
58
|
+
assert.equal(result.code, 0, `子进程应为 graceful exit,但拿到 ${result.code}: ${result.stderr.slice(0, 200)}`);
|
|
59
|
+
assert.match(result.stdout, /CHILD_DONE/, `子进程应触发我的 400ms setTimeout: stdout=${result.stdout} stderr=${result.stderr.slice(0, 200)}`);
|
|
60
|
+
const childLogPath = resolveIncidentsPath(tmp);
|
|
61
|
+
const childLogContent = await readFile(childLogPath, 'utf-8');
|
|
62
|
+
const lines = childLogContent.trim().split('\n').filter(Boolean);
|
|
63
|
+
assert.ok(lines.length >= 1, `应至少写一行 incident, 实际为: ${childLogContent}`);
|
|
64
|
+
const lastLine = JSON.parse(lines.at(-1));
|
|
65
|
+
assert.equal(lastLine.kind, 'uncaughtException', `kind 应是 uncaughtException,实际: ${lastLine.kind}`);
|
|
66
|
+
assert.match(lastLine.message, /boom-from-child/, `应记录错误消息,实际: ${lastLine.message}`);
|
|
67
|
+
assert.equal(lastLine.source, 'test-child', `source 来自 labels.uncaughtException,实际: ${lastLine.source}`);
|
|
68
|
+
console.log('INTEG 1 OK: 子进程 uncaughtException → fsync 写盘,handler 不阻塞 graceful exit');
|
|
69
|
+
{
|
|
70
|
+
const { guardToolExecute } = await import('../capabilities/incident-log.js');
|
|
71
|
+
const asyncBoom = guardToolExecute('synthetic-tool', tmp, async ()=>{
|
|
72
|
+
throw new Error('boom-async');
|
|
73
|
+
});
|
|
74
|
+
const r1 = await asyncBoom({
|
|
75
|
+
x: 1
|
|
76
|
+
}, undefined);
|
|
77
|
+
assert.equal(r1.ok, false, '异步抛错应降级为结构化错误,不向上抛');
|
|
78
|
+
assert.ok(String(r1.summary).includes('synthetic-tool'), 'summary 指明来源工具');
|
|
79
|
+
assert.ok(String(r1.evidence).includes('tool_execute_error'), 'evidence 带 incident 标记');
|
|
80
|
+
const syncBoom = guardToolExecute('synthetic-tool-sync', tmp, ()=>{
|
|
81
|
+
throw new Error('boom-sync');
|
|
82
|
+
});
|
|
83
|
+
const r2 = await syncBoom({}, undefined);
|
|
84
|
+
assert.equal(r2.ok, false, '同步抛错同样隔离');
|
|
85
|
+
const guardedLog = (await readFile(path1, 'utf8')).trim().split('\n').filter(Boolean);
|
|
86
|
+
const toolErrs = guardedLog.map((l)=>JSON.parse(l)).filter((x)=>x.kind === 'tool_execute_error');
|
|
87
|
+
assert.equal(toolErrs.length, 2, '两条 tool_execute_error 均应落盘');
|
|
88
|
+
console.log('UNIT 2 OK: guardToolExecute 同步/异步异常隔离 + incident 落盘');
|
|
89
|
+
}await rm(tmp, {
|
|
90
|
+
recursive: true,
|
|
91
|
+
force: true
|
|
92
|
+
});
|
|
93
|
+
console.log('INCIDENT TESTS: 3/3 OK');
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/incident-tests.ts
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { solveJourney, parseFlightPackLegs } from '../src/journey.js';
|
|
5
|
+
import { hhmmToMin } from '../src/model.js';
|
|
6
|
+
const pack = JSON.parse(await readFile(join('..', 'data', 'flights_2026.json'), 'utf-8'));
|
|
7
|
+
const legs = parseFlightPackLegs(pack);
|
|
8
|
+
const r1 = await solveJourney({
|
|
9
|
+
legs,
|
|
10
|
+
budgetCny: 9000
|
|
11
|
+
});
|
|
12
|
+
assert.equal(r1.feasible, true);
|
|
13
|
+
assert.equal(r1.money_cny, r1.legs.reduce((a, l)=>a + l.price_cny, 0));
|
|
14
|
+
assert.ok(r1.money_cny >= 7300 && r1.money_cny <= 8550, `有效解区间: ${r1.money_cny}(M-4 reconcile 已知 f3=FD582 路径下限; demo 上限 8550)`);
|
|
15
|
+
const f5 = r1.legs.find((l)=>l.service === 'EK329');
|
|
16
|
+
assert.equal(f5.energy_pct, 75);
|
|
17
|
+
assert.ok(f5.wake.includes('前一日'), `wake 跨日显示: ${f5.wake}`);
|
|
18
|
+
const f4 = r1.legs.find((l)=>[
|
|
19
|
+
'MU5233',
|
|
20
|
+
'ZH9108',
|
|
21
|
+
'DZ6252'
|
|
22
|
+
].includes(l.service));
|
|
23
|
+
assert.ok(f4 !== undefined, 'f4 没产出任何候选');
|
|
24
|
+
console.log(`3. f4 → ${f4.service}`);
|
|
25
|
+
const tight = legs.map((l)=>l.id === 'f1' ? {
|
|
26
|
+
...l,
|
|
27
|
+
arriveByMin: hhmmToMin('15:00')
|
|
28
|
+
} : l);
|
|
29
|
+
const r2 = await solveJourney({
|
|
30
|
+
legs: tight
|
|
31
|
+
});
|
|
32
|
+
assert.equal(r2.feasible, false);
|
|
33
|
+
assert.ok(r2.unsat_core.includes('f1:arrive_by'));
|
|
34
|
+
assert.ok(r2.suggestions.some((sg)=>sg.relax === 'f1:arrive_by'));
|
|
35
|
+
const r3 = await solveJourney({
|
|
36
|
+
legs,
|
|
37
|
+
budgetCny: 1000
|
|
38
|
+
});
|
|
39
|
+
assert.equal(r3.feasible, false);
|
|
40
|
+
assert.ok(r3.unsat_core.includes('total:budget'));
|
|
41
|
+
assert.ok(r3.suggestions.some((sg)=>sg.relax === 'total:budget'));
|
|
42
|
+
console.log(`TS JOURNEY TESTS: 5/5 OK(五段全链可行,¥${r1.money_cny},与 Python oracle 结构一致)`);
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/journey-tests.ts
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const BASE = 'https://opensky-network.org/api';
|
|
2
|
+
async function checkArrival(callsign, airport) {
|
|
3
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4
|
+
const url = airport ? `${BASE}/arrivals?airport=${airport}&begin=${now - 7 * 86400}&end=${now}` : `${BASE}/arrivals?airport=OMDB&begin=${now - 7 * 86400}&end=${now}`;
|
|
5
|
+
const res = await fetch(url, {
|
|
6
|
+
headers: {
|
|
7
|
+
Accept: 'application/json'
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
if (!res.ok) return `[校验不可用:opensky ${res.status}]`;
|
|
11
|
+
const data = await res.json();
|
|
12
|
+
const hit = data.find((a)=>(a.callsign ?? '').trim().toUpperCase().includes(callsign.toUpperCase()));
|
|
13
|
+
const ts = new Date().toISOString();
|
|
14
|
+
return hit ? `[实时API:opensky@${ts}] ✅ ${callsign} 近 7 天执飞记录:到达 ${hit.arrivalAirport ?? '?'}(callsign ${hit.callsign?.trim()})` : `[实时API:opensky@${ts}] ○ 近 7 天 ${airport ?? 'OMDB'} 到达列表未见 ${callsign}(ADS-B 覆盖有限,不作否定结论)`;
|
|
15
|
+
}
|
|
16
|
+
const target = process.argv[2] ?? 'EK329';
|
|
17
|
+
console.log(await checkArrival(target, process.argv[3] ?? null));
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/opensky-check.ts
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { verifyFlight } from '../capabilities/opensky.js';
|
|
3
|
+
const live = await verifyFlight({
|
|
4
|
+
callsign: 'EK329',
|
|
5
|
+
airport: 'OMDB'
|
|
6
|
+
});
|
|
7
|
+
assert.ok(live.sampleSize >= 0, 'sampleSize ≥ 0(空数组也合法)');
|
|
8
|
+
assert.ok([
|
|
9
|
+
'observed',
|
|
10
|
+
'not_observed',
|
|
11
|
+
'unavailable'
|
|
12
|
+
].includes(live.verdict), `verdict 三值之一:${live.verdict}`);
|
|
13
|
+
if (live.verdict === 'observed') {
|
|
14
|
+
assert.ok((live.hits?.length ?? 0) > 0, 'observed 必须配 hits');
|
|
15
|
+
const h = live.hits[0];
|
|
16
|
+
assert.ok(typeof h.icao24 === 'string' && h.icao24.length > 0, 'icao24 存在');
|
|
17
|
+
assert.match(live.evidence, /\[实时API:opensky@/, '证据链标注');
|
|
18
|
+
console.log(`1. EK329 OMDB → ${live.verdict} (sample=${live.sampleSize}, hits=${live.hits?.length}) OK`);
|
|
19
|
+
} else if (live.verdict === 'not_observed') {
|
|
20
|
+
assert.match(live.evidence, /○|not_observed|未见/, 'not_observed 标注 ○');
|
|
21
|
+
console.log(`1. EK329 OMDB → not_observed 当前观测窗没飞 (o) OK`);
|
|
22
|
+
} else {
|
|
23
|
+
assert.match(live.evidence, /error/, 'unavailable 标注 error');
|
|
24
|
+
console.log(`1. EK329 OMDB → unavailable (API 异常,降级合法) OK`);
|
|
25
|
+
}
|
|
26
|
+
const fake = await verifyFlight({
|
|
27
|
+
callsign: 'XXDODO1234',
|
|
28
|
+
airport: 'OMDB'
|
|
29
|
+
});
|
|
30
|
+
assert.ok([
|
|
31
|
+
'not_observed',
|
|
32
|
+
'unavailable'
|
|
33
|
+
].includes(fake.verdict), `期望 not_observed/unavailable,实际 ${fake.verdict}`);
|
|
34
|
+
if (fake.verdict === 'not_observed') {
|
|
35
|
+
assert.match(fake.evidence, /○/, 'not_observed 标 ○');
|
|
36
|
+
console.log('2. XXDODO1234 OMDB → not_observed (○) OK');
|
|
37
|
+
} else {
|
|
38
|
+
console.log('2. XXDODO1234 OMDB → unavailable (API 限流 OK) OK');
|
|
39
|
+
}
|
|
40
|
+
import { verifyFlight as vf } from '../capabilities/opensky.js';
|
|
41
|
+
const to = await vf({
|
|
42
|
+
callsign: 'EK329',
|
|
43
|
+
airport: 'OMDB',
|
|
44
|
+
timeoutMs: 1
|
|
45
|
+
});
|
|
46
|
+
assert.equal(to.verdict, 'unavailable', `timeout 应 unavailable,实际 ${to.verdict}`);
|
|
47
|
+
assert.match(to.evidence, /error/, 'unavailable 证据带 error');
|
|
48
|
+
console.log('3. timeout → unavailable OK');
|
|
49
|
+
console.log('\nOPEN-SKY TESTS: 3/3 OK(免费匿名 OpenSky API)');
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/opensky-tests.ts
|