@zhushanwen/pi-provider-live-sync 0.2.0 → 0.3.0
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 +25 -4
- package/package.json +5 -3
- package/skills/provider-live-sync-ext-config/SKILL.md +53 -15
- package/src/__tests__/catalog-bridge.test.ts +891 -0
- package/src/__tests__/model-sync.test.ts +6 -32
- package/src/__tests__/pi-runtime-testkit.ts +57 -0
- package/src/__tests__/pi-semantics-catalog-injection.test.ts +632 -0
- package/src/__tests__/sdk-contract.test.ts +290 -0
- package/src/__tests__/tick-harness.ts +48 -0
- package/src/catalog-bridge.ts +482 -0
- package/src/index.ts +101 -34
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi 语义 guard 探针 —— 登记表 docs/pi-semantics.json 的 PS-82 ~ PS-85 + PS-87 + PS-88 断言。
|
|
3
|
+
*
|
|
4
|
+
* PS-82 ~ PS-85 四条是 provider-catalog-sync 设计(目录投喂)最重的 pi 私有语义依赖面,
|
|
5
|
+
* 方案成立全部系于它们(设计 §2.5 前提 1-4);PS-87 是 R7 规则的承重语义(refreshModels
|
|
6
|
+
* 钩子 falsy 返回=保留注入集);PS-88 是 D5 运行中注册对账的承重语义(扩展工厂返回后
|
|
7
|
+
* registerProvider 直连注册表立即生效)。pi 升级时 `scripts/check-pi-semantics.mjs` 的四包
|
|
8
|
+
* 版本门禁会 fail 并列出待重验条目,本文件即重验入口——装配顺序 / 幂等语义 / 映射替换 /
|
|
9
|
+
* 官方导出 / 钩子 falsy 保留 / post-bind 直连任一漂移都在此变红,不依赖人读 ADR 记忆。
|
|
10
|
+
*
|
|
11
|
+
* 断言分两层:
|
|
12
|
+
* - **源码层**:读 pi 实装 dist 文件,断言关键语句的存在与**先后顺序**(装配链的顺序
|
|
13
|
+
* 无法用单次调用观测,只能从实装推演——顺序契约本身是断言对象)。
|
|
14
|
+
* - **运行时层**:对实装版 pi 真实调用(`ModelRuntime` / `createExtensionRuntime` /
|
|
15
|
+
* `ExtensionRunner` / pi-ai 官方导出),断言返回结构与合并语义。禁 mock pi 本体——mock
|
|
16
|
+
* 掉的正是被测面。
|
|
17
|
+
*
|
|
18
|
+
* 行号锚全部基于 pi@1.0.0 实装实测(2026-10-20)。源码层断言按锚点语句内容定位
|
|
19
|
+
* (lineOf / lineOfAfter):pi bump 后纯行号漂移不断言也不红;锚点语句被改写/删除或
|
|
20
|
+
* 相对顺序漂移才红——此时按报错重新核对锚点语义是否仍成立,再更新
|
|
21
|
+
* docs/pi-semantics.json 的 note。
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
24
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { tmpdir } from 'node:os'
|
|
26
|
+
import { dirname, join } from 'node:path'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
import {
|
|
29
|
+
ExtensionRunner,
|
|
30
|
+
ModelRegistry,
|
|
31
|
+
ModelRuntime,
|
|
32
|
+
SessionManager,
|
|
33
|
+
createExtensionRuntime,
|
|
34
|
+
type ExtensionActions,
|
|
35
|
+
type ExtensionContextActions,
|
|
36
|
+
} from '@earendil-works/pi-coding-agent'
|
|
37
|
+
import {
|
|
38
|
+
getBuiltinProviders,
|
|
39
|
+
getBuiltinModels,
|
|
40
|
+
getBuiltinModelDataGeneratedAt,
|
|
41
|
+
} from '@earendil-works/pi-ai/providers/all'
|
|
42
|
+
|
|
43
|
+
import { expectVisibleIds, fullBaselineConfig, runtimeWithFullBaseline, swapToSingleExpectShrink } from './pi-runtime-testkit.ts'
|
|
44
|
+
|
|
45
|
+
// ── pi 实装 dist 源码读取(源码层断言的输入)────────────────────────────
|
|
46
|
+
// 两包的 exports map 都不暴露 ./package.json,改经主入口解析(`.` → dist/index.js),
|
|
47
|
+
// 取 dirname 即包内 dist 根——与硬编码 node_modules 路径等价但不依赖仓库布局假设。
|
|
48
|
+
// 用 import.meta.resolve(ESM 解析,认 exports 的 import 条件);CJS require.resolve
|
|
49
|
+
// 不认 import 条件会报 No "exports" main defined。
|
|
50
|
+
const PI_AGENT_DIST = dirname(fileURLToPath(import.meta.resolve('@earendil-works/pi-coding-agent')))
|
|
51
|
+
const PI_AI_DIST = dirname(fileURLToPath(import.meta.resolve('@earendil-works/pi-ai')))
|
|
52
|
+
|
|
53
|
+
function readDist(pkgDist: string, relPath: string): string {
|
|
54
|
+
return readFileSync(join(pkgDist, relPath), 'utf-8')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 关键语句在源码中的行号(1-based)。找不到返回 -1,调用方据此断言存在性。 */
|
|
58
|
+
function lineOf(source: string, needle: string): number {
|
|
59
|
+
const idx = source.indexOf(needle)
|
|
60
|
+
if (idx === -1) return -1
|
|
61
|
+
return source.slice(0, idx).split('\n').length
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `needle` 在 `startNeedle` **之后**首次出现的行号(1-based)。
|
|
66
|
+
*
|
|
67
|
+
* 必要性:同一语句在多个函数里重复出现时(如 `void this.refresh({ allowNetwork: false })`
|
|
68
|
+
* 在 registerNativeProvider / registerProvider / unregisterProvider / registerVirtualModel
|
|
69
|
+
* 四处都有;`no "api" specified` 抛错在 modelFromJson 与 extensionModelFromDefinition 两处
|
|
70
|
+
* 都有),全文首次匹配会命中错误的那个函数——顺序断言随即变成假红。限定搜索区间后
|
|
71
|
+
* 「X 在 Y 函数体内」才是可判定的命题。
|
|
72
|
+
*/
|
|
73
|
+
function lineOfAfter(source: string, startNeedle: string, needle: string): number {
|
|
74
|
+
const start = source.indexOf(startNeedle)
|
|
75
|
+
if (start === -1) return -1
|
|
76
|
+
const offset = source.slice(0, start).split('\n').length - 1
|
|
77
|
+
const idx = source.slice(start).indexOf(needle)
|
|
78
|
+
if (idx === -1) return -1
|
|
79
|
+
return offset + source.slice(start, start + idx).split('\n').length
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const AGENT_SERVICES = () => readDist(PI_AGENT_DIST, 'core/agent-session-services.js')
|
|
83
|
+
const LOADER = () => readDist(PI_AGENT_DIST, 'core/extensions/loader.js')
|
|
84
|
+
const RUNNER = () => readDist(PI_AGENT_DIST, 'core/extensions/runner.js')
|
|
85
|
+
const RESOURCE_LOADER = () => readDist(PI_AGENT_DIST, 'core/resource-loader.js')
|
|
86
|
+
const MODEL_RUNTIME = () => readDist(PI_AGENT_DIST, 'core/model-runtime.js')
|
|
87
|
+
const PROVIDER_COMPOSER = () => readDist(PI_AGENT_DIST, 'core/provider-composer.js')
|
|
88
|
+
const PI_AI_ALL = () => readDist(PI_AI_DIST, 'providers/all.js')
|
|
89
|
+
|
|
90
|
+
/** 被测 catalog provider:内置基线 3 个 chat 模型。 */
|
|
91
|
+
const PROVIDER = 'minimax-cn'
|
|
92
|
+
|
|
93
|
+
let tmpDir: string
|
|
94
|
+
async function newRuntime() {
|
|
95
|
+
return ModelRuntime.create({
|
|
96
|
+
modelsPath: null,
|
|
97
|
+
refreshOnCreate: false,
|
|
98
|
+
authPath: join(tmpDir, 'auth.json'),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
beforeAll(() => {
|
|
103
|
+
// 与 sdk-contract.test.ts 同形态隔离:内存 ModelsStore + 不发起网络刷新 + tmp 凭据路径
|
|
104
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'pi-semantics-catalog-'))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
112
|
+
describe('PS-82 —— 装配顺序:工厂期注册先于 --model 解析与 session_start', () => {
|
|
113
|
+
it('源码层:createAgentSessionServices 内六个环节按装配链顺序出现', () => {
|
|
114
|
+
const src = AGENT_SERVICES()
|
|
115
|
+
const anchors = [
|
|
116
|
+
['ModelRuntime.create', 'ModelRuntime.create({'],
|
|
117
|
+
['resourceLoader.reload(扩展工厂执行入口)', 'await resourceLoader.reload('],
|
|
118
|
+
['pending 队列 flush 进注册表', 'modelRuntime.registerProvider(name, config);'],
|
|
119
|
+
['一次本地 refresh', 'await modelRuntime.refresh({ allowNetwork: false });'],
|
|
120
|
+
]
|
|
121
|
+
const lines = anchors.map(([label, needle]) => {
|
|
122
|
+
const line = lineOf(src, needle)
|
|
123
|
+
expect(line, `装配链环节缺失: ${label}(${needle})`).toBeGreaterThan(0)
|
|
124
|
+
return { label, line }
|
|
125
|
+
})
|
|
126
|
+
// 严格递增 = 顺序契约成立
|
|
127
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
128
|
+
expect(
|
|
129
|
+
lines[i].line,
|
|
130
|
+
`装配顺序颠倒: ${lines[i - 1].label}(${lines[i - 1].line}) 应早于 ${lines[i].label}(${lines[i].line})`,
|
|
131
|
+
).toBeGreaterThan(lines[i - 1].line)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('源码层:--model 解析与 session_start 都在 refresh 之后(工厂期注册覆盖得到它们)', () => {
|
|
136
|
+
const src = AGENT_SERVICES()
|
|
137
|
+
const refreshLine = lineOf(src, 'await modelRuntime.refresh({ allowNetwork: false });')
|
|
138
|
+
const modelLine = lineOf(src, 'model: options.model,')
|
|
139
|
+
const sessionStartLine = lineOf(src, 'sessionStartEvent: options.sessionStartEvent,')
|
|
140
|
+
expect(refreshLine).toBeGreaterThan(0)
|
|
141
|
+
expect(modelLine, 'createAgentSession 的 model 入参(--model 解析的落点)缺失').toBeGreaterThan(refreshLine)
|
|
142
|
+
expect(sessionStartLine, 'sessionStartEvent(session_start 的 emit 载荷)缺失').toBeGreaterThan(refreshLine)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('源码层:扩展工厂在 resourceLoader.reload 内执行(工厂期 registerProvider 的时机前提)', () => {
|
|
146
|
+
const loader = RESOURCE_LOADER()
|
|
147
|
+
const reloadLine = lineOf(loader, 'async reload(options) {')
|
|
148
|
+
const loadCachedLine = lineOf(loader, 'const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus);')
|
|
149
|
+
expect(reloadLine).toBeGreaterThan(0)
|
|
150
|
+
expect(loadCachedLine, 'reload 内的 loadExtensionsCached 调用缺失').toBeGreaterThan(reloadLine)
|
|
151
|
+
|
|
152
|
+
const extLoader = LOADER()
|
|
153
|
+
const factoryLine = lineOf(extLoader, 'await factory(load.api);')
|
|
154
|
+
expect(factoryLine, '扩展工厂调用点(initializeExtension)缺失').toBeGreaterThan(0)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('运行时层:工厂期 registerProvider 进 pending 队列而非直写注册表', () => {
|
|
158
|
+
// 扩展工厂体内拿到的 pi.registerProvider 是「排队」语义——队列由装配链在
|
|
159
|
+
// --model 解析之前统一 flush。这是 D1(工厂期注册)能覆盖症状 8 的机制本体。
|
|
160
|
+
const runtime = createExtensionRuntime()
|
|
161
|
+
expect(runtime.pendingProviderRegistrations).toEqual([])
|
|
162
|
+
|
|
163
|
+
runtime.registerProvider(PROVIDER, { models: [] })
|
|
164
|
+
expect(runtime.pendingProviderRegistrations).toHaveLength(1)
|
|
165
|
+
expect(runtime.pendingProviderRegistrations[0]).toMatchObject({ name: PROVIDER })
|
|
166
|
+
|
|
167
|
+
// 队列可被 unregisterProvider 撤销(工厂期纠错通道)
|
|
168
|
+
runtime.unregisterProvider(PROVIDER)
|
|
169
|
+
expect(runtime.pendingProviderRegistrations).toEqual([])
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
174
|
+
describe('PS-83 —— registerProvider 重复注册幂等 + 尾随零网络本地 refresh', () => {
|
|
175
|
+
it('运行时层:有值字段合并(models 在只注册 refreshModels 时保留)', async () => {
|
|
176
|
+
const { rt, baseline } = await runtimeWithFullBaseline(PROVIDER, tmpDir)
|
|
177
|
+
|
|
178
|
+
// 第二次只注册 refreshModels → models 不得被清空(字段级合并,非整体替换 config)
|
|
179
|
+
rt.registerProvider(PROVIDER, { refreshModels: async () => undefined })
|
|
180
|
+
const cfg = rt.getRegisteredProviderConfig(PROVIDER)
|
|
181
|
+
expect(cfg?.models).toHaveLength(baseline.length)
|
|
182
|
+
expect(typeof cfg?.refreshModels).toBe('function')
|
|
183
|
+
expectVisibleIds(rt, PROVIDER, baseline.map((m) => m.id))
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('运行时层:models 数组整体替换(不是与上一次注册的 models 数组合并)', async () => {
|
|
187
|
+
const { rt, baseline } = await runtimeWithFullBaseline(PROVIDER, tmpDir)
|
|
188
|
+
|
|
189
|
+
// 只留一个 = 整体替换;若为合并则仍是 3 个
|
|
190
|
+
swapToSingleExpectShrink(rt, PROVIDER, baseline[0])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('运行时层:undefined 值不覆盖已有注册(合并只取有值字段)', async () => {
|
|
194
|
+
const baseline = getBuiltinModels(PROVIDER)
|
|
195
|
+
const rt = await newRuntime()
|
|
196
|
+
rt.registerProvider(PROVIDER, { name: 'Kept Name', ...fullBaselineConfig(PROVIDER) })
|
|
197
|
+
|
|
198
|
+
rt.registerProvider(PROVIDER, { name: undefined, models: undefined })
|
|
199
|
+
const cfg = rt.getRegisteredProviderConfig(PROVIDER)
|
|
200
|
+
expect(cfg?.name).toBe('Kept Name')
|
|
201
|
+
expect(cfg?.models).toHaveLength(baseline.length)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('源码层:registerProvider 尾随一次 allowNetwork:false 的本地 refresh', () => {
|
|
205
|
+
const src = MODEL_RUNTIME()
|
|
206
|
+
const registerLine = lineOf(src, 'registerProvider(providerId, config) {')
|
|
207
|
+
const unregisterLine = lineOf(src, 'unregisterProvider(providerId) {')
|
|
208
|
+
expect(registerLine).toBeGreaterThan(0)
|
|
209
|
+
expect(unregisterLine, 'unregisterProvider(registerProvider 的函数体下界)缺失').toBeGreaterThan(registerLine)
|
|
210
|
+
|
|
211
|
+
// 全部锚点限定在 registerProvider 函数体内——同一语句在四个 register* 方法里重复出现
|
|
212
|
+
const validateLine = lineOfAfter(src, 'registerProvider(providerId, config) {', 'validateExtensionProvider(providerId, this.builtins.get(providerId), this.config.getProvider(providerId), config);')
|
|
213
|
+
const mergeLine = lineOfAfter(src, 'registerProvider(providerId, config) {', 'const previous = this.extensionProviders.get(providerId);')
|
|
214
|
+
const assignLine = lineOfAfter(src, 'registerProvider(providerId, config) {', 'effective[key] = value;')
|
|
215
|
+
const refreshLine = lineOfAfter(src, 'registerProvider(providerId, config) {', 'void this.refresh({ allowNetwork: false });')
|
|
216
|
+
|
|
217
|
+
expect(validateLine, '注册前结构校验缺失').toBeGreaterThan(registerLine)
|
|
218
|
+
expect(mergeLine, 'previous 合并读取缺失').toBeGreaterThan(validateLine)
|
|
219
|
+
expect(assignLine, '有值字段合并赋值缺失').toBeGreaterThan(mergeLine)
|
|
220
|
+
expect(refreshLine, 'registerProvider 体内的尾随 refresh 缺失').toBeGreaterThan(assignLine)
|
|
221
|
+
expect(refreshLine, '尾随 refresh 应在 registerProvider 函数体内').toBeLessThan(unregisterLine)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
226
|
+
describe('PS-84 —— extension 层 models 即最终可见集合 + 注册时逐模型结构校验', () => {
|
|
227
|
+
it('运行时层:models 数组是该 provider 的最终可见集合(映射替换,非与内置层合并)', async () => {
|
|
228
|
+
const baseline = getBuiltinModels(PROVIDER)
|
|
229
|
+
const rt = await newRuntime()
|
|
230
|
+
// 注入集只含 1 个模型 → 可见集合收缩为 1(内置层 3 个未被保留)
|
|
231
|
+
swapToSingleExpectShrink(rt, PROVIDER, baseline[0])
|
|
232
|
+
// 反向:注入全集 → 可见集合 = 全集
|
|
233
|
+
rt.registerProvider(PROVIDER, fullBaselineConfig(PROVIDER))
|
|
234
|
+
expectVisibleIds(rt, PROVIDER, baseline.map((m) => m.id))
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('运行时层:缺 api/baseUrl 且无同类型基线可继承 → 注册抛错(结构校验生效)', async () => {
|
|
238
|
+
const rt = await newRuntime()
|
|
239
|
+
expect(() =>
|
|
240
|
+
rt.registerProvider('no-such-catalog-provider', {
|
|
241
|
+
models: [
|
|
242
|
+
{
|
|
243
|
+
id: 'No-Api',
|
|
244
|
+
name: 'No-Api',
|
|
245
|
+
input: ['text'],
|
|
246
|
+
cost: { input: 0, output: 0 },
|
|
247
|
+
reasoning: false,
|
|
248
|
+
contextWindow: 1,
|
|
249
|
+
maxTokens: 1,
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
}),
|
|
253
|
+
).toThrow(/no "api" specified/)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('运行时层:provider 级校验——streamSimple 无 api 注册期即抛错,且 broken 注册不污染 stored config', async () => {
|
|
257
|
+
const rt = await newRuntime()
|
|
258
|
+
expect(() =>
|
|
259
|
+
rt.registerProvider('no-such-catalog-provider', {
|
|
260
|
+
streamSimple: () => {
|
|
261
|
+
throw new Error('unused')
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
).toThrow(/"api" is required when registering streamSimple/)
|
|
265
|
+
// 抛错的注册不落进注册表(broken re-registration 不得污染 stored config)
|
|
266
|
+
expect(rt.getRegisteredProviderConfig('no-such-catalog-provider')).toBeUndefined()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('运行时层:compose 期异常落 compositionErrors 并经 getError 可见(R4 运行中归因通道)', async () => {
|
|
270
|
+
// R4 的运行中归因依赖「compose 失败落入 compositionErrors 并经 getError 可见」。
|
|
271
|
+
// 构造 = 注册期合法(全基线过校验),随后落盘写坏的用户配置 models.json 条目使
|
|
272
|
+
// compose 期变坏——配置变更使 compose 抛错而注册期校验已过的窗口正是 R4 语义。
|
|
273
|
+
// 两个构造约束(pi@1.0.0 实测):坏条目须落在无内置基线的 provider 上(有基线时
|
|
274
|
+
// modelFromJson 的四级兜底静默继承 api/baseUrl 不抛错,sdk-contract.test.ts 第③组
|
|
275
|
+
// 已锁);条目还须过 models.json 的 config schema(cost.cacheRead/cacheWrite 必填)
|
|
276
|
+
// ——schema 层错误走 configError(getError 首段),不是本用例要锁的
|
|
277
|
+
// compositionErrors 通道。
|
|
278
|
+
const modelsPath = join(tmpDir, 'compose-error-models.json')
|
|
279
|
+
const rt = await ModelRuntime.create({
|
|
280
|
+
modelsPath,
|
|
281
|
+
refreshOnCreate: false,
|
|
282
|
+
authPath: join(tmpDir, 'auth.json'),
|
|
283
|
+
})
|
|
284
|
+
rt.registerProvider(PROVIDER, fullBaselineConfig(PROVIDER))
|
|
285
|
+
await rt.refresh({ allowNetwork: false })
|
|
286
|
+
// 前置:初始态无组合错误(自证后置断言非恒真)
|
|
287
|
+
expect(rt.getError()).toBeUndefined()
|
|
288
|
+
writeFileSync(
|
|
289
|
+
modelsPath,
|
|
290
|
+
JSON.stringify({
|
|
291
|
+
providers: {
|
|
292
|
+
'no-such-catalog-provider': {
|
|
293
|
+
models: [
|
|
294
|
+
{
|
|
295
|
+
id: 'Broken-Compose',
|
|
296
|
+
name: 'Broken-Compose',
|
|
297
|
+
input: ['text'],
|
|
298
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
299
|
+
reasoning: false,
|
|
300
|
+
contextWindow: 1,
|
|
301
|
+
maxTokens: 1,
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
)
|
|
308
|
+
await rt.refresh({ allowNetwork: false })
|
|
309
|
+
// composeProvider 捕获组合异常落 compositionErrors,getError 拼 `Provider "<id>": <msg>`
|
|
310
|
+
// (dist/core/model-runtime.js composeProvider catch 分支 + getError 拼接段)——
|
|
311
|
+
// provider-live-sync 运行中 tick 消费 getError() 的正是这个形态(非空即 error 日志)。
|
|
312
|
+
expect(rt.getError()).toMatch(/Provider "/)
|
|
313
|
+
expect(rt.getError()).toMatch(/no "api" specified/)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('源码层:applyExtension 对 models 做 map 替换(非 concat / merge)', () => {
|
|
317
|
+
const src = PROVIDER_COMPOSER()
|
|
318
|
+
const applyLine = lineOf(src, 'function applyExtension(providerId, models, config) {')
|
|
319
|
+
const mapLine = lineOf(src, 'return config.models.map((definition) => extensionModelFromDefinition(providerId, models, config, definition));')
|
|
320
|
+
expect(applyLine).toBeGreaterThan(0)
|
|
321
|
+
expect(mapLine, 'applyExtension 的 map 替换分支缺失').toBeGreaterThan(applyLine)
|
|
322
|
+
// 无 models 时走 baseUrl 覆盖或原样返回(不凭空造模型)
|
|
323
|
+
expect(lineOf(src, 'return config.baseUrl ? models.map((model) => ({ ...model, baseUrl: config.baseUrl })) : [...models];')).toBeGreaterThan(applyLine)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('源码层:extensionModelFromDefinition 逐个定义校验 api / baseUrl 后抛错', () => {
|
|
327
|
+
const src = PROVIDER_COMPOSER()
|
|
328
|
+
const fnStart = 'function extensionModelFromDefinition(providerId, models, config, definition) {'
|
|
329
|
+
const fnLine = lineOf(src, fnStart)
|
|
330
|
+
// 两个抛错语句在 modelFromJson 里也有一份同款文案,必须限定在 extension 分支内
|
|
331
|
+
const apiThrow = lineOfAfter(src, fnStart, 'throw new Error(`Provider ${providerId}, model ${definition.id}: no "api" specified.')
|
|
332
|
+
const baseUrlThrow = lineOfAfter(src, fnStart, 'throw new Error(`Provider ${providerId}: "baseUrl" is required when defining custom models.`);')
|
|
333
|
+
expect(fnLine).toBeGreaterThan(0)
|
|
334
|
+
expect(apiThrow, 'extensionModelFromDefinition 内 api 缺失抛错点缺失').toBeGreaterThan(fnLine)
|
|
335
|
+
expect(baseUrlThrow, 'extensionModelFromDefinition 内 baseUrl 缺失抛错点缺失').toBeGreaterThan(fnLine)
|
|
336
|
+
// 四级兜底继承的实装落点(R3 保守档的对照面)
|
|
337
|
+
const defaultsLine = lineOf(src, 'function findExtensionModelDefaults(models, definition) {')
|
|
338
|
+
expect(defaultsLine, 'findExtensionModelDefaults(兜底继承)缺失').toBeGreaterThan(0)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('源码层:validateExtensionProvider 是 registerProvider 的前置校验入口', () => {
|
|
342
|
+
const composer = PROVIDER_COMPOSER()
|
|
343
|
+
expect(lineOf(composer, 'export function validateExtensionProvider(providerId, base, modelsConfig, extension) {')).toBeGreaterThan(0)
|
|
344
|
+
const runtime = MODEL_RUNTIME()
|
|
345
|
+
expect(
|
|
346
|
+
lineOf(runtime, 'validateExtensionProvider(providerId, this.builtins.get(providerId), this.config.getProvider(providerId), config);'),
|
|
347
|
+
).toBeGreaterThan(0)
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
352
|
+
describe('PS-85 —— 扩展进程内可经 pi-ai 官方导出拿到三态判据全部材料', () => {
|
|
353
|
+
it('运行时层:三个导出可调用且返回有效值', () => {
|
|
354
|
+
const providers = getBuiltinProviders()
|
|
355
|
+
expect(Array.isArray(providers)).toBe(true)
|
|
356
|
+
expect(providers.length).toBeGreaterThan(0)
|
|
357
|
+
expect(providers).toContain(PROVIDER)
|
|
358
|
+
|
|
359
|
+
const models = getBuiltinModels(PROVIDER)
|
|
360
|
+
expect(Array.isArray(models)).toBe(true)
|
|
361
|
+
expect(models.length).toBeGreaterThan(0)
|
|
362
|
+
// 基线模型须带三态判据与并集计算所需的字段
|
|
363
|
+
for (const m of models) {
|
|
364
|
+
expect(typeof m.id).toBe('string')
|
|
365
|
+
expect(typeof m.api).toBe('string')
|
|
366
|
+
expect(typeof m.baseUrl).toBe('string')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const generatedAt = getBuiltinModelDataGeneratedAt()
|
|
370
|
+
expect(typeof generatedAt).toBe('number')
|
|
371
|
+
expect(Number.isFinite(generatedAt)).toBe(true)
|
|
372
|
+
expect(generatedAt).toBeGreaterThan(0)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('运行时层:staleness 基准时刻与内置基线同源(同一 manifest 的同一字段)', () => {
|
|
376
|
+
// D3 的判定基准 = pi-ai 官方导出,与 pi 执行侧 withRemoteCatalog 用的时刻锚完全同源。
|
|
377
|
+
// 同一进程内两次读取必须稳定(manifest 是编译期常量)。
|
|
378
|
+
expect(getBuiltinModelDataGeneratedAt()).toBe(getBuiltinModelDataGeneratedAt())
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('运行时层:未知 provider 的基线为空数组(never-seen 判据的输入形态)', () => {
|
|
382
|
+
expect(getBuiltinModels('no-such-catalog-provider')).toEqual([])
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('源码层:三个导出实装于 pi-ai 的 providers/all.js', () => {
|
|
386
|
+
const src = PI_AI_ALL()
|
|
387
|
+
expect(lineOf(src, 'export function getBuiltinProviders() {')).toBeGreaterThan(0)
|
|
388
|
+
expect(lineOf(src, 'export function getBuiltinModels(provider) {')).toBeGreaterThan(0)
|
|
389
|
+
expect(lineOf(src, 'export function getBuiltinModelDataGeneratedAt() {')).toBeGreaterThan(0)
|
|
390
|
+
// 时刻锚读的是编译期 manifest 的 generatedAt 字段(非运行时时钟)
|
|
391
|
+
expect(lineOf(src, 'const generatedAt = Date.parse(modelDataManifest.generatedAt);')).toBeGreaterThan(0)
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
396
|
+
describe('PS-87 —— refreshModels 钩子 falsy 返回=保留注入集(R7 承重)', () => {
|
|
397
|
+
it('运行时层:钩子返回 undefined → refresh() 后注入集保留(发布分支被跳过)', async () => {
|
|
398
|
+
const { rt, baseline } = await runtimeWithFullBaseline(PROVIDER, tmpDir)
|
|
399
|
+
|
|
400
|
+
// R7 形态:钩子按实装语义返回 undefined(「本次不发布新列表」,瞬时读失败不清空)。
|
|
401
|
+
// hookCalled 自证钩子被真实调用——pi-ai ModelsStore.refresh 对 refreshable provider
|
|
402
|
+
// 无论 allowNetwork 都先跑一轮 allowNetwork:false 的 phase(:195 早退 :198 之前),
|
|
403
|
+
// 否则「注入集不变」可能是「钩子根本没被调用」的假绿。
|
|
404
|
+
let hookCalled = false
|
|
405
|
+
rt.registerProvider(PROVIDER, { refreshModels: async () => { hookCalled = true; return undefined } })
|
|
406
|
+
await rt.refresh({ allowNetwork: false })
|
|
407
|
+
|
|
408
|
+
expect(hookCalled).toBe(true)
|
|
409
|
+
expectVisibleIds(rt, PROVIDER, baseline.map((m) => m.id))
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('运行时层:对照——同一链路钩子返回有值列表时发布分支生效(可见集合替换)', async () => {
|
|
413
|
+
const { rt, baseline } = await runtimeWithFullBaseline(PROVIDER, tmpDir)
|
|
414
|
+
|
|
415
|
+
rt.registerProvider(PROVIDER, { refreshModels: async () => [{ ...baseline[0] }] })
|
|
416
|
+
await rt.refresh({ allowNetwork: false })
|
|
417
|
+
|
|
418
|
+
expectVisibleIds(rt, PROVIDER, [baseline[0].id])
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('源码层:类型声明不含 undefined + 合成包装的 falsy 发布分支', () => {
|
|
422
|
+
// 声明层锚:pi 声明不收 undefined——catalog-bridge 侧宽一档声明(RefreshModelsFn)
|
|
423
|
+
// + 显式断言挂 ProviderConfig 的依据(类型声明若改收 undefined,断言即多余可清理)
|
|
424
|
+
const types = readDist(PI_AGENT_DIST, 'core/extensions/types.d.ts')
|
|
425
|
+
expect(
|
|
426
|
+
lineOf(types, 'refreshModels?(context: RefreshModelsContext): Promise<ProviderModelConfig[]>;'),
|
|
427
|
+
).toBeGreaterThan(0)
|
|
428
|
+
|
|
429
|
+
const src = PROVIDER_COMPOSER()
|
|
430
|
+
const callLine = lineOf(src, 'refreshed = await extension.refreshModels(context);')
|
|
431
|
+
expect(callLine).toBeGreaterThan(0)
|
|
432
|
+
// falsy 分支:if (refreshed) 才 applyExtension 发布——返回 undefined 即跳过(保留注入)
|
|
433
|
+
const branchLine = lineOfAfter(src, 'refreshed = await extension.refreshModels(context);', 'if (refreshed) {')
|
|
434
|
+
expect(branchLine, '发布分支 if (refreshed) 缺失').toBeGreaterThan(callLine)
|
|
435
|
+
const publishLine = lineOfAfter(
|
|
436
|
+
src,
|
|
437
|
+
'if (refreshed) {',
|
|
438
|
+
'applyExtension(providerId, applyModelsJson(providerId, getAllProviderModels(base), config), {',
|
|
439
|
+
)
|
|
440
|
+
expect(publishLine, '发布分支内的 applyExtension 缺失').toBeGreaterThan(branchLine)
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
445
|
+
describe('PS-88 —— 扩展工厂返回后 registerProvider 直连注册表立即生效(D5 注册对账承重)', () => {
|
|
446
|
+
/** 无内置基线的合成 provider:注册前可见集合恒空,注册后出现即「立即生效」的直接证据。 */
|
|
447
|
+
const SYNTHETIC = 'no-such-catalog-provider'
|
|
448
|
+
|
|
449
|
+
/** 合成 provider 的完整模型定义(克隆一条内置模型换 id,api/baseUrl 齐全、过结构校验)。 */
|
|
450
|
+
function syntheticModel(id: string) {
|
|
451
|
+
return { ...getBuiltinModels(PROVIDER)[0], id, name: id }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* bindCore 的两组回调桩:bindCore 只是把它们拷进 runtime(runner.js bindCore 开头的逐字段
|
|
456
|
+
* 赋值),本探针不触发任何会话动作,故一律用抛错桩——被意外调用即红,而不是静默假绿。
|
|
457
|
+
*/
|
|
458
|
+
const notBound = (): never => {
|
|
459
|
+
throw new Error('probe stub: session action must not be invoked by registerProvider')
|
|
460
|
+
}
|
|
461
|
+
const actions: ExtensionActions = {
|
|
462
|
+
sendMessage: notBound,
|
|
463
|
+
sendUserMessage: notBound,
|
|
464
|
+
appendEntry: notBound,
|
|
465
|
+
setSessionName: notBound,
|
|
466
|
+
getSessionName: notBound,
|
|
467
|
+
setLabel: notBound,
|
|
468
|
+
getActiveTools: notBound,
|
|
469
|
+
getAllTools: notBound,
|
|
470
|
+
getSettings: notBound,
|
|
471
|
+
setActiveTools: notBound,
|
|
472
|
+
refreshTools: notBound,
|
|
473
|
+
getCommands: notBound,
|
|
474
|
+
setModel: notBound,
|
|
475
|
+
getThinkingLevel: notBound,
|
|
476
|
+
setThinkingLevel: notBound,
|
|
477
|
+
}
|
|
478
|
+
const contextActions: ExtensionContextActions = {
|
|
479
|
+
getModel: notBound,
|
|
480
|
+
getScopedModels: notBound,
|
|
481
|
+
isIdle: notBound,
|
|
482
|
+
isProjectTrusted: notBound,
|
|
483
|
+
getSignal: notBound,
|
|
484
|
+
abort: notBound,
|
|
485
|
+
hasPendingMessages: notBound,
|
|
486
|
+
shutdown: notBound,
|
|
487
|
+
getContextUsage: notBound,
|
|
488
|
+
compact: notBound,
|
|
489
|
+
getSystemPrompt: notBound,
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** 真实装配骨架:扩展 runtime + 真实 ModelRuntime + ExtensionRunner(与 agent-session.js 同形态:ModelRegistry 包装 ModelRuntime)。 */
|
|
493
|
+
async function newRunnerSetup() {
|
|
494
|
+
const runtime = createExtensionRuntime()
|
|
495
|
+
const rt = await newRuntime()
|
|
496
|
+
const runner = new ExtensionRunner([], runtime, tmpDir, SessionManager.inMemory(tmpDir), new ModelRegistry(rt))
|
|
497
|
+
return { runtime, rt, runner }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
it('运行时层:bindCore 前排队不可见;bindCore flush 后可见;bindCore 后再注册立即可见且不再排队', async () => {
|
|
501
|
+
const { runtime, rt, runner } = await newRunnerSetup()
|
|
502
|
+
|
|
503
|
+
// 工厂期形态(pre-bind):只排队,注册表不动——PS-82 的「工厂期进 pending 队列」在此做对照
|
|
504
|
+
runtime.registerProvider(SYNTHETIC, { models: [syntheticModel('Factory-Model')] })
|
|
505
|
+
expect(runtime.pendingProviderRegistrations).toHaveLength(1)
|
|
506
|
+
expectVisibleIds(rt, SYNTHETIC, [])
|
|
507
|
+
|
|
508
|
+
// bindCore(无 providerActions → 直连 modelRegistry,runner.js :314 分支):flush 工厂期队列
|
|
509
|
+
runner.bindCore(actions, contextActions)
|
|
510
|
+
expect(runtime.pendingProviderRegistrations).toEqual([])
|
|
511
|
+
expectVisibleIds(rt, SYNTHETIC, ['Factory-Model'])
|
|
512
|
+
|
|
513
|
+
// post-bind 形态(provider-live-sync 运行中注册对账走的路径):同步调用返回即可见,
|
|
514
|
+
// 不进队列、不等任何 refresh / reload——catalog-bridge reconcile 对「工厂期 never-seen →
|
|
515
|
+
// 运行中变 fresh」补注册依赖的正是这一点
|
|
516
|
+
runtime.registerProvider(SYNTHETIC, { models: [syntheticModel('Factory-Model'), syntheticModel('Post-Bind-Model')] })
|
|
517
|
+
expect(runtime.pendingProviderRegistrations).toEqual([])
|
|
518
|
+
expectVisibleIds(rt, SYNTHETIC, ['Factory-Model', 'Post-Bind-Model'])
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it('运行时层:post-bind 注册一个全新 provider(工厂期从未注册过)同样立即可见', async () => {
|
|
522
|
+
const { runtime, rt, runner } = await newRunnerSetup()
|
|
523
|
+
runner.bindCore(actions, contextActions)
|
|
524
|
+
expectVisibleIds(rt, SYNTHETIC, [])
|
|
525
|
+
|
|
526
|
+
runtime.registerProvider(SYNTHETIC, { models: [syntheticModel('Late-Joiner')] })
|
|
527
|
+
expectVisibleIds(rt, SYNTHETIC, ['Late-Joiner'])
|
|
528
|
+
// 与 PS-83 一致:带 refreshModels 钩子的注册同样立即落表(对账注册携带钩子的形态)
|
|
529
|
+
runtime.registerProvider(SYNTHETIC, { refreshModels: async () => undefined })
|
|
530
|
+
expect(typeof rt.getRegisteredProviderConfig(SYNTHETIC)?.refreshModels).toBe('function')
|
|
531
|
+
expectVisibleIds(rt, SYNTHETIC, ['Late-Joiner'])
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it('运行时层:生产形态的 providerActions.registerProvider 分支(agent-session.js 第三参)同步直达同一 ModelRuntime', async () => {
|
|
535
|
+
const { runtime, rt, runner } = await newRunnerSetup()
|
|
536
|
+
let viaProviderActions = 0
|
|
537
|
+
runner.bindCore(actions, contextActions, {
|
|
538
|
+
registerProvider: (name, config) => {
|
|
539
|
+
viaProviderActions += 1
|
|
540
|
+
rt.registerProvider(name, config)
|
|
541
|
+
},
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
runtime.registerProvider(SYNTHETIC, { models: [syntheticModel('Via-Actions')] })
|
|
545
|
+
expect(viaProviderActions).toBe(1)
|
|
546
|
+
expectVisibleIds(rt, SYNTHETIC, ['Via-Actions'])
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('运行时层:post-bind 注册的结构校验同步抛错且不污染注册表(catalog-bridge R4 在运行中对账期可捕获)', async () => {
|
|
550
|
+
const { runtime, rt, runner } = await newRunnerSetup()
|
|
551
|
+
runner.bindCore(actions, contextActions)
|
|
552
|
+
|
|
553
|
+
// 工厂期同样的坏入参被 bindCore flush 时只进 emitError 不抛回扩展;post-bind 直连则同步抛——
|
|
554
|
+
// 这正是 reconcile 复用 R4 try/catch 的依据
|
|
555
|
+
expect(() =>
|
|
556
|
+
runtime.registerProvider(SYNTHETIC, {
|
|
557
|
+
models: [
|
|
558
|
+
{
|
|
559
|
+
id: 'No-Api',
|
|
560
|
+
name: 'No-Api',
|
|
561
|
+
input: ['text'],
|
|
562
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
563
|
+
reasoning: false,
|
|
564
|
+
contextWindow: 1,
|
|
565
|
+
maxTokens: 1,
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
}),
|
|
569
|
+
).toThrow(/no "api" specified/)
|
|
570
|
+
expect(rt.getRegisteredProviderConfig(SYNTHETIC)).toBeUndefined()
|
|
571
|
+
expectVisibleIds(rt, SYNTHETIC, [])
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('源码层:bindCore 在 flush pending 队列之后把 runtime.registerProvider 替换为直连 modelRegistry', () => {
|
|
575
|
+
const src = RUNNER()
|
|
576
|
+
const bindLine = lineOf(src, 'bindCore(actions, contextActions, providerActions) {')
|
|
577
|
+
expect(bindLine).toBeGreaterThan(0)
|
|
578
|
+
const flushLine = lineOfAfter(src, 'bindCore(actions, contextActions, providerActions) {', 'this.runtime.pendingProviderRegistrations = [];')
|
|
579
|
+
const commentLine = lineOfAfter(
|
|
580
|
+
src,
|
|
581
|
+
'bindCore(actions, contextActions, providerActions) {',
|
|
582
|
+
'// From this point on, provider registration/unregistration takes effect immediately',
|
|
583
|
+
)
|
|
584
|
+
const rebindLine = lineOfAfter(src, 'bindCore(actions, contextActions, providerActions) {', 'this.runtime.registerProvider = (name, config) => {')
|
|
585
|
+
const directLine = lineOfAfter(src, 'this.runtime.registerProvider = (name, config) => {', 'this.modelRegistry.registerProvider(name, config);')
|
|
586
|
+
expect(flushLine, 'bindCore 内 pending 队列清空缺失').toBeGreaterThan(bindLine)
|
|
587
|
+
expect(commentLine, '「takes effect immediately」注释缺失——post-bind 立即生效的契约陈述被改写').toBeGreaterThan(flushLine)
|
|
588
|
+
expect(rebindLine, 'bindCore 内 runtime.registerProvider 的 post-bind 替换缺失').toBeGreaterThan(commentLine)
|
|
589
|
+
expect(directLine, 'post-bind registerProvider 直连 modelRegistry.registerProvider 缺失').toBeGreaterThan(rebindLine)
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('源码层:loader 的 applyRuntimeChange 非 loading 态直接执行;api.registerProvider 经它转发;commit 置 active', () => {
|
|
593
|
+
const src = LOADER()
|
|
594
|
+
const stateLine = lineOf(src, 'let state = "loading";')
|
|
595
|
+
const applyLine = lineOf(src, 'const applyRuntimeChange = (change) => {')
|
|
596
|
+
const queueLine = lineOfAfter(src, 'const applyRuntimeChange = (change) => {', 'pendingRuntimeChanges.push(change);')
|
|
597
|
+
const directLine = lineOfAfter(src, 'pendingRuntimeChanges.push(change);', 'change();')
|
|
598
|
+
expect(stateLine).toBeGreaterThan(0)
|
|
599
|
+
expect(applyLine, 'applyRuntimeChange 缺失').toBeGreaterThan(stateLine)
|
|
600
|
+
expect(queueLine, 'loading 态排队分支缺失').toBeGreaterThan(applyLine)
|
|
601
|
+
expect(directLine, '非 loading 态直接执行分支缺失').toBeGreaterThan(queueLine)
|
|
602
|
+
|
|
603
|
+
const apiLine = lineOf(src, 'registerProvider(providerOrName, config) {')
|
|
604
|
+
const forwardLine = lineOfAfter(
|
|
605
|
+
src,
|
|
606
|
+
'registerProvider(providerOrName, config) {',
|
|
607
|
+
'applyRuntimeChange(() => runtime.registerProvider(providerOrName, config, extension.path));',
|
|
608
|
+
)
|
|
609
|
+
expect(apiLine, 'api.registerProvider 缺失').toBeGreaterThan(applyLine)
|
|
610
|
+
expect(forwardLine, 'api.registerProvider 未经 applyRuntimeChange 转发').toBeGreaterThan(apiLine)
|
|
611
|
+
|
|
612
|
+
const commitLine = lineOf(src, 'commit: () => {')
|
|
613
|
+
const replayLine = lineOfAfter(src, 'commit: () => {', 'for (const apply of pendingRuntimeChanges)')
|
|
614
|
+
const activeLine = lineOfAfter(src, 'commit: () => {', 'state = "active";')
|
|
615
|
+
expect(commitLine, 'commit 缺失').toBeGreaterThan(apiLine)
|
|
616
|
+
expect(replayLine, 'commit 内回放 pendingRuntimeChanges 缺失').toBeGreaterThan(commitLine)
|
|
617
|
+
expect(activeLine, 'commit 内 state = "active" 缺失').toBeGreaterThan(replayLine)
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
it('源码层:ModelRuntime.registerProvider 体内 recomposeProvider → updateModelSnapshot(注册即进可见快照)', () => {
|
|
621
|
+
const src = MODEL_RUNTIME()
|
|
622
|
+
const start = 'registerProvider(providerId, config) {'
|
|
623
|
+
const registerLine = lineOf(src, start)
|
|
624
|
+
const unregisterLine = lineOf(src, 'unregisterProvider(providerId) {')
|
|
625
|
+
const recomposeLine = lineOfAfter(src, start, 'this.recomposeProvider(providerId);')
|
|
626
|
+
const snapshotLine = lineOfAfter(src, start, 'this.updateModelSnapshot();')
|
|
627
|
+
expect(registerLine).toBeGreaterThan(0)
|
|
628
|
+
expect(recomposeLine, 'registerProvider 体内 recomposeProvider 缺失').toBeGreaterThan(registerLine)
|
|
629
|
+
expect(snapshotLine, 'registerProvider 体内 updateModelSnapshot 缺失').toBeGreaterThan(recomposeLine)
|
|
630
|
+
expect(snapshotLine, '快照更新应在 registerProvider 函数体内').toBeLessThan(unregisterLine)
|
|
631
|
+
})
|
|
632
|
+
})
|