@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.19
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 +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +236 -3
- package/bin/gotry-inner.js +66 -20
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +15 -5
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +285 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
- package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +87 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +121 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +7 -4
- package/dist/src/benchmark-environment-bridge.js +68 -7
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +735 -149
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +6 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/package.json +1 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +14 -6
- package/ts/src/benchmark-environment-bridge.ts +29 -10
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +489 -171
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +361 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
package/dist/scripts/smoke.js
CHANGED
|
@@ -45,10 +45,10 @@ async function main() {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
}, null);
|
|
48
|
-
if (rejectedMotivation.ok !== false ||
|
|
49
|
-
throw new Error(`FAIL: profile without evidence should have been rejected, actual ${JSON.stringify(rejectedMotivation).slice(0,
|
|
48
|
+
if (rejectedMotivation.ok !== false || !/evidence/i.test(rejectedMotivation.summary ?? '')) {
|
|
49
|
+
throw new Error(`FAIL: profile without evidence should have been rejected, actual ${JSON.stringify(rejectedMotivation).slice(0, 160)}`);
|
|
50
50
|
}
|
|
51
|
-
console.log(`motivation without evidence rejected: ${rejectedMotivation.summary.slice(0, 80)}...`);
|
|
51
|
+
console.log(`motivation without evidence rejected: ${(rejectedMotivation.summary ?? '').slice(0, 80)}...`);
|
|
52
52
|
const saved = await motivation.execute({
|
|
53
53
|
profile: {
|
|
54
54
|
weights: {
|
|
@@ -112,11 +112,9 @@ async function main() {
|
|
|
112
112
|
}
|
|
113
113
|
const ar = byName('gotry_agent_reach');
|
|
114
114
|
const arView = ar.presentResult({
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
method: 'get_hot_topics'
|
|
119
|
-
}
|
|
115
|
+
action: 'reach',
|
|
116
|
+
channel: 'v2ex',
|
|
117
|
+
method: 'get_hot_topics'
|
|
120
118
|
}, {
|
|
121
119
|
verdict: 'found',
|
|
122
120
|
summary: '10 topics'
|
|
@@ -127,22 +125,12 @@ async function main() {
|
|
|
127
125
|
const r1 = await ws.execute({
|
|
128
126
|
query: 'not-a-url'
|
|
129
127
|
}, null);
|
|
128
|
+
if (!(r1.ok === false && !!r1.evidence)) throw new Error(`FAIL: 字符串 blob 形态应被宿主权结构化拒绝(ToolFailure),实际 ${JSON.stringify(r1).slice(0, 160)}`);
|
|
130
129
|
const r2 = await ws.execute({
|
|
131
130
|
url: 'not-a-url'
|
|
132
131
|
}, null);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
'string',
|
|
136
|
-
r1
|
|
137
|
-
],
|
|
138
|
-
[
|
|
139
|
-
'bare-obj',
|
|
140
|
-
r2
|
|
141
|
-
]
|
|
142
|
-
]){
|
|
143
|
-
if (r.summary === 'url 必填') throw new Error(`FAIL: ${name} 形态未被 unwrapQuery 接住`);
|
|
144
|
-
}
|
|
145
|
-
console.log('unwrapQuery: string + bare-object shapes both accepted');
|
|
132
|
+
if (r2.summary === 'url 必填') throw new Error(`FAIL: 平铺 url 应进 execute 走降级,而非参数闸,实际 ${r2.summary}`);
|
|
133
|
+
console.log('D-30 typed 契约:web_search 字符串 blob → 宿主权拒绝;平铺 url → 正常降级');
|
|
146
134
|
const skTool = byName('gotry_skeleton_check');
|
|
147
135
|
const skFlat = await skTool.execute({
|
|
148
136
|
from: 'HKG',
|
|
@@ -160,20 +148,16 @@ async function main() {
|
|
|
160
148
|
{
|
|
161
149
|
const hotel = byName('gotry_hotel_search');
|
|
162
150
|
const resolved = await hotel.execute({
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
checkOut: '2026-09-06'
|
|
167
|
-
}
|
|
151
|
+
destination: '大理',
|
|
152
|
+
checkIn: '2026-9-4',
|
|
153
|
+
checkOut: '2026-09-06'
|
|
168
154
|
}, null);
|
|
169
155
|
if (!resolved.date_notes?.some((n)=>n.includes('2026-9-4 → 2026-09-04'))) {
|
|
170
156
|
throw new Error(`FAIL: 非规整 ISO 应产生 slot-resolved note,实际 ${JSON.stringify(resolved.date_notes)}`);
|
|
171
157
|
}
|
|
172
158
|
const unresolved = await hotel.execute({
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
checkIn: '近期'
|
|
176
|
-
}
|
|
159
|
+
destination: '大理',
|
|
160
|
+
checkIn: '近期'
|
|
177
161
|
}, null);
|
|
178
162
|
if (!unresolved.date_notes?.some((n)=>n.includes('日期未解析:近期'))) {
|
|
179
163
|
throw new Error(`FAIL: 词表外表达应产生「日期未解析」note(不猜),实际 ${JSON.stringify(unresolved.date_notes)}`);
|
|
@@ -290,16 +274,20 @@ async function main() {
|
|
|
290
274
|
}
|
|
291
275
|
{
|
|
292
276
|
const fa = await byName('gotry_flyai_search').execute({
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
date: '2026-10-01'
|
|
298
|
-
}
|
|
277
|
+
kind: 'flight',
|
|
278
|
+
from: '上海',
|
|
279
|
+
to: '丽江',
|
|
280
|
+
date: '2026-10-01'
|
|
299
281
|
}, null);
|
|
300
282
|
const faBlocked = fa.verdict === 'error' && /sentinel|block|trial limit/i.test(fa.error ?? '');
|
|
301
283
|
const faErrTerminal = fa.ok === false && fa.verdict === 'error' && /^flyai-error$/.test(String(fa.via ?? '')) && /\[实时API:flyai@error@/.test(String(fa.evidence ?? ''));
|
|
302
|
-
|
|
284
|
+
const faNeedsSetup = fa.ok === false && fa.verdict === 'needs-setup';
|
|
285
|
+
if (faNeedsSetup) {
|
|
286
|
+
if (!/FLYAI_API_KEY/.test(String(fa.setup ?? '')) || !/勿重试|不要重试|请勿重试/.test(String(fa.setup ?? ''))) {
|
|
287
|
+
throw new Error(`FAIL: flyai needs-setup 应带配 key 指引与勿重试明示,实际 setup:${String(fa.setup ?? '').slice(0, 200)}`);
|
|
288
|
+
}
|
|
289
|
+
console.log(' WARN - flyai 匿名试用额度达限(needs-setup,配置态),setup 指引合同通过(hit 断言跳过)');
|
|
290
|
+
} else if (faBlocked) {
|
|
303
291
|
console.log(' WARN - flyai 上游限流中(Sentinel/trial-limit 429),降级合同通过(hit 断言跳过)');
|
|
304
292
|
} else if (faErrTerminal) {
|
|
305
293
|
console.log(' WARN - flyai 端点不可达(超时/降级),证据链合同通过(hit 断言跳过)');
|
|
@@ -308,12 +296,10 @@ async function main() {
|
|
|
308
296
|
}
|
|
309
297
|
const prof = mkdtempSync(join(smokeRoot, 'sess-'));
|
|
310
298
|
const faPast = await byName('gotry_flyai_search').execute({
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
date: '2026-01-01'
|
|
316
|
-
}
|
|
299
|
+
kind: 'flight',
|
|
300
|
+
from: '深圳',
|
|
301
|
+
to: '普吉',
|
|
302
|
+
date: '2026-01-01'
|
|
317
303
|
}, null);
|
|
318
304
|
if (!(faPast.ok === false && /已是过去/.test(String(faPast.summary ?? '')))) {
|
|
319
305
|
throw new Error(`FAIL: flyai 过去日期应代码层预校验拒绝,实际:${JSON.stringify(faPast).slice(0, 200)}`);
|
|
@@ -327,11 +313,9 @@ async function main() {
|
|
|
327
313
|
process.env.CHROME_USER_DATA_DIR = prof;
|
|
328
314
|
try {
|
|
329
315
|
ss = await byName('gotry_session_search').execute({
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
date: '2026-10-01'
|
|
334
|
-
}
|
|
316
|
+
from: '上海',
|
|
317
|
+
to: '丽江',
|
|
318
|
+
date: '2026-10-01'
|
|
335
319
|
}, null);
|
|
336
320
|
} finally{
|
|
337
321
|
if (previousChromeUserDataDir === undefined) delete process.env.CHROME_USER_DATA_DIR;
|
|
@@ -349,38 +333,83 @@ async function main() {
|
|
|
349
333
|
const faOutcome = faBlocked ? 'sentinel-限流降级' : faErrTerminal ? '端点不可达降级' : `live hit(${fa.options?.length ?? 0} 条)`;
|
|
350
334
|
console.log(`session-face tools: flyai ${faOutcome}; session cdp 隔离门禁=needs-attach`);
|
|
351
335
|
const fh = await byName('gotry_flyai_search').execute({
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
checkOut: '2026-10-03'
|
|
357
|
-
}
|
|
336
|
+
kind: 'hotel',
|
|
337
|
+
to: '大理',
|
|
338
|
+
checkIn: '2026-10-01',
|
|
339
|
+
checkOut: '2026-10-03'
|
|
358
340
|
}, null);
|
|
359
341
|
const fhBlocked = fh.verdict === 'error' && /sentinel|block|trial limit/i.test(fh.error ?? '');
|
|
360
342
|
const fhErrTerminal = fh.ok === false && fh.verdict === 'error' && /^flyai-error$/.test(String(fh.via ?? '')) && /\[实时API:flyai@error@/.test(String(fh.evidence ?? ''));
|
|
361
|
-
|
|
343
|
+
const fhNeedsSetup = fh.ok === false && fh.verdict === 'needs-setup';
|
|
344
|
+
if (fhNeedsSetup) {
|
|
345
|
+
if (!/FLYAI_API_KEY/.test(String(fh.setup ?? ''))) {
|
|
346
|
+
throw new Error(`FAIL: flyai hotel needs-setup 应带配 key 指引,实际 setup:${String(fh.setup ?? '').slice(0, 200)}`);
|
|
347
|
+
}
|
|
348
|
+
console.log(' WARN - flyai hotel 试用额度达限(needs-setup,配置态),setup 指引合同通过(hit 断言跳过)');
|
|
349
|
+
} else if (fhBlocked || fhErrTerminal) {
|
|
362
350
|
console.log(' WARN - flyai hotel 限流/端点降级,证据链合同通过(hit 断言跳过)');
|
|
363
351
|
} else if (fh.ok !== true || fh.verdict !== 'hit' || (fh.hotels?.length ?? 0) < 1 || !/\[实时API:flyai@/.test(fh.evidence ?? '')) {
|
|
364
352
|
throw new Error(`FAIL: flyai hotel 应 live hit,实际:${JSON.stringify(fh).slice(0, 220)}`);
|
|
365
353
|
}
|
|
366
354
|
const fhDest = await byName('gotry_flyai_search').execute({
|
|
355
|
+
kind: 'hotel'
|
|
356
|
+
}, null);
|
|
357
|
+
if (fhDest.ok !== false) throw new Error('FAIL: hotel 缺目的地应参数闸拒绝');
|
|
358
|
+
const faLegacy = await byName('gotry_flyai_search').execute({
|
|
367
359
|
query: {
|
|
368
|
-
kind: '
|
|
360
|
+
kind: 'flight',
|
|
361
|
+
from: '上海',
|
|
362
|
+
to: '丽江',
|
|
363
|
+
date: '2026-10-01'
|
|
369
364
|
}
|
|
370
365
|
}, null);
|
|
371
|
-
if (
|
|
372
|
-
|
|
366
|
+
if (!(faLegacy.ok === false && !!faLegacy.evidence && /内部错误|参数|schema|violat/i.test(String(faLegacy.summary ?? '')))) {
|
|
367
|
+
throw new Error(`FAIL: flyai legacy blob 形态应被 typed 契约结构化拒绝(ADR-13 ToolFailure),实际:${JSON.stringify(faLegacy).slice(0, 200)}`);
|
|
368
|
+
}
|
|
369
|
+
const faBadEnum = await byName('gotry_flyai_search').execute({
|
|
370
|
+
kind: 'plane'
|
|
371
|
+
}, null);
|
|
372
|
+
if (!(faBadEnum.ok === false && !!faBadEnum.evidence)) {
|
|
373
|
+
throw new Error(`FAIL: flyai kind 枚举外值应被宿主权校验拒绝,实际:${JSON.stringify(faBadEnum).slice(0, 200)}`);
|
|
374
|
+
}
|
|
375
|
+
console.log(' D-30 typed 契约:legacy blob/枚举外值 → 宿主权结构化拒绝(ToolFailure 形状保持)');
|
|
376
|
+
const sh = await byName('gotry_session_search').execute({
|
|
377
|
+
kind: 'hotel',
|
|
378
|
+
to: '不在码表的城市'
|
|
379
|
+
}, null);
|
|
380
|
+
if (!(sh.ok === false && /city=/.test(String(sh.summary ?? '')))) {
|
|
381
|
+
throw new Error(`FAIL: 会话酒店未收录城市应带 cityId 指引,实际:${JSON.stringify(sh).slice(0, 200)}`);
|
|
382
|
+
}
|
|
383
|
+
console.log(' hotel kind 路由:未收录城市 → error + cityId 指引(transport 前短路)');
|
|
384
|
+
const st = await byName('gotry_session_search').execute({
|
|
385
|
+
kind: 'train',
|
|
386
|
+
from: '不在码表',
|
|
387
|
+
to: '也不在',
|
|
388
|
+
date: '2026-12-01'
|
|
389
|
+
}, null);
|
|
390
|
+
if (!(st.ok === false && /fromStationTelecode/.test(String(st.summary ?? '')))) {
|
|
391
|
+
throw new Error(`FAIL: 会话火车未收录电报码应带发现指引,实际:${JSON.stringify(st).slice(0, 200)}`);
|
|
392
|
+
}
|
|
393
|
+
console.log(' train kind 路由:未收录电报码 → error + 电报码指引(transport 前短路)');
|
|
394
|
+
const ssLegacy = await byName('gotry_session_search').execute({
|
|
373
395
|
query: {
|
|
374
|
-
kind: 'hotel'
|
|
375
|
-
to: '大理',
|
|
376
|
-
checkIn: '2026-01-01',
|
|
377
|
-
checkOut: '2026-01-03'
|
|
396
|
+
kind: 'hotel'
|
|
378
397
|
}
|
|
379
398
|
}, null);
|
|
399
|
+
if (!(ssLegacy.ok === false && !('routing' in ssLegacy) && /需要 to/.test(String(ssLegacy.summary ?? '')))) {
|
|
400
|
+
throw new Error(`FAIL: session legacy blob 应经容忍层归一走业务闸(结构化报错,无 routing),实际:${JSON.stringify(ssLegacy).slice(0, 200)}`);
|
|
401
|
+
}
|
|
402
|
+
console.log(' D-30 typed 契约:session legacy blob → 容忍层归一 + 结构化业务报错(全字段可选无法宿主权拒)');
|
|
403
|
+
const fhPast = await byName('gotry_flyai_search').execute({
|
|
404
|
+
kind: 'hotel',
|
|
405
|
+
to: '大理',
|
|
406
|
+
checkIn: '2026-01-01',
|
|
407
|
+
checkOut: '2026-01-03'
|
|
408
|
+
}, null);
|
|
380
409
|
if (!(fhPast.ok === false && /不是未来合法区间/.test(String(fhPast.summary ?? '')))) {
|
|
381
410
|
throw new Error(`FAIL: 酒店过去入住日应代码层预校验拒绝,实际:${JSON.stringify(fhPast).slice(0, 200)}`);
|
|
382
411
|
}
|
|
383
|
-
const fhOutcome = fhBlocked ? 'sentinel-限流降级' : fhErrTerminal ? '端点不可达降级' : `${fh.hotels?.length ?? 0} 家`;
|
|
412
|
+
const fhOutcome = fhNeedsSetup ? '试用达限(needs-setup)' : fhBlocked ? 'sentinel-限流降级' : fhErrTerminal ? '端点不可达降级' : `${fh.hotels?.length ?? 0} 家`;
|
|
384
413
|
console.log(` hotel channel: ${fhOutcome}; 参数闸/过去日闸生效`);
|
|
385
414
|
}
|
|
386
415
|
{
|
|
@@ -438,9 +467,7 @@ async function main() {
|
|
|
438
467
|
});
|
|
439
468
|
ledger.settleWorkflowRun('art-probe-1', '# 交付·账本权威\nD1 大理\nD2 洱海');
|
|
440
469
|
const listTool = byName('gotry_artifacts_list');
|
|
441
|
-
const ledList = await listTool.execute({
|
|
442
|
-
query: {}
|
|
443
|
-
}, null);
|
|
470
|
+
const ledList = await listTool.execute({}, null);
|
|
444
471
|
const run = ledList.artifacts?.find((a)=>a.id === 'art-probe-1');
|
|
445
472
|
if (!ledList.ok || !run || run.source !== 'async-run' || run.status !== 'settled') {
|
|
446
473
|
throw new Error(`FAIL: artifacts list 应发现账本已交付工单,实际:${JSON.stringify(ledList).slice(0, 200)}`);
|
|
@@ -454,17 +481,13 @@ async function main() {
|
|
|
454
481
|
}
|
|
455
482
|
const readTool = byName('gotry_artifacts_read');
|
|
456
483
|
const r1 = await readTool.execute({
|
|
457
|
-
|
|
458
|
-
path: 'art-probe-1'
|
|
459
|
-
}
|
|
484
|
+
path: 'art-probe-1'
|
|
460
485
|
}, null);
|
|
461
486
|
if (!r1.ok || r1.lines?.[0]?.number !== 1 || !r1.lines?.[0]?.text.includes('交付·账本权威') || r1.lang !== 'markdown') {
|
|
462
487
|
throw new Error(`FAIL: 裸工单 id 应从账本读出行号视图,实际:${JSON.stringify(r1).slice(0, 200)}`);
|
|
463
488
|
}
|
|
464
489
|
const view = readTool.presentResult?.({
|
|
465
|
-
|
|
466
|
-
path: 'art-probe-1'
|
|
467
|
-
}
|
|
490
|
+
path: 'art-probe-1'
|
|
468
491
|
}, r1);
|
|
469
492
|
if (view?.card !== 'read' || view.path !== r1.path || view.offset !== 1 || view.lines?.length !== r1.lines?.length || view.totalLines !== r1.totalLines || view.lang !== 'markdown') {
|
|
470
493
|
throw new Error(`FAIL: read 卡字段不齐,实际:${JSON.stringify(view).slice(0, 200)}`);
|
|
@@ -548,10 +571,8 @@ async function main() {
|
|
|
548
571
|
'- 国泰航空 CX771 08:05→10:35(已按 exact-date 检索记录)'
|
|
549
572
|
].join('\n');
|
|
550
573
|
const pass = await gate.execute({
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
tripYear: 2027
|
|
554
|
-
}
|
|
574
|
+
markdown: passMd,
|
|
575
|
+
tripYear: 2027
|
|
555
576
|
}, null);
|
|
556
577
|
if (pass.verdict !== 'pass' || pass.presentation !== 'verified_itinerary_allowed') {
|
|
557
578
|
throw new Error(`FAIL: 可回溯产物应 pass,实际:${JSON.stringify(pass).slice(0, 300)}`);
|
|
@@ -562,17 +583,13 @@ async function main() {
|
|
|
562
583
|
'- 香港快运 UO784 10:05→12:40 直飞 ✓'
|
|
563
584
|
].join('\n');
|
|
564
585
|
const blocked = await gate.execute({
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
tripYear: 2027
|
|
568
|
-
}
|
|
586
|
+
markdown: badMd,
|
|
587
|
+
tripYear: 2027
|
|
569
588
|
}, null);
|
|
570
589
|
if (blocked.verdict !== 'blocked' || !blocked.violations?.some((v)=>v.kind === 'not_in_source')) {
|
|
571
590
|
throw new Error(`FAIL: exact-date miss 被 UO784 填充必须 blocked(not_in_source),实际:${JSON.stringify(blocked).slice(0, 300)}`);
|
|
572
591
|
}
|
|
573
|
-
const view = gate.presentResult?.({
|
|
574
|
-
query: {}
|
|
575
|
-
}, blocked);
|
|
592
|
+
const view = gate.presentResult?.({}, blocked);
|
|
576
593
|
if (!view?.title?.includes('blocked')) throw new Error(`FAIL: blocked 呈现卡标题应含 blocked,实际:${view?.title}`);
|
|
577
594
|
console.log(`fact gate: registry 1+1(hit/miss);verified 措辞 pass;miss 填充 UO784 blocked(not_in_source) + 呈现卡`);
|
|
578
595
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { listTurnHandoffTickets, settleTurnHandoffTicket, writeTurnHandoffTicket } from '../src/turn-deadline.js';
|
|
7
|
+
const TS_DIR = join(import.meta.dirname, '..');
|
|
8
|
+
const TSX = join(TS_DIR, 'node_modules', 'tsx', 'dist', 'cli.mjs');
|
|
9
|
+
function runCollector(args, env = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const stdout = execFileSync(process.execPath, [
|
|
12
|
+
TSX,
|
|
13
|
+
'scripts/turn-handoff-collect.ts',
|
|
14
|
+
...args
|
|
15
|
+
], {
|
|
16
|
+
cwd: TS_DIR,
|
|
17
|
+
env: {
|
|
18
|
+
...process.env,
|
|
19
|
+
...env
|
|
20
|
+
},
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
stdio: [
|
|
23
|
+
'ignore',
|
|
24
|
+
'pipe',
|
|
25
|
+
'pipe'
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
status: 0,
|
|
30
|
+
stdout,
|
|
31
|
+
stderr: ''
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const e = error;
|
|
35
|
+
return {
|
|
36
|
+
status: e.status ?? null,
|
|
37
|
+
stdout: e.stdout ?? '',
|
|
38
|
+
stderr: e.stderr ?? ''
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const root = mkdtempSync(join(tmpdir(), 'gotry-handoff-collect-'));
|
|
43
|
+
const planners = mkdtempSync(join(tmpdir(), 'gotry-handoff-planners-'));
|
|
44
|
+
const plannerCalls = join(planners, 'calls.log');
|
|
45
|
+
const okPlanner = join(planners, 'ok.mjs');
|
|
46
|
+
writeFileSync(okPlanner, `import { appendFileSync } from 'node:fs'
|
|
47
|
+
appendFileSync(${JSON.stringify(plannerCalls)}, [
|
|
48
|
+
'call child=' + process.env.GOTRY_HANDOFF_CHILD,
|
|
49
|
+
'pin=' + (process.env.GOTRY_TURN_DEADLINE_SOFT_MS ?? 'CLEARED'),
|
|
50
|
+
'root=' + (process.env.GOTRY_TURN_HANDOFF_ROOT ?? 'CLEARED'),
|
|
51
|
+
'msg=' + process.argv[2]?.slice(0, 12),
|
|
52
|
+
].join(' | ') + '\\n')
|
|
53
|
+
console.log('# 规划结果\\n方案A:9.26 DXB→CAN,10.3 衡阳婚礼,10.5 返程')
|
|
54
|
+
`);
|
|
55
|
+
const failPlanner = join(planners, 'fail.mjs');
|
|
56
|
+
writeFileSync(failPlanner, `import { appendFileSync } from 'node:fs'
|
|
57
|
+
appendFileSync(${JSON.stringify(plannerCalls)}, 'call fail\\n')
|
|
58
|
+
process.exit(3)
|
|
59
|
+
`);
|
|
60
|
+
const emptyPlanner = join(planners, 'empty.mjs');
|
|
61
|
+
writeFileSync(emptyPlanner, `import { appendFileSync } from 'node:fs'
|
|
62
|
+
appendFileSync(${JSON.stringify(plannerCalls)}, 'call empty\\n')
|
|
63
|
+
`);
|
|
64
|
+
const calls = ()=>existsSync(plannerCalls) ? readFileSync(plannerCalls, 'utf-8').trim().split('\n') : [];
|
|
65
|
+
try {
|
|
66
|
+
assert.equal(runCollector([
|
|
67
|
+
'th-missing',
|
|
68
|
+
root
|
|
69
|
+
]).status, 1);
|
|
70
|
+
const t1 = await writeTurnHandoffTicket(root, '帮我规划10.3衡阳婚礼十几天行程');
|
|
71
|
+
{
|
|
72
|
+
const r = runCollector([
|
|
73
|
+
t1.id,
|
|
74
|
+
root
|
|
75
|
+
], {
|
|
76
|
+
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
77
|
+
});
|
|
78
|
+
assert.equal(r.status, 0, `stderr=${r.stderr}`);
|
|
79
|
+
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
80
|
+
assert.equal(terminal['schema'], 'gotry_turn_handoff_terminal.v1');
|
|
81
|
+
assert.equal(terminal['status'], 'succeeded');
|
|
82
|
+
assert.ok(String(terminal['deliverable_path']).includes(`${t1.id}.deliverable.md`));
|
|
83
|
+
}
|
|
84
|
+
const t1After = JSON.parse(readFileSync(join(root, 'gotry-state', 'turn-handoffs', `${t1.id}.json`), 'utf8'));
|
|
85
|
+
assert.equal(t1After['status'], 'settled');
|
|
86
|
+
assert.ok(t1After['settledAt']);
|
|
87
|
+
const deliverable1 = readFileSync(join(root, 'gotry-state', 'turn-handoffs', `${t1.id}.deliverable.md`), 'utf8');
|
|
88
|
+
assert.ok(deliverable1.includes('方案A') && deliverable1.includes(t1.id));
|
|
89
|
+
{
|
|
90
|
+
const before = calls().length;
|
|
91
|
+
const r = runCollector([
|
|
92
|
+
t1.id,
|
|
93
|
+
root
|
|
94
|
+
], {
|
|
95
|
+
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
96
|
+
});
|
|
97
|
+
assert.equal(r.status, 0);
|
|
98
|
+
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
99
|
+
assert.equal(terminal['status'], 'succeeded');
|
|
100
|
+
assert.equal(calls().length, before, 'settled ticket must not re-run the planner');
|
|
101
|
+
}
|
|
102
|
+
{
|
|
103
|
+
const last = [
|
|
104
|
+
...calls()
|
|
105
|
+
].reverse().find((l)=>l.includes('child='));
|
|
106
|
+
assert.ok(last, 'planner ran');
|
|
107
|
+
assert.ok(last.includes('child=1'), `GOTRY_HANDOFF_CHILD must be 1: ${last}`);
|
|
108
|
+
assert.ok(last.includes('pin=CLEARED'), `parent deadline pins must be cleared: ${last}`);
|
|
109
|
+
assert.ok(last.includes('root=CLEARED'), `parent handoff root must be cleared: ${last}`);
|
|
110
|
+
assert.ok(last.includes('msg=帮我规划10.3衡阳婚礼'), 'planner receives the verbatim user ask');
|
|
111
|
+
}
|
|
112
|
+
const t2 = await writeTurnHandoffTicket(root, '另一趟失败的规划');
|
|
113
|
+
{
|
|
114
|
+
const r = runCollector([
|
|
115
|
+
t2.id,
|
|
116
|
+
root
|
|
117
|
+
], {
|
|
118
|
+
GOTRY_HANDOFF_PLANNER_BIN: failPlanner
|
|
119
|
+
});
|
|
120
|
+
assert.equal(r.status, 2);
|
|
121
|
+
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
122
|
+
assert.equal(terminal['status'], 'failed');
|
|
123
|
+
assert.ok(String(terminal['error']).startsWith('planner exited 3'), `error shape: ${terminal['error']}`);
|
|
124
|
+
}
|
|
125
|
+
const t2After = JSON.parse(readFileSync(join(root, 'gotry-state', 'turn-handoffs', `${t2.id}.json`), 'utf8'));
|
|
126
|
+
assert.equal(t2After['status'], 'failed');
|
|
127
|
+
assert.ok(String(t2After['error']).startsWith('planner exited 3'));
|
|
128
|
+
const deliverable2 = readFileSync(join(root, 'gotry-state', 'turn-handoffs', `${t2.id}.deliverable.md`), 'utf8');
|
|
129
|
+
assert.ok(deliverable2.includes('没有完成') && deliverable2.includes('planner exited 3'), 'honest failure note');
|
|
130
|
+
{
|
|
131
|
+
const before = calls().length;
|
|
132
|
+
assert.equal(runCollector([
|
|
133
|
+
t2.id,
|
|
134
|
+
root
|
|
135
|
+
], {
|
|
136
|
+
GOTRY_HANDOFF_PLANNER_BIN: failPlanner
|
|
137
|
+
}).status, 2);
|
|
138
|
+
assert.equal(calls().length, before);
|
|
139
|
+
}
|
|
140
|
+
const t3 = await writeTurnHandoffTicket(root, '空输出的规划');
|
|
141
|
+
{
|
|
142
|
+
const r = runCollector([
|
|
143
|
+
t3.id,
|
|
144
|
+
root
|
|
145
|
+
], {
|
|
146
|
+
GOTRY_HANDOFF_PLANNER_BIN: emptyPlanner
|
|
147
|
+
});
|
|
148
|
+
assert.equal(r.status, 2);
|
|
149
|
+
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
150
|
+
assert.equal(terminal['error'], 'planner produced empty output');
|
|
151
|
+
}
|
|
152
|
+
const t4 = await writeTurnHandoffTicket(root, '批量规划一');
|
|
153
|
+
const t5 = await writeTurnHandoffTicket(root, '批量规划二');
|
|
154
|
+
{
|
|
155
|
+
const dir = join(root, 'gotry-state', 'turn-handoffs');
|
|
156
|
+
const t5json = JSON.parse(readFileSync(join(dir, `${t5.id}.json`), 'utf8'));
|
|
157
|
+
writeFileSync(join(dir, `${t5.id}.json`), JSON.stringify({
|
|
158
|
+
...t5json,
|
|
159
|
+
requestedAt: new Date(Date.now() + 5_000).toISOString()
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
{
|
|
163
|
+
const before = calls().length;
|
|
164
|
+
const r = runCollector([
|
|
165
|
+
'--all',
|
|
166
|
+
root
|
|
167
|
+
], {
|
|
168
|
+
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
169
|
+
});
|
|
170
|
+
assert.equal(r.status, 0, `stderr=${r.stderr}`);
|
|
171
|
+
const terminal = JSON.parse(r.stdout.trim().split('\n').at(-1));
|
|
172
|
+
assert.equal(terminal['status'], 'succeeded');
|
|
173
|
+
assert.equal(calls().length, before + 2, 'exactly the two open tickets collected');
|
|
174
|
+
}
|
|
175
|
+
assert.equal(runCollector([
|
|
176
|
+
'--all',
|
|
177
|
+
root
|
|
178
|
+
], {
|
|
179
|
+
GOTRY_HANDOFF_PLANNER_BIN: okPlanner
|
|
180
|
+
}).status, 0);
|
|
181
|
+
{
|
|
182
|
+
const views = await listTurnHandoffTickets(root);
|
|
183
|
+
assert.equal(views.length, 5);
|
|
184
|
+
assert.equal(views[0].id, t5.id, 'newest first');
|
|
185
|
+
const settled = views.find((v)=>v.id === t1.id);
|
|
186
|
+
assert.equal(settled.status, 'settled');
|
|
187
|
+
assert.ok(settled.deliverableExcerpt?.includes('方案A'));
|
|
188
|
+
const failed = views.find((v)=>v.id === t2.id);
|
|
189
|
+
assert.equal(failed.status, 'failed');
|
|
190
|
+
assert.ok(failed.error?.startsWith('planner exited 3'));
|
|
191
|
+
}
|
|
192
|
+
console.log('turn handoff collector tests: OK (settle/fail/empty/idempotent replay/--all/recursion guard env/revisit views)');
|
|
193
|
+
} finally{
|
|
194
|
+
rmSync(root, {
|
|
195
|
+
recursive: true,
|
|
196
|
+
force: true
|
|
197
|
+
});
|
|
198
|
+
rmSync(planners, {
|
|
199
|
+
recursive: true,
|
|
200
|
+
force: true
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
{
|
|
204
|
+
const r = mkdtempSync(join(tmpdir(), 'gotry-handoff-settle-'));
|
|
205
|
+
const t = await writeTurnHandoffTicket(r, '直调结算');
|
|
206
|
+
const { deliverablePath } = await settleTurnHandoffTicket(r, t, 'settled', '交付内容');
|
|
207
|
+
assert.ok(existsSync(deliverablePath));
|
|
208
|
+
const views = await listTurnHandoffTickets(r);
|
|
209
|
+
assert.equal(views[0].deliverableExcerpt, '交付内容');
|
|
210
|
+
rmSync(r, {
|
|
211
|
+
recursive: true,
|
|
212
|
+
force: true
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//# sourceURL=ts/scripts/turn-handoff-collect-tests.ts
|