@danceiny/gotry 0.0.1-rc.10
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/LICENSE +21 -0
- package/README.md +267 -0
- package/bin/gotry-inner.js +214 -0
- package/bin/gotry-stdio-ask.js +59 -0
- package/bin/gotry.js +28 -0
- package/cordis.gotry-patch.yml +86 -0
- package/data/flights_2026.json +218 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +151 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +202 -0
- package/dist/capabilities/agent-reach-bridge.py +90 -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/flyai.js +131 -0
- package/dist/capabilities/hbcli.js +143 -0
- package/dist/capabilities/incident-log.js +100 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/session/action-cache.js +120 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
- package/dist/capabilities/session/adapters/meituan-local.js +73 -0
- package/dist/capabilities/session/extract.js +40 -0
- package/dist/capabilities/session/read-guard.js +99 -0
- package/dist/capabilities/session/transport.js +101 -0
- package/dist/capabilities/session-search.js +93 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/action-cache-tests.js +107 -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 +37 -0
- package/dist/scripts/companion-tests.js +112 -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 +101 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/ledger-tests.js +365 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +24 -0
- package/dist/scripts/nudge-digest.js +83 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +88 -0
- package/dist/scripts/publish-preverify.js +68 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +116 -0
- package/dist/scripts/session-attach-diagnose.js +27 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/skeleton-check.js +37 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +328 -0
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +248 -0
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/scripts/travel-timeline-tests.js +123 -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/companions.js +112 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +174 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +1239 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +360 -0
- package/dist/src/memory-capture.js +40 -0
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/memory-utility.js +55 -0
- package/dist/src/mock-llm.js +103 -0
- package/dist/src/model.js +134 -0
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/state-ledger.js +901 -0
- package/dist/src/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +538 -0
- package/dist/src/wish-pool.js +27 -0
- package/package.json +84 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +158 -0
- package/ts/capabilities/anything.ts +201 -0
- package/ts/capabilities/flyai.ts +151 -0
- package/ts/capabilities/hbcli.ts +152 -0
- package/ts/capabilities/incident-log.ts +148 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/transport.ts +114 -0
- package/ts/capabilities/session-search.ts +129 -0
- package/ts/capabilities/weather.ts +176 -0
- package/ts/package.json +27 -0
- package/ts/scripts/async-collect.ts +50 -0
- package/ts/scripts/skeleton-check.ts +49 -0
- package/ts/scripts/skeleton-integration-test.ts +23 -0
- package/ts/scripts/state-cli.ts +201 -0
- package/ts/src/bridge.ts +44 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +165 -0
- package/ts/src/dsh-llm.ts +155 -0
- package/ts/src/index.ts +968 -0
- package/ts/src/loop.ts +411 -0
- package/ts/src/memory-capture.ts +59 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +100 -0
- package/ts/src/model.ts +220 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +849 -0
- package/ts/src/time-anchor.ts +134 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/unified.ts +582 -0
- package/ts/src/wish-pool.ts +66 -0
|
@@ -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.length > 200, 'content 非空 markdown');
|
|
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,37 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
import { collectDeepPlanning, loadAsyncTicket, makeJournaledSolvePort, settleAsyncTicket } from '../src/loop.js';
|
|
3
|
+
import { solveUnified } from '../src/unified.js';
|
|
4
|
+
import { openLedgerIfExists } from '../src/state-ledger.js';
|
|
5
|
+
const ticketId = process.argv[2];
|
|
6
|
+
const stateRoot = process.argv[3] ?? '.';
|
|
7
|
+
if (!ticketId) {
|
|
8
|
+
console.error('用法:npx tsx scripts/async-collect.ts <ticketId> [stateRoot](权威在账本 workflow_runs,视图在 gotry-state/async/)');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const loaded = await loadAsyncTicket(ticketId, stateRoot);
|
|
12
|
+
if (!loaded) {
|
|
13
|
+
console.error(`工单 ${ticketId} 不存在(账本 workflow_runs / gotry-state/async/${ticketId}.json)`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
17
|
+
const run = ledger?.getWorkflowRun(ticketId);
|
|
18
|
+
if (run?.status === 'settled' && run.deliverable) {
|
|
19
|
+
console.log(`工单 ${ticketId} 已交付(账本终态,复诵不重算):\n\n${run.deliverable}`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
if (!ledger) {
|
|
23
|
+
console.error(`工单 ${ticketId}:stateRoot(${stateRoot})无账本——持久化先于回收,不应发生`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const solve = makeJournaledSolvePort(ledger, ticketId, solveUnified, {
|
|
27
|
+
onRealSolve: ()=>{
|
|
28
|
+
const f = process.env['GOTRY_SOLVE_COUNT_FILE'];
|
|
29
|
+
if (f) appendFileSync(f, 'solve\n');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const { reply } = await collectDeepPlanning(loaded.state, loaded.ticket, solve);
|
|
33
|
+
const out = await settleAsyncTicket(ticketId, reply, stateRoot);
|
|
34
|
+
console.log(`交付物已落盘:${out}\n\n${reply}`);
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/async-collect.ts
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { companionId, sensitiveViolation, upsertCompanion } from '../src/companions.js';
|
|
3
|
+
let n = 0;
|
|
4
|
+
function pass(name, body) {
|
|
5
|
+
body();
|
|
6
|
+
console.log(` ${++n}. ${name} OK`);
|
|
7
|
+
}
|
|
8
|
+
pass('负面清单守卫:证件号/手机号拒收入库(红线 6 工程形态)', ()=>{
|
|
9
|
+
assert.ok(sensitiveViolation('护照号 E12345678')?.includes('负面清单'), '证件拒');
|
|
10
|
+
assert.ok(sensitiveViolation('我电话 13812345678')?.includes('负面清单'), '手机号拒');
|
|
11
|
+
assert.equal(sensitiveViolation('爸爸65,轻度高血压,走不动山路'), null, '行为约束正常');
|
|
12
|
+
const r = upsertCompanion([], {
|
|
13
|
+
label: '爸爸',
|
|
14
|
+
constraints: {
|
|
15
|
+
health: [
|
|
16
|
+
'轻度高血压'
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
evidence: '身份证号 110101199001011234,轻度高血压'
|
|
20
|
+
});
|
|
21
|
+
assert.equal(r.appended, false);
|
|
22
|
+
assert.ok(r.reason?.includes('负面清单'));
|
|
23
|
+
});
|
|
24
|
+
pass('新建:约束+证据落库,companion_id 语义派生', ()=>{
|
|
25
|
+
const r = upsertCompanion([], {
|
|
26
|
+
label: '爸 爸',
|
|
27
|
+
constraints: {
|
|
28
|
+
health: [
|
|
29
|
+
'轻度高血压'
|
|
30
|
+
],
|
|
31
|
+
mobility: '步行≤4h'
|
|
32
|
+
},
|
|
33
|
+
evidence: '爸爸65有轻度高血压,别太累'
|
|
34
|
+
});
|
|
35
|
+
assert.equal(r.appended, true);
|
|
36
|
+
assert.equal(r.companionId, '爸爸');
|
|
37
|
+
const p = r.profiles[0];
|
|
38
|
+
assert.deepEqual(p.constraints.health, [
|
|
39
|
+
'轻度高血压'
|
|
40
|
+
]);
|
|
41
|
+
assert.deepEqual(p.evidence, [
|
|
42
|
+
'爸爸65有轻度高血压,别太累'
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
pass('upsert 合并:数组追加不删史/覆盖取新/幂等', ()=>{
|
|
46
|
+
let ps = [];
|
|
47
|
+
ps = upsertCompanion(ps, {
|
|
48
|
+
label: '爸爸',
|
|
49
|
+
constraints: {
|
|
50
|
+
health: [
|
|
51
|
+
'轻度高血压'
|
|
52
|
+
],
|
|
53
|
+
mobility: '步行≤4h'
|
|
54
|
+
},
|
|
55
|
+
evidence: '原话一'
|
|
56
|
+
}).profiles;
|
|
57
|
+
ps = upsertCompanion(ps, {
|
|
58
|
+
label: '爸爸',
|
|
59
|
+
constraints: {
|
|
60
|
+
health: [
|
|
61
|
+
'晕车'
|
|
62
|
+
],
|
|
63
|
+
mobility: '步行≤3h'
|
|
64
|
+
},
|
|
65
|
+
evidence: '原话二:晕车'
|
|
66
|
+
}).profiles;
|
|
67
|
+
const p = ps.find((x)=>x.companion_id === '爸爸');
|
|
68
|
+
assert.deepEqual(p.constraints.health, [
|
|
69
|
+
'轻度高血压',
|
|
70
|
+
'晕车'
|
|
71
|
+
], '数组追加不删史');
|
|
72
|
+
assert.equal(p.constraints.mobility, '步行≤3h', '标量取新值');
|
|
73
|
+
assert.equal(p.evidence.length, 2, '证据两条');
|
|
74
|
+
const r3 = upsertCompanion(ps, {
|
|
75
|
+
label: '爸爸',
|
|
76
|
+
constraints: {
|
|
77
|
+
health: [
|
|
78
|
+
'晕车'
|
|
79
|
+
],
|
|
80
|
+
mobility: '步行≤3h'
|
|
81
|
+
},
|
|
82
|
+
evidence: '原话二:晕车'
|
|
83
|
+
});
|
|
84
|
+
assert.equal(r3.appended, false, '全量相同幂等 no-op');
|
|
85
|
+
});
|
|
86
|
+
pass('多人并存与 id 稳定性', ()=>{
|
|
87
|
+
let ps = [];
|
|
88
|
+
ps = upsertCompanion(ps, {
|
|
89
|
+
label: '爸爸',
|
|
90
|
+
constraints: {
|
|
91
|
+
health: [
|
|
92
|
+
'高血压'
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
evidence: 'a'
|
|
96
|
+
}).profiles;
|
|
97
|
+
ps = upsertCompanion(ps, {
|
|
98
|
+
label: '女朋友',
|
|
99
|
+
constraints: {
|
|
100
|
+
prefs: [
|
|
101
|
+
'怕吵'
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
evidence: 'b'
|
|
105
|
+
}).profiles;
|
|
106
|
+
assert.equal(ps.length, 2);
|
|
107
|
+
assert.equal(companionId('女 朋友'), '女朋友');
|
|
108
|
+
});
|
|
109
|
+
console.log(`\nCOMPANION TESTS: ${n}/4 OK(memory-design P2 守门面)`);
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/companion-tests.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,101 @@
|
|
|
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
|
+
{
|
|
70
|
+
const { writeFileSync, chmodSync } = await import('node:fs');
|
|
71
|
+
const echoBin = join(tmp, 'hbcli-echo');
|
|
72
|
+
writeFileSync(echoBin, '#!/bin/sh\necho "ARGS:$@"; echo \'{}\' ');
|
|
73
|
+
chmodSync(echoBin, 0o755);
|
|
74
|
+
const re = await searchHotels({
|
|
75
|
+
destination: '普吉',
|
|
76
|
+
adults: 2
|
|
77
|
+
}, {
|
|
78
|
+
hbcliBin: echoBin
|
|
79
|
+
});
|
|
80
|
+
const argLine = JSON.stringify(re);
|
|
81
|
+
if (!argLine.includes('destination-name') || !argLine.includes('room-occupancies')) {
|
|
82
|
+
throw new Error(`FAIL: v0.3.0 旗标未对齐,实际 ${argLine.slice(0, 200)}`);
|
|
83
|
+
}
|
|
84
|
+
console.log('5. v0.3.0 旗标(--destination-name/--room-occupancies)对齐 OK');
|
|
85
|
+
}const r4 = await searchHotels({
|
|
86
|
+
destination: '普吉'
|
|
87
|
+
}, {
|
|
88
|
+
hbcliBin: failBin2,
|
|
89
|
+
fallbackPath: fallback
|
|
90
|
+
});
|
|
91
|
+
assert.equal(r4.via, 'hbcli-error', 'searchHotels: fails to hbcli → fallback');
|
|
92
|
+
assert.equal(r4.summary.includes('降级到静态包'), true, 'summary 指明降级');
|
|
93
|
+
assert.ok(r4.hotels, 'hotels 字段填上静态包内容');
|
|
94
|
+
await rm(tmp, {
|
|
95
|
+
recursive: true,
|
|
96
|
+
force: true
|
|
97
|
+
});
|
|
98
|
+
console.log('HBCLI TESTS: 5/5 OK (happy / error / no-binary / fallback / v0.3.0 旗标回归)');
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
//# 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
|