@danceiny/gotry 0.0.1-rc.17 → 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 +185 -148
- package/README.zh-CN.md +195 -154
- package/bin/gotry-bootstrap.js +246 -26
- package/bin/gotry-inner.js +68 -38
- 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 +115 -44
- 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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { runDoctorChecks, renderDoctorReportMd, nodeOk } from '../capabilities/doctor.js';
|
|
6
|
+
import { apply } from '../src/index.js';
|
|
7
|
+
const tmp = await mkdtemp(join(tmpdir(), 'gotry-doctor-test-'));
|
|
8
|
+
const emptyRepo = join(tmp, 'repo-empty');
|
|
9
|
+
const emptyHome = join(tmp, 'home-empty');
|
|
10
|
+
await mkdir(emptyRepo, {
|
|
11
|
+
recursive: true
|
|
12
|
+
});
|
|
13
|
+
await mkdir(emptyHome, {
|
|
14
|
+
recursive: true
|
|
15
|
+
});
|
|
16
|
+
const r1 = await runDoctorChecks({
|
|
17
|
+
repoRoot: emptyRepo,
|
|
18
|
+
homeDir: emptyHome,
|
|
19
|
+
env: {}
|
|
20
|
+
});
|
|
21
|
+
const byId = (id)=>r1.items.find((i)=>i.id === id);
|
|
22
|
+
assert.ok(byId('agent-reach'), 'agent-reach 项存在');
|
|
23
|
+
assert.equal(byId('agent-reach').status, 'missing', `空仓 agent-reach=missing,实际 ${byId('agent-reach').status}`);
|
|
24
|
+
assert.match(byId('agent-reach').fix ?? '', /npx gotry doctor --fix/, 'missing 项带精确补装指引');
|
|
25
|
+
assert.equal(byId('extension').status, 'missing', '空 home 扩展=missing');
|
|
26
|
+
assert.equal(byId('flyai').status, 'degraded', '无 key flyai=degraded(有共享试用,非缺失)');
|
|
27
|
+
assert.equal(byId('llm-key').status, 'ok', 'LLM key 恒 ok(让渡面)');
|
|
28
|
+
assert.equal(r1.ok, false, '有 missing 项时报告 ok=false');
|
|
29
|
+
assert.match(r1.summary, /待处理/, 'summary 指明待处理项');
|
|
30
|
+
console.log('1. 空 tmp 环境 → missing/degraded 分级正确,LLM key 让渡 OK');
|
|
31
|
+
const repoPartial = join(tmp, 'repo-partial');
|
|
32
|
+
await mkdir(join(repoPartial, '.venv/bin'), {
|
|
33
|
+
recursive: true
|
|
34
|
+
});
|
|
35
|
+
await writeFile(join(repoPartial, '.venv/bin/python'), '#!/bin/sh\n', {
|
|
36
|
+
mode: 0o755
|
|
37
|
+
});
|
|
38
|
+
const r2a = await runDoctorChecks({
|
|
39
|
+
repoRoot: repoPartial,
|
|
40
|
+
homeDir: emptyHome,
|
|
41
|
+
env: {}
|
|
42
|
+
});
|
|
43
|
+
assert.equal(r2a.items.find((i)=>i.id === 'agent-reach').status, 'degraded', '只 python → degraded(半可用显式区分)');
|
|
44
|
+
const repoFull = join(tmp, 'repo-full');
|
|
45
|
+
await mkdir(join(repoFull, '.venv/bin'), {
|
|
46
|
+
recursive: true
|
|
47
|
+
});
|
|
48
|
+
await writeFile(join(repoFull, '.venv/bin/python'), '#!/bin/sh\n', {
|
|
49
|
+
mode: 0o755
|
|
50
|
+
});
|
|
51
|
+
await writeFile(join(repoFull, '.venv/bin/agent-reach'), '#!/bin/sh\n', {
|
|
52
|
+
mode: 0o755
|
|
53
|
+
});
|
|
54
|
+
const r2b = await runDoctorChecks({
|
|
55
|
+
repoRoot: repoFull,
|
|
56
|
+
homeDir: emptyHome,
|
|
57
|
+
env: {}
|
|
58
|
+
});
|
|
59
|
+
assert.equal(r2b.items.find((i)=>i.id === 'agent-reach').status, 'ok', 'python+agent-reach 齐 → ok');
|
|
60
|
+
console.log('2. .venv 三态(missing/degraded/ok)分级 OK');
|
|
61
|
+
const r3 = await runDoctorChecks({
|
|
62
|
+
repoRoot: emptyRepo,
|
|
63
|
+
homeDir: emptyHome,
|
|
64
|
+
env: {
|
|
65
|
+
FLYAI_API_KEY: 'test-key'
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
assert.equal(r3.items.find((i)=>i.id === 'flyai').status, 'ok', '有正式 key → flyai=ok');
|
|
69
|
+
console.log('3. FLYAI_API_KEY 注入 → flyai ok OK');
|
|
70
|
+
assert.equal(nodeOk('22.14.0'), false, '22.14 不足');
|
|
71
|
+
assert.equal(nodeOk('22.15.0'), true, '22.15 达标');
|
|
72
|
+
assert.equal(nodeOk('23.0.0'), true, '23.x 达标');
|
|
73
|
+
console.log('4. nodeOk 边界 OK');
|
|
74
|
+
const md = renderDoctorReportMd(r1);
|
|
75
|
+
assert.match(md, /# GoTry 依赖体检报告/, '报告标题');
|
|
76
|
+
assert.match(md, /`npx gotry doctor --fix`/, '命令类 fix 加反引号');
|
|
77
|
+
assert.match(md, /flyai\.open\.fliggy\.com 控制台申请正式 key/, 'prose 类 fix 原样呈现');
|
|
78
|
+
assert.ok(!md.includes('`到 flyai'), 'prose 类 fix 不应整体包反引号');
|
|
79
|
+
assert.match(md, /LLM key/, '让渡面(LLM key)照常入表');
|
|
80
|
+
console.log('5. 报告渲染 OK');
|
|
81
|
+
const smokeRoot = await mkdtemp(join(tmpdir(), 'gotry-doctor-state-'));
|
|
82
|
+
const registered = [];
|
|
83
|
+
const ctx = {
|
|
84
|
+
tools: {
|
|
85
|
+
register: (t)=>registered.push(t)
|
|
86
|
+
},
|
|
87
|
+
systemPrompt: {
|
|
88
|
+
variable: ()=>{}
|
|
89
|
+
},
|
|
90
|
+
on: ()=>()=>{}
|
|
91
|
+
};
|
|
92
|
+
apply(ctx, {
|
|
93
|
+
stateRoot: smokeRoot,
|
|
94
|
+
timeoutMs: 30_000,
|
|
95
|
+
hbcliBin: 'hbcli-not-on-path',
|
|
96
|
+
sessionAccess: 'ask'
|
|
97
|
+
});
|
|
98
|
+
const doctorTool = registered.find((t)=>t.name === 'gotry_doctor');
|
|
99
|
+
assert.ok(doctorTool, 'gotry_doctor 已注册');
|
|
100
|
+
const out = await doctorTool.execute({
|
|
101
|
+
query: {}
|
|
102
|
+
}, null);
|
|
103
|
+
assert.equal(out.ok, true, '体检工具 ok=true(体检本身成功,与依赖是否缺失无关)');
|
|
104
|
+
assert.equal(out.verdict, 'needs-attention', '注入环境外仍能出 verdict(本机真实状态,不判 all-clear)');
|
|
105
|
+
assert.ok(Array.isArray(out.items) && out.items.length >= 6, 'items 齐全');
|
|
106
|
+
assert.ok(out.report_path && out.report_path.includes(smokeRoot), `报告落在 isolated stateRoot: ${out.report_path}`);
|
|
107
|
+
const reportMd = await readFile(out.report_path, 'utf-8');
|
|
108
|
+
assert.match(reportMd, /# GoTry 依赖体检报告/, '落盘报告可预览');
|
|
109
|
+
await rm(tmp, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
force: true
|
|
112
|
+
});
|
|
113
|
+
await rm(smokeRoot, {
|
|
114
|
+
recursive: true,
|
|
115
|
+
force: true
|
|
116
|
+
});
|
|
117
|
+
console.log('6. gotry_doctor 工具面 + 报告落盘 OK');
|
|
118
|
+
console.log('doctor-tests: 全部通过');
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
//# sourceURL=ts/scripts/doctor-tests.ts
|
|
@@ -468,7 +468,280 @@ assert.equal(m10a.result.rates?.rooms?.length, 1, 'RATES 夹具回放');
|
|
|
468
468
|
assert.equal(m10b.result.avail?.avail, true, 'CHECK_AVAIL 夹具回放');
|
|
469
469
|
assert.match(m10a.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
|
|
470
470
|
console.log('10c. mock 解译器夹具回放(预订链 CI 形态)OK');
|
|
471
|
-
|
|
471
|
+
__resetEffectBreakersForTest();
|
|
472
|
+
let flakyCalls = 0;
|
|
473
|
+
const ipTimeout = makeProductionInterpreter({
|
|
474
|
+
sleep: sleep0,
|
|
475
|
+
breakers: new Map(),
|
|
476
|
+
handlers: {
|
|
477
|
+
HBCLI_HOTEL_SEARCH: async ()=>{
|
|
478
|
+
flakyCalls += 1;
|
|
479
|
+
if (flakyCalls === 1) return {
|
|
480
|
+
via: 'hbcli-error',
|
|
481
|
+
exitCode: -1,
|
|
482
|
+
result: null,
|
|
483
|
+
evidence: '[实时API:hbcli@timeout@ts]',
|
|
484
|
+
latencyMs: 1,
|
|
485
|
+
error: 'timeout after 30000ms'
|
|
486
|
+
};
|
|
487
|
+
return {
|
|
488
|
+
via: 'hbcli-realtime',
|
|
489
|
+
exitCode: 0,
|
|
490
|
+
result: {
|
|
491
|
+
hotels: []
|
|
492
|
+
},
|
|
493
|
+
evidence: '[实时API:hbcli@ts]',
|
|
494
|
+
latencyMs: 1
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
const itp11 = await ipTimeout({
|
|
500
|
+
effect: 'HBCLI_HOTEL_SEARCH',
|
|
501
|
+
params: {
|
|
502
|
+
destination: '迪拜'
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
assert.equal(itp11.trace.attempts, 2, `timeout 重试一次(attempts=2),实际 ${itp11.trace.attempts}`);
|
|
506
|
+
assert.equal(itp11.result?.via, 'hbcli-realtime', '重试后恢复实时');
|
|
507
|
+
assert.equal(itp11.trace.breaker, 'closed', '恢复即成功,断路不计数');
|
|
508
|
+
let hardCalls = 0;
|
|
509
|
+
const ipEnoent = makeProductionInterpreter({
|
|
510
|
+
sleep: sleep0,
|
|
511
|
+
breakers: new Map(),
|
|
512
|
+
handlers: {
|
|
513
|
+
HBCLI_HOTEL_SEARCH: async ()=>{
|
|
514
|
+
hardCalls += 1;
|
|
515
|
+
return {
|
|
516
|
+
via: 'hbcli-error',
|
|
517
|
+
exitCode: -1,
|
|
518
|
+
result: null,
|
|
519
|
+
evidence: '[实时API:hbcli@spawn_error@ts]',
|
|
520
|
+
latencyMs: 1,
|
|
521
|
+
error: 'spawn hbcli ENOENT'
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
const itp11b = await ipEnoent({
|
|
527
|
+
effect: 'HBCLI_HOTEL_SEARCH',
|
|
528
|
+
params: {
|
|
529
|
+
destination: '迪拜'
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
assert.equal(itp11b.trace.attempts, 1, 'ENOENT 类永不重试(切换不是重试契约不变)');
|
|
533
|
+
console.log('11. HBCLI×timeout 冷启动重试(timeout→attempts=2 恢复实时;ENOENT→attempts=1)OK');
|
|
534
|
+
__resetEffectBreakersForTest();
|
|
535
|
+
const sixChannels = [
|
|
536
|
+
[
|
|
537
|
+
'ANYTHING_SEARCH',
|
|
538
|
+
'cli',
|
|
539
|
+
{
|
|
540
|
+
ok: true,
|
|
541
|
+
via: 'hbcli-anything',
|
|
542
|
+
verdict: 'hit',
|
|
543
|
+
evidence: 'e',
|
|
544
|
+
latencyMs: 1
|
|
545
|
+
}
|
|
546
|
+
],
|
|
547
|
+
[
|
|
548
|
+
'WEB_READ',
|
|
549
|
+
'api',
|
|
550
|
+
{
|
|
551
|
+
ok: true,
|
|
552
|
+
via: 'r.jina.ai',
|
|
553
|
+
evidence: 'e',
|
|
554
|
+
latencyMs: 1
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
[
|
|
558
|
+
'GITHUB_SEARCH',
|
|
559
|
+
'cli',
|
|
560
|
+
{
|
|
561
|
+
ok: true,
|
|
562
|
+
verdict: 'found',
|
|
563
|
+
evidence: 'e',
|
|
564
|
+
latencyMs: 1
|
|
565
|
+
}
|
|
566
|
+
],
|
|
567
|
+
[
|
|
568
|
+
'VIDEO_SUBTITLE',
|
|
569
|
+
'cli',
|
|
570
|
+
{
|
|
571
|
+
ok: true,
|
|
572
|
+
verdict: 'found',
|
|
573
|
+
evidence: 'e',
|
|
574
|
+
latencyMs: 1
|
|
575
|
+
}
|
|
576
|
+
],
|
|
577
|
+
[
|
|
578
|
+
'AGENT_REACH',
|
|
579
|
+
'cli',
|
|
580
|
+
{
|
|
581
|
+
ok: true,
|
|
582
|
+
verdict: 'found',
|
|
583
|
+
channel: 'v2ex',
|
|
584
|
+
method: 'read',
|
|
585
|
+
evidence: 'e',
|
|
586
|
+
latencyMs: 1
|
|
587
|
+
}
|
|
588
|
+
],
|
|
589
|
+
[
|
|
590
|
+
'SESSION_LOGIN',
|
|
591
|
+
'browser',
|
|
592
|
+
{
|
|
593
|
+
ok: true,
|
|
594
|
+
via: 'session-login',
|
|
595
|
+
verdict: 'pending',
|
|
596
|
+
evidence: 'e',
|
|
597
|
+
latencyMs: 1
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
];
|
|
601
|
+
for (const [fx, channel, obs] of sixChannels){
|
|
602
|
+
const ip = makeProductionInterpreter({
|
|
603
|
+
sleep: sleep0,
|
|
604
|
+
breakers: new Map(),
|
|
605
|
+
handlers: {
|
|
606
|
+
[fx]: async ()=>obs
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
const out = await ip({
|
|
610
|
+
effect: fx,
|
|
611
|
+
params: {}
|
|
612
|
+
});
|
|
613
|
+
assert.equal(out.result, obs, `${fx} 透传原样`);
|
|
614
|
+
assert.equal(out.trace.channel, channel, `${fx} trace.channel=${channel}(策略表行在案)`);
|
|
615
|
+
assert.equal(out.trace.attempts, 1, `${fx} 成功单次执行`);
|
|
616
|
+
}
|
|
617
|
+
console.log('12a. 六渠道入表(透传 + trace.channel 与策略表一致)OK');
|
|
618
|
+
for (const fx of [
|
|
619
|
+
'AGENT_REACH',
|
|
620
|
+
'SESSION_LOGIN'
|
|
621
|
+
]){
|
|
622
|
+
const ip = makeProductionInterpreter({
|
|
623
|
+
sleep: sleep0,
|
|
624
|
+
breakers: new Map(),
|
|
625
|
+
handlers: {
|
|
626
|
+
[fx]: async ()=>{
|
|
627
|
+
throw new Error('upstream says no');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
const out = await ip({
|
|
632
|
+
effect: fx,
|
|
633
|
+
params: {}
|
|
634
|
+
});
|
|
635
|
+
assert.equal(out.result, null, `${fx} 抛错 → result=null`);
|
|
636
|
+
assert.equal(out.trace.attempts, 1, `${fx} 永不重试(attempts=1)`);
|
|
637
|
+
}
|
|
638
|
+
console.log('12b. AGENT_REACH/SESSION_LOGIN 永不重试(反射桥透传/浏览器风控红线)OK');
|
|
639
|
+
__resetEffectBreakersForTest();
|
|
640
|
+
let webCalls = 0;
|
|
641
|
+
const ipWeb = makeProductionInterpreter({
|
|
642
|
+
sleep: sleep0,
|
|
643
|
+
breakers: new Map(),
|
|
644
|
+
handlers: {
|
|
645
|
+
WEB_READ: async ()=>{
|
|
646
|
+
webCalls += 1;
|
|
647
|
+
if (webCalls === 1) throw new Error('socket hang up');
|
|
648
|
+
return {
|
|
649
|
+
ok: true,
|
|
650
|
+
via: 'r.jina.ai',
|
|
651
|
+
evidence: 'e',
|
|
652
|
+
latencyMs: 1
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
const outWeb = await ipWeb({
|
|
658
|
+
effect: 'WEB_READ',
|
|
659
|
+
params: {
|
|
660
|
+
url: 'https://example.com'
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
assert.equal(outWeb.trace.attempts, 2, `WEB_READ 瞬时抖动重试 1 次,实际 ${outWeb.trace.attempts}`);
|
|
664
|
+
let ghTimeoutCalls = 0;
|
|
665
|
+
const ipGhTimeout = makeProductionInterpreter({
|
|
666
|
+
sleep: sleep0,
|
|
667
|
+
breakers: new Map(),
|
|
668
|
+
handlers: {
|
|
669
|
+
GITHUB_SEARCH: async ()=>{
|
|
670
|
+
ghTimeoutCalls += 1;
|
|
671
|
+
if (ghTimeoutCalls === 1) return {
|
|
672
|
+
ok: false,
|
|
673
|
+
verdict: 'timeout',
|
|
674
|
+
evidence: 'e',
|
|
675
|
+
latencyMs: 1,
|
|
676
|
+
stdout: '',
|
|
677
|
+
stderr: ''
|
|
678
|
+
};
|
|
679
|
+
return {
|
|
680
|
+
ok: true,
|
|
681
|
+
verdict: 'found',
|
|
682
|
+
evidence: 'e',
|
|
683
|
+
latencyMs: 1
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
const outGhT = await ipGhTimeout({
|
|
689
|
+
effect: 'GITHUB_SEARCH',
|
|
690
|
+
params: {
|
|
691
|
+
query: 'x'
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
assert.equal(outGhT.trace.attempts, 2, 'GITHUB timeout 类重试 1 次(冷启动同 HBCLI 实况)');
|
|
695
|
+
let ghNiCalls = 0;
|
|
696
|
+
const ipGhNi = makeProductionInterpreter({
|
|
697
|
+
sleep: sleep0,
|
|
698
|
+
breakers: new Map(),
|
|
699
|
+
handlers: {
|
|
700
|
+
GITHUB_SEARCH: async ()=>{
|
|
701
|
+
ghNiCalls += 1;
|
|
702
|
+
return {
|
|
703
|
+
ok: false,
|
|
704
|
+
verdict: 'not-installed',
|
|
705
|
+
evidence: 'e',
|
|
706
|
+
latencyMs: 0
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
const outGhNi = await ipGhNi({
|
|
712
|
+
effect: 'GITHUB_SEARCH',
|
|
713
|
+
params: {
|
|
714
|
+
query: 'x'
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
assert.equal(outGhNi.trace.attempts, 1, 'not-installed 是配置态(needs-setup 族)不重试');
|
|
718
|
+
console.log('12c. WEB_READ/GITHUB/VIDEO 策略行(timeout 重试/配置态不重试)OK');
|
|
719
|
+
__resetEffectBreakersForTest();
|
|
720
|
+
const mockIp = makeMockInterpreter({
|
|
721
|
+
ANYTHING_SEARCH: {
|
|
722
|
+
ok: true,
|
|
723
|
+
via: 'hbcli-anything',
|
|
724
|
+
verdict: 'hit',
|
|
725
|
+
evidence: '[实时API:hbcli-anything@ts]',
|
|
726
|
+
latencyMs: 1
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
const outMock = await mockIp({
|
|
730
|
+
effect: 'ANYTHING_SEARCH',
|
|
731
|
+
params: {
|
|
732
|
+
keyword: '大理'
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
assert.equal(outMock.result.verdict, 'hit', 'mock 夹具回放原样');
|
|
736
|
+
assert.equal(outMock.trace.channel, 'mock', 'mock 通道标注');
|
|
737
|
+
const outMockBad = await mockIp({
|
|
738
|
+
effect: 'SESSION_LOGIN',
|
|
739
|
+
params: {}
|
|
740
|
+
});
|
|
741
|
+
assert.equal(outMockBad.result, null, '未登记夹具 → result=null');
|
|
742
|
+
assert.equal(outMockBad.trace.declined, 'unknown-effect', '未登记夹具 → 结构化拒绝');
|
|
743
|
+
console.log('12d. mock 夹具回放/未登记拒绝(新效应同语义)OK');
|
|
744
|
+
console.log('EFFECT INTERPRETER TESTS: 12/12 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应/HBCLI timeout 重试/D-23 六渠道入表,纯离线)');
|
|
472
745
|
|
|
473
746
|
|
|
474
747
|
//# sourceURL=ts/scripts/effect-tests.ts
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { fork } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { BRIDGE_PORTS, EXTENSION_ID, EXTENSION_ID_STORE, EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE, EXTENSION_ORIGINS, EXTENSION_STORE_URL, createSessionBridge, __resetSessionBridgeForTest, __setSessionBridgeForTest } from '../capabilities/session/extension-bridge.js';
|
|
8
|
+
import { BRIDGE_PORTS, EXTENSION_ID, EXTENSION_ID_STORE, EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE, EXTENSION_ORIGINS, EXTENSION_STORE_URL, createSessionBridge, localExtensionInstalled, needsExtensionSummary, __resetSessionBridgeForTest, __setSessionBridgeForTest } from '../capabilities/session/extension-bridge.js';
|
|
9
9
|
import { classifyBridgeFailure, extensionCookieNames, extensionSearchJob } from '../capabilities/session/extension-channel.js';
|
|
10
|
-
import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch } from '../capabilities/session-search.js';
|
|
10
|
+
import { appendExtensionAudit, resolveTransportMode, sessionFlightSearch, sessionHotelSearch, sessionTrainSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
|
|
11
11
|
import { sessionLogin } from '../capabilities/session-login.js';
|
|
12
12
|
import { LOGIN_COOKIE_NAMES, NETWORK_HINTS, SITE_DOMAIN } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
13
|
+
import { HOTEL_NETWORK_HINTS, HOTEL_SITE_HOST, buildHotelEntryUrl } from '../capabilities/session/adapters/ctrip-hotel.js';
|
|
14
|
+
import { TRAIN_NETWORK_HINTS, TRAIN_SITE_HOST } from '../capabilities/session/adapters/rail-12306.js';
|
|
13
15
|
import { evaluateDoubleSource } from '../capabilities/session/benchmark.js';
|
|
14
16
|
const EXT_DIR = fileURLToPath(new URL('../../extension/', import.meta.url));
|
|
15
17
|
const UNREF_CHILD = fileURLToPath(new URL('./fixtures/extension-bridge-unref-child.mjs', import.meta.url));
|
|
@@ -161,11 +163,14 @@ async function main() {
|
|
|
161
163
|
].sort((a, b)=>a - b));
|
|
162
164
|
assert.ok(manifest.host_permissions.includes('https://*.ctrip.com/*'));
|
|
163
165
|
});
|
|
164
|
-
await check('防漂移:content_scripts 双 world
|
|
166
|
+
await check('防漂移:content_scripts 双 world 挂 ctrip 双站+12306(MAIN 嗅探 + ISOLATED 桥;2026-09-03 酒/火实装)', ()=>{
|
|
165
167
|
assert.equal(manifest.content_scripts.length, 2);
|
|
166
168
|
for (const cs of manifest.content_scripts){
|
|
167
169
|
assert.deepEqual(cs.matches, [
|
|
168
|
-
'https://flights.ctrip.com/*'
|
|
170
|
+
'https://flights.ctrip.com/*',
|
|
171
|
+
`https://${HOTEL_SITE_HOST}/*`,
|
|
172
|
+
`https://${TRAIN_SITE_HOST}/*`,
|
|
173
|
+
'https://www.12306.cn/*'
|
|
169
174
|
]);
|
|
170
175
|
assert.equal(cs.run_at, 'document_start');
|
|
171
176
|
}
|
|
@@ -182,19 +187,42 @@ async function main() {
|
|
|
182
187
|
const backgroundJs = read('background.js');
|
|
183
188
|
const contentMainJs = read('content-main.js');
|
|
184
189
|
const contentBridgeJs = read('content-bridge.js');
|
|
190
|
+
await check('版本跟随主版本:manifest version_name = package.json version;version = 四段投影(0.0.1-rc.N → 0.0.1.N,founder 2026-09-03 拍板)', ()=>{
|
|
191
|
+
const pkgVersion = JSON.parse(readFileSync(join(EXT_DIR, '..', 'package.json'), 'utf8')).version;
|
|
192
|
+
assert.equal(manifest.version_name, pkgVersion, 'version_name 应与 gotry 主版本逐字一致');
|
|
193
|
+
const m = /^(\d+)\.(\d+)\.(\d+)-rc\.(\d+)$/.exec(pkgVersion);
|
|
194
|
+
assert.ok(m, `主版本形态应为 x.y.z-rc.N,实际 ${pkgVersion}`);
|
|
195
|
+
assert.equal(manifest.version, `${m[1]}.${m[2]}.${m[3]}.${m[4]}`, 'version 应为主版本的四段投影(Chrome manifest 只收点分整数)');
|
|
196
|
+
assert.equal(manifest.content_scripts.length, 2);
|
|
197
|
+
});
|
|
185
198
|
await check('防漂移:票据 cookie 名单(Node LOGIN_COOKIE_NAMES)= 扩展 SITES.ticketNames', ()=>{
|
|
186
199
|
for (const name of LOGIN_COOKIE_NAMES){
|
|
187
200
|
assert.ok(backgroundJs.includes(`'${name}'`), `background.js 缺票据名 ${name}`);
|
|
188
201
|
}
|
|
189
202
|
assert.ok(backgroundJs.includes("domain: 'ctrip.com'"), 'background.js 站点域应与 LOGIN_TARGETS.domain 对账');
|
|
190
203
|
});
|
|
191
|
-
await check('防漂移:NETWORK_HINTS(Node)= content-main
|
|
204
|
+
await check('防漂移:NETWORK_HINTS + HOTEL_NETWORK_HINTS(Node)= content-main 嗅探面(MAIN-world)', ()=>{
|
|
192
205
|
for (const hint of NETWORK_HINTS){
|
|
193
206
|
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺 hint ${hint.source}`);
|
|
194
207
|
}
|
|
208
|
+
for (const hint of HOTEL_NETWORK_HINTS){
|
|
209
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺酒店 hint ${hint.source}`);
|
|
210
|
+
}
|
|
211
|
+
for (const hint of TRAIN_NETWORK_HINTS){
|
|
212
|
+
assert.ok(stripRe(contentMainJs).includes(stripRe(hint.source)), `content-main.js 缺火车 hint ${hint.source}`);
|
|
213
|
+
}
|
|
214
|
+
assert.ok(contentMainJs.includes('12306'), 'content-main.js 应感知 12306 页域(火车嗅探作用域)');
|
|
215
|
+
assert.ok(contentMainJs.includes('hotels\\.ctrip\\.com'), 'content-main.js 应感知酒店页域(形状嗅探兜底的作用域)');
|
|
216
|
+
});
|
|
217
|
+
await check('防漂移:酒店形状签名(Node looksLikeHotelListBody 同款)= content-main HOTEL_BODY_SIG_RE', ()=>{
|
|
218
|
+
assert.ok(stripRe(contentMainJs).includes('"hotelList"|"hotelMatchInfos"|"hotelName"'), '形状嗅探签名两侧必须逐字一致');
|
|
195
219
|
});
|
|
196
|
-
await check('防漂移:检索/登录 URL 面(background
|
|
197
|
-
assert.ok(backgroundJs.includes('https://flights.ctrip.com/'));
|
|
220
|
+
await check('防漂移:检索/登录 URL 面(background per-site 白名单 flights+hotels;ISOLATED 桥转发两类事件)', ()=>{
|
|
221
|
+
assert.ok(backgroundJs.includes('https://flights.ctrip.com/'), 'background 应含机票检索白名单');
|
|
222
|
+
assert.ok(backgroundJs.includes(`https://${HOTEL_SITE_HOST}/`), 'background 应含酒店检索白名单(2026-09-03 实装)');
|
|
223
|
+
assert.ok(backgroundJs.includes('https://kyfw.12306.cn/'), 'background 应含火车检索白名单(2026-09-03 实装)');
|
|
224
|
+
assert.ok(backgroundJs.includes("'ctrip-hotel'"), 'background SITES 应注册 ctrip-hotel');
|
|
225
|
+
assert.ok(backgroundJs.includes("'train-12306'"), 'background SITES 应注册 train-12306');
|
|
198
226
|
assert.ok(contentMainJs.includes('gotry-ctrip-sniff'));
|
|
199
227
|
assert.ok(contentBridgeJs.includes('gotry-ctrip-sniff'));
|
|
200
228
|
assert.ok(contentBridgeJs.includes('gotry-page'));
|
|
@@ -453,16 +481,43 @@ async function main() {
|
|
|
453
481
|
]
|
|
454
482
|
};
|
|
455
483
|
if (job.kind === 'search') {
|
|
456
|
-
|
|
484
|
+
const u = String(job.url);
|
|
485
|
+
const isHotel = u.startsWith(`https://${HOTEL_SITE_HOST}/`);
|
|
486
|
+
const isTrain = u.startsWith('https://kyfw.12306.cn/');
|
|
487
|
+
assert.ok(u.startsWith('https://flights.ctrip.com/') || isHotel || isTrain, `search job 只允许已注册站点域,实际 ${u}`);
|
|
457
488
|
return {
|
|
458
489
|
ok: true,
|
|
459
490
|
kind: 'search',
|
|
460
|
-
body: JSON.stringify({
|
|
491
|
+
body: isHotel ? JSON.stringify({
|
|
492
|
+
data: {
|
|
493
|
+
hotelList: [
|
|
494
|
+
{
|
|
495
|
+
hotelId: 442516,
|
|
496
|
+
hotelName: 'Hotel X',
|
|
497
|
+
star: 5,
|
|
498
|
+
commentScore: 4.7,
|
|
499
|
+
priceInfo: {
|
|
500
|
+
avgPrice: 680
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
}
|
|
505
|
+
}) : isTrain ? JSON.stringify({
|
|
506
|
+
data: {
|
|
507
|
+
result: [
|
|
508
|
+
'|预订|24000000G1375|G1375|SHH|KMM|SHH|KMM|07:35|15:27|07:52|Y|yp|x|x|loc|01|02|Y|0|--|--|--|--|--|--|有|--|--|有|有|有|有|--|ex|st|'
|
|
509
|
+
],
|
|
510
|
+
map: {
|
|
511
|
+
SHH: '上海南',
|
|
512
|
+
KMM: '昆明'
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}) : JSON.stringify({
|
|
461
516
|
data: {
|
|
462
517
|
flightItineraryList: []
|
|
463
518
|
}
|
|
464
519
|
}),
|
|
465
|
-
title: '机票列表'
|
|
520
|
+
title: isHotel ? '酒店列表' : isTrain ? '12306 车票预订' : '机票列表'
|
|
466
521
|
};
|
|
467
522
|
}
|
|
468
523
|
return {
|
|
@@ -491,6 +546,40 @@ async function main() {
|
|
|
491
546
|
assert.ok(search.ok);
|
|
492
547
|
assert.equal(search.ok ? search.timedOut : true, false);
|
|
493
548
|
assert.ok(search.ok ? search.body.includes('flightItineraryList') : false);
|
|
549
|
+
const hotelLane = await extensionSearchJob({
|
|
550
|
+
site: 'ctrip-hotel',
|
|
551
|
+
url: `https://${HOTEL_SITE_HOST}/hotels/list?city=220&checkin=2026-12-01&checkout=2026-12-03`,
|
|
552
|
+
timeoutMs: 3_000
|
|
553
|
+
});
|
|
554
|
+
assert.ok(hotelLane.ok, '酒店 search job 应放行(per-site 白名单)');
|
|
555
|
+
assert.ok(hotelLane.ok ? hotelLane.body.includes('hotelList') : false);
|
|
556
|
+
__resetRateLimiterForTest();
|
|
557
|
+
const hotelSession = await sessionHotelSearch({
|
|
558
|
+
to: '迪拜',
|
|
559
|
+
cityId: 220,
|
|
560
|
+
checkIn: '2026-12-01',
|
|
561
|
+
checkOut: '2026-12-03',
|
|
562
|
+
timeoutMs: 3_000
|
|
563
|
+
});
|
|
564
|
+
assert.equal(hotelSession.verdict, 'hit', `酒店会话检索应 hit,实际 ${hotelSession.verdict}:${hotelSession.error ?? ''}`);
|
|
565
|
+
assert.ok(hotelSession.evidence.includes('[会话:ctrip-hotel@'), '酒店证据链 [会话:ctrip-hotel@ts]');
|
|
566
|
+
assert.equal(hotelSession.hotels?.[0]?.name, 'Hotel X');
|
|
567
|
+
assert.equal(hotelSession.hotels?.[0]?.price, 680);
|
|
568
|
+
assert.ok(hotelSession.hotels?.[0]?.jumpUrl?.includes('/hotel/442516'), 'jumpUrl 由 hotelId 构造(预订由人在落地页完成)');
|
|
569
|
+
__resetRateLimiterForTest();
|
|
570
|
+
const trainSession = await sessionTrainSearch({
|
|
571
|
+
from: '上海',
|
|
572
|
+
to: '昆明',
|
|
573
|
+
date: '2026-12-01',
|
|
574
|
+
timeoutMs: 3_000
|
|
575
|
+
});
|
|
576
|
+
assert.equal(trainSession.verdict, 'hit', `火车会话检索应 hit,实际 ${trainSession.verdict}:${trainSession.error ?? ''}`);
|
|
577
|
+
assert.ok(trainSession.evidence.includes('[会话:train-12306@'), '火车证据链 [会话:train-12306@ts]');
|
|
578
|
+
assert.ok(trainSession.evidence.includes('公开查询面'), '证据链标注公开查询面(无登录闸的诚实口径)');
|
|
579
|
+
assert.equal(trainSession.trains?.[0]?.trainCode, 'G1375');
|
|
580
|
+
assert.equal(trainSession.trains?.[0]?.depTime, '07:35');
|
|
581
|
+
assert.equal(trainSession.trains?.[0]?.durationMin, 472);
|
|
582
|
+
assert.ok(trainSession.trains?.[0]?.jumpUrl?.includes('kyfw.12306.cn'), 'jumpUrl=查询落地页(人选车完成预订)');
|
|
494
583
|
const login = await sessionLogin({
|
|
495
584
|
site: 'ctrip-flight'
|
|
496
585
|
});
|
|
@@ -558,6 +647,35 @@ async function main() {
|
|
|
558
647
|
assert.equal(e.quota_disposition, 'no_spend_waiting_user');
|
|
559
648
|
assert.equal(e.retry_allowed, false);
|
|
560
649
|
});
|
|
650
|
+
await check('needs-extension 自适应文案:本地已落位 → 双通道完整指引(含开发者模式)', ()=>{
|
|
651
|
+
const s = needsExtensionSummary({
|
|
652
|
+
localInstalled: true
|
|
653
|
+
});
|
|
654
|
+
assert.match(s, /开发者模式「加载已解压的扩展程序」/, '本地通道在 → 保留开发者模式指引');
|
|
655
|
+
assert.ok(s.includes(EXTENSION_STORE_URL), '商店 URL 仍在');
|
|
656
|
+
});
|
|
657
|
+
await check('needs-extension 自适应文案:本地未落位(商店版用户/未装)→ 自动跳过 dev-mode 文案', ()=>{
|
|
658
|
+
const s = needsExtensionSummary({
|
|
659
|
+
localInstalled: false
|
|
660
|
+
});
|
|
661
|
+
assert.doesNotMatch(s, /开发者模式/, '不再推开发者模式/本地通道(商店版用户不该被指导 load unpacked)');
|
|
662
|
+
assert.doesNotMatch(s, /加载已解压/, '不再出现 load unpacked 字样');
|
|
663
|
+
assert.match(s, /已从 Chrome 商店安装/, '给「已装商店版?」提示(打开 Chrome 即可,无需再装)');
|
|
664
|
+
assert.match(s, /应用商店一键装/, '未装用户仍得商店一键装指引');
|
|
665
|
+
});
|
|
666
|
+
await check('localExtensionInstalled:HOME 注入可测(manifest 存在=本地通道在)', ()=>{
|
|
667
|
+
const fakeHome = mkdtempSync(join(tmpdir(), 'gotry-ext-home-'));
|
|
668
|
+
assert.equal(localExtensionInstalled(fakeHome), false, '空 HOME → 本地通道未落位');
|
|
669
|
+
mkdirSync(join(fakeHome, '.gotry', 'extension'), {
|
|
670
|
+
recursive: true
|
|
671
|
+
});
|
|
672
|
+
writeFileSync(join(fakeHome, '.gotry', 'extension', 'manifest.json'), '{}', 'utf-8');
|
|
673
|
+
assert.equal(localExtensionInstalled(fakeHome), true, 'manifest 落位 → true');
|
|
674
|
+
rmSync(fakeHome, {
|
|
675
|
+
recursive: true,
|
|
676
|
+
force: true
|
|
677
|
+
});
|
|
678
|
+
});
|
|
561
679
|
await b.close();
|
|
562
680
|
console.log(`\n§38 extension bridge: ${passed} 段全绿`);
|
|
563
681
|
}
|