@zhushanwen/pi-provider-live-sync 0.2.1 → 0.3.1

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * provider-live-sync 单元测试(设计 §4.2「单元(extension)」全部断言)。
3
3
  *
4
- * 被测面 = 纯函数(`readObservation` / `evaluateSnapshot`),不依赖 pi 运行时:
4
+ * 被测对象 = 纯函数(`readObservation` / `evaluateSnapshot`),不依赖 pi 运行时:
5
5
  * 决策与基线推进是全部语义所在,时钟/timer 不进断言(timer 只做调度)。
6
6
  *
7
7
  * 覆盖映射(设计行 → 用例):
@@ -19,12 +19,12 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
19
19
  import providerLiveSync, {
20
20
  evaluateSnapshot,
21
21
  readObservation,
22
- POLL_INTERVAL_MS,
23
22
  WATCHED_FILES,
24
23
  type FileObservation,
25
24
  type Snapshot,
26
25
  type WatchedFile,
27
26
  } from '../index.ts'
27
+ import { startWithRegistryCtx, tickOnce } from './tick-harness.ts'
28
28
 
29
29
  // tick 层用例:mock pi 宿主模块的 getAgentDir 指向真实 tmp fixture。纯函数用例不经
30
30
  // getAgentDir(readObservation 显式传目录),mock 对它们无影响。
@@ -110,7 +110,8 @@ describe('evaluateSnapshot —— D3 四态(models.json 缺失是 catalog-only
110
110
  const d = evaluateSnapshot(snap, obs({ 'models.json': { state: 'absent' }, 'auth.json': present('X') }))
111
111
  expect(d).toEqual({ action: 'skip-missing', missingFiles: ['models.json'] })
112
112
  // 持续缺失:抑制保持(每拍仍判定 skip-missing),但调用方按跃迁去重 → 只在第一拍记一行
113
- // 日志(`missingLogged`),后续拍零输出(稳态无噪声,设计 §3.6 行 2/S9a②)。
113
+ // 日志(`missingLogged`),后续拍零输出(抑制持续语义见 src/index.ts 的
114
+ // Snapshot.suppressed 注释与 evaluateSnapshot 实装)。
114
115
  const d2 = evaluateSnapshot(snap, obs({ 'models.json': { state: 'absent' }, 'auth.json': present('X') }))
115
116
  expect(d2).toEqual({ action: 'skip-missing', missingFiles: ['models.json'] })
116
117
  })
@@ -242,40 +243,13 @@ describe('tick 层 —— 首基线置位时机与 refresh 触发', () => {
242
243
  rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
243
244
  })
244
245
 
246
+ /** 经 harness 启动扩展(pi 宿主面 = appendEntry spy;拍屏障说明见 tick-harness 头注释)。 */
245
247
  function startWithFakeCtx(): { refresh: ReturnType<typeof vi.fn>; appendEntry: ReturnType<typeof vi.fn> } {
246
- const refresh = vi.fn(async () => ({ aborted: false, errors: new Map() }))
247
- const getError = vi.fn(() => null)
248
- const ctx = { modelRegistry: { refresh, getError } }
249
248
  const appendEntry = vi.fn()
250
- const on = vi.fn()
251
- providerLiveSync({ on, appendEntry } as unknown as ExtensionAPI)
252
- const handler = on.mock.calls.find((call) => call[0] === 'session_start')?.[1] as
253
- | ((event: unknown, sessionCtx: unknown) => void)
254
- | undefined
255
- expect(handler).toBeTypeOf('function')
256
- handler({ type: 'session_start' }, ctx)
249
+ const { refresh } = startWithRegistryCtx({ appendEntry })
257
250
  return { refresh, appendEntry }
258
251
  }
259
252
 
260
- /** 逐真实事件循环轮次(setImmediate 未被 fake)轮询等待谓词成立。 */
261
- async function waitFor(predicate: () => boolean, maxTurns = 5_000): Promise<void> {
262
- for (let i = 0; i < maxTurns && !predicate(); i += 1) {
263
- await new Promise<void>((resolve) => setImmediate(resolve))
264
- }
265
- }
266
-
267
- /**
268
- * 推进一个轮询周期并等本拍 tick **完整落地**。确定性拍屏障:tick 完成后其
269
- * `.finally(schedule)` 必然注册下一个 fake timer——等 `getTimerCount() >= 1`
270
- * 即证明本拍 readFile 链(真实线程池 I/O)已结束。固定轮次 flush 不行:线程池
271
- * 完成回调的到达轮次不定,偶发「tick 晚于下一拍的 writeFileSync 完成 → 新内容
272
- * 被当首基线」假红。
273
- */
274
- async function tickOnce(): Promise<void> {
275
- await vi.advanceTimersByTimeAsync(POLL_INTERVAL_MS)
276
- await waitFor(() => vi.getTimerCount() >= 1)
277
- }
278
-
279
253
  it('MF-1-1:首拍双 absent(catalog-only 全新安装)→ auth.json 首次出现 → 必须 refresh', async () => {
280
254
  const { refresh } = startWithFakeCtx()
281
255
  await tickOnce() // 首拍:双 absent 建基线,decision=none(首拍本就不刷新)
@@ -0,0 +1,57 @@
1
+ /**
2
+ * pi 运行时契约测试共用夹具(pi-semantics-catalog-injection 与 sdk-contract 的同款
3
+ * runtime 装配、注册入参与可见集合断言)。不 mock pi 本体——契约测试的价值即
4
+ * 「实装版 pi 的真实组合行为」,夹具只收敛重复形态,断言对象仍由各用例持有。
5
+ */
6
+ import { expect } from 'vitest'
7
+ import { join } from 'node:path'
8
+ import { ModelRuntime } from '@earendil-works/pi-coding-agent'
9
+ import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
10
+
11
+ /** pi-ai 内置基线模型(getBuiltinModels 的元素类型)。 */
12
+ export type BuiltinModel = ReturnType<typeof getBuiltinModels>[number]
13
+
14
+ /** 每用例独立 runtime:内存 ModelsStore + 不发起网络刷新 + tmp 凭据路径(同款隔离)。 */
15
+ async function newRuntime(authDir: string): Promise<ModelRuntime> {
16
+ return ModelRuntime.create({
17
+ modelsPath: null,
18
+ refreshOnCreate: false,
19
+ authPath: join(authDir, 'auth.json'),
20
+ })
21
+ }
22
+
23
+ /** 全量内置基线的注册入参(逐模型拷贝,注册侧不持共享引用)。 */
24
+ export function fullBaselineConfig(provider: string): { models: BuiltinModel[] } {
25
+ return { models: getBuiltinModels(provider).map((m) => ({ ...m })) }
26
+ }
27
+
28
+ /** 单模型注册入参(「整体替换 / 可见集合收缩」类用例)。 */
29
+ function singleModelConfig(model: BuiltinModel): { models: BuiltinModel[] } {
30
+ return { models: [{ ...model }] }
31
+ }
32
+
33
+ /**
34
+ * 新 runtime、注册全量内置基线并**自证可见集合 = 基线**(契约用例共同开场;断言单源
35
+ * 在此执行,防开场即坏),返回 baseline 供后续断言引用。
36
+ */
37
+ export async function runtimeWithFullBaseline(
38
+ provider: string,
39
+ authDir: string,
40
+ ): Promise<{ rt: ModelRuntime; baseline: BuiltinModel[] }> {
41
+ const baseline = getBuiltinModels(provider)
42
+ const rt = await newRuntime(authDir)
43
+ rt.registerProvider(provider, fullBaselineConfig(provider))
44
+ expectVisibleIds(rt, provider, baseline.map((m) => m.id))
45
+ return { rt, baseline }
46
+ }
47
+
48
+ /** 换注单模型并断言可见集合收缩为该模型(pi 重注册整体替换 / sdk 映射替换共用断言)。 */
49
+ export function swapToSingleExpectShrink(rt: ModelRuntime, provider: string, model: BuiltinModel): void {
50
+ rt.registerProvider(provider, singleModelConfig(model))
51
+ expectVisibleIds(rt, provider, [model.id])
52
+ }
53
+
54
+ /** 该 provider 可见模型 id 顺序断言(getModels 即最终可见集合)。 */
55
+ export function expectVisibleIds(rt: ModelRuntime, provider: string, ids: string[]): void {
56
+ expect(rt.getModels(provider).map((m) => m.id)).toEqual(ids)
57
+ }