Skip to content
Source
Sign inSign up

providerSkills.test.ts

432 lines16.5 KB

1import {
2 EnvironmentId,
3 ProviderDriverKind,
4 ProviderInstanceId,
5 type ServerProvider,
6} from "@t3tools/contracts";
7import * as Cause from "effect/Cause";
8import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
9import { afterEach, describe, expect, it, vi } from "vite-plus/test";
10​
11import {
12 createProviderWorkspaceSnapshotRefreshAtomFamily,
13 getProviderSkillsForSlashMenu,
14 hasCompleteProviderWorkspaceSnapshot,
15 isProviderWorkspaceSnapshotScanning,
16 resolveProviderSkillsForCwd,
17 resolveProviderSlashCommandsForCwd,
18 resolveProvidersForCwd,
19} from "./providerSkills.ts";
20​
21const provider = {
22 instanceId: ProviderInstanceId.make("codex"),
23 driver: ProviderDriverKind.make("codex"),
24 enabled: true,
25 installed: true,
26 version: "1.0.0",
27 status: "ready",
28 auth: { status: "authenticated" },
29 checkedAt: "2026-01-01T00:00:00.000Z",
30 models: [],
31 slashCommands: [{ name: "global" }],
32 skills: [{ name: "global", path: "/global/SKILL.md", enabled: true }],
33 workspaceSnapshots: [
34 {
35 cwd: "/workspace/project-a",
36 checkedAt: "2026-01-01T00:01:00.000Z",
37 slashCommands: [{ name: "project" }],
38 skills: [{ name: "project", path: "/workspace/project-a/SKILL.md", enabled: true }],
39 },
40 ],
41} satisfies ServerProvider;
42​
43describe("getProviderSkillsForSlashMenu", () => {
44 it("shows one row when enabled skills share a name", () => {
45 const skills = [
46 {
47 name: "babysit-pr",
48 path: "/Users/matt/.codex/skills/babysit-pr/SKILL.md",
49 enabled: true,
50 },
51 {
52 name: "browser",
53 path: "/Users/matt/.agents/skills/browser/SKILL.md",
54 enabled: true,
55 },
56 {
57 name: "babysit-pr",
58 path: "/Users/matt/.agents/skills/babysit-pr/SKILL.md",
59 enabled: true,
60 },
61 ];
62​
63 expect(getProviderSkillsForSlashMenu(skills, true).map((skill) => skill.name)).toEqual([
64 "babysit-pr",
65 "browser",
66 ]);
67 });
68​
69 it("keeps an enabled skill when a disabled duplicate appears first", () => {
70 const enabledSkill = {
71 name: "babysit-pr",
72 path: "/Users/matt/.agents/skills/babysit-pr/SKILL.md",
73 enabled: true,
74 };
75 const skills = [
76 {
77 name: "babysit-pr",
78 path: "/Users/matt/.codex/skills/babysit-pr/SKILL.md",
79 enabled: false,
80 },
81 enabledSkill,
82 ];
83​
84 expect(getProviderSkillsForSlashMenu(skills, true)).toEqual([enabledSkill]);
85 });
86});
87​
88describe("workspace provider snapshots", () => {
89 it("uses the cwd snapshot after a provider session has populated it", () => {
90 expect(resolveProviderSkillsForCwd(provider, "/workspace/project-a")).toEqual([
91 { name: "project", path: "/workspace/project-a/SKILL.md", enabled: true },
92 ]);
93 expect(resolveProviderSlashCommandsForCwd(provider, "/workspace/project-a")).toEqual([
94 { name: "project" },
95 ]);
96 });
97​
98 it("keeps the machine snapshot before this cwd has a provider snapshot", () => {
99 expect(resolveProviderSkillsForCwd(provider, "/workspace/project-b")).toEqual(provider.skills);
100 expect(resolveProviderSlashCommandsForCwd(provider, null)).toEqual(provider.slashCommands);
101 });
102​
103 it("uses partial workspace skills and commands while keeping discovery retryable", () => {
104 const partial = {
105 ...provider,
106 workspaceSnapshots: provider.workspaceSnapshots.map((snapshot) => ({
107 ...snapshot,
108 slashCommands: [{ name: "compact" }],
109 slashCommandsPending: true,
110 })),
111 } satisfies ServerProvider;
112 expect(resolveProviderSkillsForCwd(partial, "/workspace/project-a")).toEqual(
113 provider.workspaceSnapshots[0]?.skills,
114 );
115 expect(resolveProviderSlashCommandsForCwd(partial, "/workspace/project-a")).toEqual([
116 { name: "compact" },
117 ]);
118 expect(hasCompleteProviderWorkspaceSnapshot(partial, "/workspace/project-a")).toBe(false);
119 expect(hasCompleteProviderWorkspaceSnapshot(provider, "/workspace/project-a")).toBe(true);
120 expect(hasCompleteProviderWorkspaceSnapshot(provider, "/workspace/project-b")).toBe(false);
121 expect(hasCompleteProviderWorkspaceSnapshot(undefined, "/workspace/project-a")).toBe(false);
122 expect(hasCompleteProviderWorkspaceSnapshot(provider, null)).toBe(false);
123 });
124​
125 it("lists a project's own models only for that project", () => {
126 const projectModel = {
127 slug: "uni/medium-gpt",
128 name: "Medium GPT",
129 isCustom: false,
130 capabilities: null,
131 } satisfies ServerProvider["models"][number];
132 const withProjectModels = {
133 ...provider,
134 workspaceSnapshots: provider.workspaceSnapshots.map((snapshot) => ({
135 ...snapshot,
136 models: [projectModel],
137 })),
138 } satisfies ServerProvider;
139 const providers = [provider, withProjectModels];
140​
141 expect(resolveProvidersForCwd(providers, "/workspace/project-a").map((p) => p.models)).toEqual([
142 [],
143 [projectModel],
144 ]);
145 expect(resolveProvidersForCwd(providers, "/workspace/project-a")[0]).toBe(provider);
146 expect(resolveProvidersForCwd(providers, "/workspace/project-b")).toBe(providers);
147 });
148});
149​
150describe("provider workspace snapshot refresh atom", () => {
151 afterEach(() => {
152 vi.useRealTimers();
153 });
154​
155 const cwd = "/workspace/project-a";
156 const environmentId = EnvironmentId.make("env");
157 const withSnapshot = (
158 checkedAt: string,
159 slashCommandsPending?: boolean,
160 ): ReadonlyArray<ServerProvider> => [
161 {
162 ...provider,
163 workspaceSnapshots: [
164 {
165 ...provider.workspaceSnapshots[0]!,
166 checkedAt,
167 ...(slashCommandsPending ? { slashCommandsPending } : {}),
168 },
169 ],
170 },
171 ];
172 const noSnapshot: ReadonlyArray<ServerProvider> = [{ ...provider, workspaceSnapshots: [] }];
173​
174 type Response = () =>
175 | ReadonlyArray<ServerProvider>
176 | null
177 | Promise<ReadonlyArray<ServerProvider> | null>;
178​
179 function setup(initialProviders: ReadonlyArray<ServerProvider>, initialResponses?: Response[]) {
180 const providersAtom = Atom.make<ReadonlyArray<ServerProvider> | null>(initialProviders);
181 const responses: Response[] = [...(initialResponses ?? [])];
182 const run = vi.fn(async () => {
183 const providers = await (responses.shift() ?? (() => null))();
184 return providers === null
185 ? AsyncResult.failure<{ providers: ReadonlyArray<ServerProvider> }, string>(
186 Cause.fail("offline"),
187 )
188 : AsyncResult.success({ providers });
189 });
190 const family = createProviderWorkspaceSnapshotRefreshAtomFamily({
191 providers: () => providersAtom,
192 refreshProviders: { label: "refresh", run },
193 });
194 const registry = AtomRegistry.make();
195 const atom = family({ environmentId, instanceId: provider.instanceId, cwd });
196 const unmount = registry.mount(atom);
197 return { registry, atom, family, providersAtom, responses, run, unmount };
198 }
199​
200 it("scans a missing snapshot once and skips a repeat inside the TTL", async () => {
201 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
202 const { registry, atom, family, responses, run, unmount } = setup(noSnapshot);
203 // A second composer on the same target shares the scan.
204 registry.mount(family({ environmentId, instanceId: provider.instanceId, cwd }))();
205 expect(run).toHaveBeenCalledTimes(1);
206 expect(run).toHaveBeenCalledWith(expect.anything(), {
207 environmentId,
208 input: { instanceId: provider.instanceId, cwd },
209 });
210 responses.push(() => withSnapshot("2026-01-01T00:00:00.000Z"));
211 await vi.advanceTimersByTimeAsync(0);
212 registry.refresh(atom);
213 expect(run).toHaveBeenCalledTimes(1);
214 unmount();
215 });
216​
217 it.each(["already expired", "expires during discovery"])(
218 "retries an unchanged catalog that %s after the short cooldown",
219 async (expiry) => {
220 const start = Date.parse("2026-01-01T00:05:00.000Z");
221 vi.useFakeTimers({ now: expiry === "expires during discovery" ? start - 1 : start });
222 const stale = withSnapshot("2026-01-01T00:00:00.000Z");
223 const { registry, atom, responses, run, unmount } = setup(
224 expiry === "expires during discovery"
225 ? withSnapshot("2026-01-01T00:00:00.000Z", true)
226 : stale,
227 [
228 () => {
229 if (expiry === "expires during discovery") vi.setSystemTime(start + 1);
230 return stale;
231 },
232 ],
233 );
234 await vi.advanceTimersByTimeAsync(0);
235 await vi.advanceTimersByTimeAsync(9_999);
236 registry.refresh(atom);
237 expect(run).toHaveBeenCalledTimes(1);
238 responses.push(() => withSnapshot("2026-01-01T00:05:10.000Z"));
239 await vi.advanceTimersByTimeAsync(1);
240 registry.refresh(atom);
241 expect(run).toHaveBeenCalledTimes(2);
242 unmount();
243 },
244 );
245​
246 it("keeps the TTL guard for a newly scanned catalog when the client clock is ahead", async () => {
247 vi.useFakeTimers({ now: Date.parse("2026-01-01T01:00:00.000Z") });
248 const { registry, atom, run, unmount } = setup(withSnapshot("2026-01-01T00:00:00.000Z"), [
249 () => withSnapshot("2026-01-01T00:01:00.000Z"),
250 ]);
251 await vi.advanceTimersByTimeAsync(10_000);
252 registry.refresh(atom);
253 expect(run).toHaveBeenCalledTimes(1);
254 unmount();
255 });
256​
257 it("bounds retries when discovery repeatedly returns the same expired catalog", async () => {
258 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:05:00.000Z") });
259 const stale = withSnapshot("2026-01-01T00:00:00.000Z");
260 const { registry, atom, run, unmount } = setup(stale, [() => stale, () => stale, () => stale]);
261 await vi.advanceTimersByTimeAsync(0);
262 await vi.advanceTimersByTimeAsync(10_000);
263 registry.refresh(atom);
264 await vi.advanceTimersByTimeAsync(0);
265 await vi.advanceTimersByTimeAsync(10_000);
266 registry.refresh(atom);
267 await vi.advanceTimersByTimeAsync(0);
268 await vi.advanceTimersByTimeAsync(10_000);
269 registry.refresh(atom);
270 expect(run).toHaveBeenCalledTimes(3);
271 unmount();
272 });
273​
274 it("does not rescan a current snapshot, and rescans once it ages past the TTL", () => {
275 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:01:00.000Z") });
276 const { registry, atom, run, unmount } = setup(withSnapshot("2026-01-01T00:00:00.000Z"));
277 expect(run).not.toHaveBeenCalled();
278 vi.setSystemTime(Date.parse("2026-01-01T00:05:00.000Z"));
279 registry.refresh(atom);
280 expect(run).toHaveBeenCalledTimes(1);
281 unmount();
282 });
283​
284 it("cools down a failed scan, then retries on its own while the scan stays pending", async () => {
285 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
286 const { registry, atom, run, unmount } = setup(withSnapshot("2026-01-01T00:00:00.000Z", true));
287 expect(run).toHaveBeenCalledTimes(1);
288 await vi.advanceTimersByTimeAsync(0);
289 registry.refresh(atom);
290 expect(run).toHaveBeenCalledTimes(1);
291 await vi.advanceTimersByTimeAsync(9_999);
292 expect(run).toHaveBeenCalledTimes(1);
293 await vi.advanceTimersByTimeAsync(1);
294 expect(run).toHaveBeenCalledTimes(2);
295 unmount();
296 await vi.advanceTimersByTimeAsync(20_000);
297 expect(run).toHaveBeenCalledTimes(2);
298 expect(vi.getTimerCount()).toBe(0);
299 });
300​
301 it("rescans a current snapshot whose commands are complete but models are pending", () => {
302 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
303 const [current] = withSnapshot("2026-01-01T00:00:00.000Z");
304 const { run, unmount } = setup([
305 {
306 ...current!,
307 workspaceSnapshots: current!.workspaceSnapshots!.map((snapshot) => ({
308 ...snapshot,
309 modelsPending: true,
310 })),
311 },
312 ]);
313 expect(run).toHaveBeenCalledTimes(1);
314 unmount();
315 });
316​
317 it("stops retrying after three failed scans until the TTL lapses", async () => {
318 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
319 const { registry, atom, run, unmount } = setup(withSnapshot("2026-01-01T00:00:00.000Z", true));
320 await vi.advanceTimersByTimeAsync(60_000);
321 expect(run).toHaveBeenCalledTimes(3);
322 registry.refresh(atom);
323 await vi.advanceTimersByTimeAsync(0);
324 expect(run).toHaveBeenCalledTimes(3);
325 expect(vi.getTimerCount()).toBe(0);
326 vi.setSystemTime(Date.parse("2026-01-01T00:10:00.000Z"));
327 registry.refresh(atom);
328 expect(run).toHaveBeenCalledTimes(4);
329 unmount();
330 });
331​
332 it("reports skills loading while a missing snapshot is scanned, until it arrives", async () => {
333 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
334 const scanned = withSnapshot("2026-01-01T00:00:00.000Z");
335 const { registry, atom, providersAtom, unmount } = setup(noSnapshot, [() => scanned]);
336 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(true);
337 registry.set(providersAtom, scanned);
338 await vi.advanceTimersByTimeAsync(0);
339 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(false);
340 unmount();
341 });
342​
343 it("stops reporting skills loading when the scan fails", async () => {
344 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
345 const { registry, atom, unmount } = setup(noSnapshot);
346 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(true);
347 await vi.advanceTimersByTimeAsync(0);
348 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(false);
349 unmount();
350 });
351​
352 it("keeps reporting skills loading while the scan returns a pending snapshot", async () => {
353 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
354 const pending = withSnapshot("2026-01-01T00:00:00.000Z", true);
355 const { registry, atom, providersAtom, unmount } = setup(pending, [() => pending]);
356 await vi.advanceTimersByTimeAsync(0);
357 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(true);
358 registry.set(providersAtom, withSnapshot("2026-01-01T00:00:00.000Z"));
359 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(false);
360 unmount();
361 });
362​
363 it("stops reporting skills loading once a pending snapshot exhausts its retries", async () => {
364 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
365 const { registry, atom, run, unmount } = setup(withSnapshot("2026-01-01T00:00:00.000Z", true));
366 await vi.advanceTimersByTimeAsync(60_000);
367 expect(run).toHaveBeenCalledTimes(3);
368 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(false);
369 unmount();
370 });
371​
372 it("does not report skills loading for a current snapshot", () => {
373 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:01:00.000Z") });
374 const { registry, atom, unmount } = setup(withSnapshot("2026-01-01T00:00:00.000Z"));
375 expect(isProviderWorkspaceSnapshotScanning(registry.get(atom))).toBe(false);
376 unmount();
377 });
378​
379 it("does not retry on a timer when the failed scan left no pending snapshot", async () => {
380 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
381 const { registry, atom, run, unmount } = setup(noSnapshot);
382 await vi.advanceTimersByTimeAsync(10_000);
383 expect(run).toHaveBeenCalledTimes(1);
384 registry.refresh(atom);
385 expect(run).toHaveBeenCalledTimes(2);
386 unmount();
387 });
388​
389 it("rescans at once when the server drops the snapshot, lifting TTL and cooldown", async () => {
390 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
391 const { registry, providersAtom, run, unmount } = setup(noSnapshot);
392 await vi.advanceTimersByTimeAsync(0);
393 expect(run).toHaveBeenCalledTimes(1);
394 registry.set(providersAtom, withSnapshot("2026-01-01T00:00:00.000Z"));
395 expect(run).toHaveBeenCalledTimes(1);
396 registry.set(providersAtom, noSnapshot);
397 expect(run).toHaveBeenCalledTimes(2);
398 unmount();
399 });
400​
401 it("does not scan again when a scan fails after the last subscriber left", async () => {
402 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:00:00.000Z") });
403 let release = () => {};
404 const gate = new Promise<void>((resolve) => {
405 release = resolve;
406 });
407 const { run, unmount } = setup(withSnapshot("2026-01-01T00:00:00.000Z", true), [
408 async () => {
409 await gate;
410 return null;
411 },
412 ]);
413 expect(run).toHaveBeenCalledTimes(1);
414 unmount();
415 await vi.advanceTimersByTimeAsync(0);
416 release();
417 await vi.advanceTimersByTimeAsync(20_000);
418 expect(run).toHaveBeenCalledTimes(1);
419 expect(vi.getTimerCount()).toBe(0);
420 });
421​
422 it("ignores provider updates that leave the snapshot status unchanged", () => {
423 vi.useFakeTimers({ now: Date.parse("2026-01-01T00:01:00.000Z") });
424 const { registry, providersAtom, run, unmount } = setup(
425 withSnapshot("2026-01-01T00:00:00.000Z"),
426 );
427 vi.setSystemTime(Date.parse("2026-01-01T00:06:00.000Z"));
428 registry.set(providersAtom, withSnapshot("2026-01-01T00:00:00.000Z"));
429 expect(run).not.toHaveBeenCalled();
430 unmount();
431 });
432});