@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.
Files changed (137) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +236 -3
  4. package/bin/gotry-inner.js +66 -20
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +87 -2
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. package/ts/src/tool-budget.ts +0 -165
@@ -45,10 +45,10 @@ async function main() {
45
45
  }
46
46
  }
47
47
  }, null);
48
- if (rejectedMotivation.ok !== false || !rejectedMotivation.summary?.includes('without evidence')) {
49
- throw new Error(`FAIL: profile without evidence should have been rejected, actual ${JSON.stringify(rejectedMotivation).slice(0, 120)}`);
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
- query: {
116
- action: 'reach',
117
- channel: 'v2ex',
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
- for (const [name, r] of [
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
- query: {
164
- destination: '大理',
165
- checkIn: '2026-9-4',
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
- query: {
174
- destination: '大理',
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
- query: {
294
- kind: 'flight',
295
- from: '上海',
296
- to: '丽江',
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
- if (faBlocked) {
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
- query: {
312
- kind: 'flight',
313
- from: '深圳',
314
- to: '普吉',
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
- query: {
331
- from: '上海',
332
- to: '丽江',
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
- query: {
353
- kind: 'hotel',
354
- to: '大理',
355
- checkIn: '2026-10-01',
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
- if (fhBlocked || fhErrTerminal) {
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: 'hotel'
360
+ kind: 'flight',
361
+ from: '上海',
362
+ to: '丽江',
363
+ date: '2026-10-01'
369
364
  }
370
365
  }, null);
371
- if (fhDest.ok !== false) throw new Error('FAIL: hotel 缺目的地应参数闸拒绝');
372
- const fhPast = await byName('gotry_flyai_search').execute({
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
- query: {
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
- query: {
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
- query: {
552
- markdown: passMd,
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
- query: {
566
- markdown: badMd,
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