1import { AcpRegistryOperationError, ProviderInstanceId } from "@t3tools/contracts";
2import { AcpRegistrySettings } from "../settings.ts";
3import { describe, expect, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
7
8import * as AcpRegistrySupport from "./AcpRegistrySupport.ts";
9import {
10 acpRegistrySnapshotReadiness,
11 applyAcpRegistryAvailableCommands,
12 applyAcpRegistryLiveConfiguration,
13 buildCheckedAcpRegistrySnapshot,
14 checkAcpRegistryProviderReadiness,
15 checkAcpRegistryProviderStatus,
16} from "./driver.ts";
17
18const decodeSettings = Schema.decodeSync(AcpRegistrySettings);
19const identity = {
20 instanceId: ProviderInstanceId.make("acpRegistry_test"),
21 displayName: "Test ACP",
22 accentColor: undefined,
23 continuationKey: "acpRegistry:instance:acpRegistry_test",
24};
25const noSessionManagement = {
26 canList: false,
27 canLoad: false,
28 canResume: false,
29 canLogout: false,
30 canDelete: false,
31 canConfigureProviders: false,
32} as const;
33
34function catalogWithInspection(
35 inspection: AcpRegistrySupport.AcpRegistryInspection,
36): AcpRegistrySupport.AcpRegistryCatalog["Service"] {
37 return {
38 search: () => Effect.die("unused search"),
39 prepare: () => Effect.die("unused prepare"),
40 inspect: () => Effect.succeed(inspection),
41 resolve: () => Effect.die("unused resolve"),
42 uninstallManagedBinary: () => Effect.die("unused uninstall"),
43 };
44}
45
46describe("acpRegistrySnapshotReadiness", () => {
47 it("treats a live empty command advertisement as an authoritative replacement", () => {
48 const provider = buildCheckedAcpRegistrySnapshot({
49 ...identity,
50 settings: decodeSettings({ agentId: "test-agent" }),
51 checkedAt: "2026-08-13T10:00:00.000Z",
52 inspection: {
53 status: "ready",
54 agentId: "test-agent",
55 version: "1.0.0",
56 distribution: "npx",
57 documentationUrl: "https://example.test/agent/setup",
58 },
59 probe: {
60 probe: {
61 instanceId: identity.instanceId,
62 ready: true,
63 icon: null,
64 authMethods: [],
65 models: [],
66 currentModelId: null,
67 configOptions: [],
68 sessionManagement: noSessionManagement,
69 },
70 slashCommands: [{ name: "stale" }],
71 skills: [{ name: "stale-skill", path: "stale", enabled: true }],
72 },
73 });
74
75 const replaced = applyAcpRegistryAvailableCommands(
76 provider,
77 Option.some({ slashCommands: [], skills: [] }),
78 );
79 expect(replaced.slashCommands).toEqual([]);
80 expect(replaced.skills).toEqual([]);
81 expect(applyAcpRegistryAvailableCommands(provider, Option.none()).slashCommands).toEqual([
82 { name: "stale" },
83 ]);
84 expect(provider.iconUrl).toBe(
85 "https://cdn.agentclientprotocol.com/registry/v1/latest/test-agent.svg",
86 );
87 expect(provider.setup).toMatchObject({
88 canAuthenticate: false,
89 documentationUrl: "https://example.test/agent/setup",
90 });
91 });
92
93 it("keeps authentication unknown when an agent permits a discovery session before sign-in", () => {
94 const snapshot = buildCheckedAcpRegistrySnapshot({
95 ...identity,
96 settings: decodeSettings({ agentId: "test-agent" }),
97 checkedAt: "2026-08-13T10:00:00.000Z",
98 inspection: {
99 status: "ready",
100 agentId: "test-agent",
101 version: "1.0.0",
102 distribution: "binary",
103 },
104 probe: {
105 probe: {
106 instanceId: identity.instanceId,
107 ready: true,
108 icon: null,
109 authMethods: [{ id: "browser", name: "Browser", description: null, type: "agent" }],
110 models: [],
111 currentModelId: null,
112 configOptions: [],
113 sessionManagement: noSessionManagement,
114 },
115 slashCommands: [],
116 skills: [],
117 },
118 });
119 expect(snapshot.auth.status).toBe("unknown");
120 expect(snapshot.setup?.canAuthenticate).toBe(true);
121 expect(
122 applyAcpRegistryLiveConfiguration(
123 snapshot,
124 { models: [], currentModelId: null, configOptions: [] },
125 [],
126 ).auth.status,
127 ).toBe("unknown");
128 });
129
130 it("overlays live configuration without dropping probe-owned session capabilities", () => {
131 const provider = buildCheckedAcpRegistrySnapshot({
132 ...identity,
133 settings: decodeSettings({ agentId: "test-agent" }),
134 checkedAt: "2026-08-13T10:00:00.000Z",
135 inspection: {
136 status: "ready",
137 agentId: "test-agent",
138 version: "1.0.0",
139 distribution: "npx",
140 },
141 probe: {
142 probe: {
143 instanceId: identity.instanceId,
144 ready: true,
145 icon: null,
146 authMethods: [],
147 models: [{ id: "probe-model", name: "Probe model", description: null }],
148 currentModelId: "probe-model",
149 configOptions: [],
150 sessionManagement: {
151 canList: true,
152 canLoad: true,
153 canResume: true,
154 canLogout: true,
155 canDelete: true,
156 canConfigureProviders: true,
157 },
158 },
159 slashCommands: [],
160 skills: [],
161 },
162 });
163
164 expect(
165 applyAcpRegistryLiveConfiguration(
166 provider,
167 {
168 models: [{ id: "live-model", name: "Live model", description: null }],
169 currentModelId: "live-model",
170 configOptions: [],
171 },
172 [],
173 ),
174 ).toMatchObject({
175 auth: { status: "unknown", canLogout: true },
176 nativeSessions: { canList: true, canLoad: true, canResume: true },
177 models: [{ slug: "live-model", isDefault: true }],
178 });
179 });
180
181 it("maps registry inspection status to provider readiness", () => {
182 expect(
183 acpRegistrySnapshotReadiness({
184 status: "ready",
185 agentId: "gemini-cli",
186 version: "1.2.3",
187 distribution: "npx",
188 }),
189 ).toEqual({ installed: true, version: "1.2.3", status: "ready" });
190
191 expect(
192 acpRegistrySnapshotReadiness({
193 status: "missing_runner",
194 agentId: "gemini-cli",
195 version: "1.2.3",
196 distribution: "npx",
197 runner: "npx",
198 }),
199 ).toMatchObject({ installed: false, version: "1.2.3", status: "error" });
200
201 expect(
202 acpRegistrySnapshotReadiness({
203 status: "missing_runner",
204 version: null,
205 distribution: "local",
206 }),
207 ).toEqual({
208 installed: false,
209 version: null,
210 status: "error",
211 message: "Local ACP executable is not available on this environment's PATH.",
212 });
213
214 expect(
215 acpRegistrySnapshotReadiness({
216 status: "unprepared",
217 agentId: "zed-agent",
218 version: "2.0.0",
219 distribution: "binary",
220 }),
221 ).toMatchObject({ installed: false, version: "2.0.0", status: "warning" });
222
223 expect(
224 acpRegistrySnapshotReadiness({ status: "failed", message: "Registry unavailable." }),
225 ).toEqual({
226 installed: false,
227 version: null,
228 status: "error",
229 message: "Registry unavailable.",
230 });
231 });
232
233 it("projects discovery without claiming authentication when no login methods are advertised", () => {
234 const snapshot = buildCheckedAcpRegistrySnapshot({
235 ...identity,
236 settings: decodeSettings({
237 agentId: "test-agent",
238 customModels: [" custom-model ", "gpt-discovered"],
239 }),
240 checkedAt: "2026-08-13T10:00:00.000Z",
241 inspection: {
242 status: "ready",
243 agentId: "test-agent",
244 version: "1.0.0",
245 distribution: "npx",
246 },
247 probe: {
248 probe: {
249 instanceId: identity.instanceId,
250 ready: true,
251 icon: null,
252 authMethods: [],
253 models: [{ id: "gpt-discovered", name: "GPT Discovered", description: null }],
254 currentModelId: "gpt-discovered",
255 configOptions: [],
256 sessionManagement: noSessionManagement,
257 },
258 slashCommands: [{ name: "plan", description: "Create a plan", input: { hint: "topic" } }],
259 skills: [{ name: "workspace-skill", path: "acp://skill/workspace-skill", enabled: true }],
260 },
261 });
262
263 expect(snapshot.auth).toEqual({ status: "unknown", canLogout: false });
264 expect(snapshot.supportsTextGeneration).toBe(false);
265 expect(
266 snapshot.models.map(({ slug, name, isCustom, isDefault }) => ({
267 slug,
268 name,
269 isCustom,
270 isDefault,
271 })),
272 ).toEqual([
273 {
274 slug: "gpt-discovered",
275 name: "GPT Discovered",
276 isCustom: false,
277 isDefault: true,
278 },
279 {
280 slug: "custom-model",
281 name: "custom-model",
282 isCustom: true,
283 isDefault: undefined,
284 },
285 ]);
286 expect(snapshot.slashCommands).toEqual([
287 { name: "plan", description: "Create a plan", input: { hint: "topic" } },
288 ]);
289 expect(snapshot.skills).toEqual([
290 { name: "workspace-skill", path: "acp://skill/workspace-skill", enabled: true },
291 ]);
292 });
293
294 it("keeps a default model only when the agent advertises none", () => {
295 const snapshot = buildCheckedAcpRegistrySnapshot({
296 ...identity,
297 settings: decodeSettings({ agentId: "test-agent" }),
298 checkedAt: "2026-08-13T10:00:00.000Z",
299 inspection: {
300 status: "ready",
301 agentId: "test-agent",
302 version: "1.0.0",
303 distribution: "uvx",
304 },
305 probe: {
306 probe: {
307 instanceId: identity.instanceId,
308 ready: true,
309 icon: null,
310 authMethods: [],
311 models: [],
312 currentModelId: null,
313 configOptions: [],
314 sessionManagement: noSessionManagement,
315 },
316 slashCommands: [],
317 skills: [],
318 },
319 });
320
321 expect(snapshot.models.map((model) => model.slug)).toEqual(["default"]);
322 expect(snapshot.models[0]?.isDefault).toBe(true);
323 });
324
325 it.each(["registry", "local"] as const)(
326 "reports failed authentication after successful %s inspection",
327 (source) => {
328 const snapshot = buildCheckedAcpRegistrySnapshot({
329 ...identity,
330 settings: decodeSettings({
331 source,
332 ...(source === "local" ? { commandPath: "test-agent" } : { agentId: "test-agent" }),
333 authMethodId: "grok-login",
334 }),
335 checkedAt: "2026-08-13T10:00:00.000Z",
336 inspection: {
337 status: "ready",
338 agentId: "test-agent",
339 version: source === "local" ? null : "1.0.0",
340 distribution: source === "local" ? "local" : "binary",
341 },
342 probeError: new AcpRegistryOperationError({
343 reason: "authentication_failed",
344 message: "Login required.",
345 authMethods: [
346 {
347 id: "api-key",
348 name: "API key",
349 description: null,
350 type: "env_var",
351 },
352 {
353 id: "grok-login",
354 name: "Log in with Grok",
355 description: null,
356 type: "agent",
357 },
358 ],
359 }),
360 });
361
362 expect(snapshot).toMatchObject({
363 installed: true,
364 version: source === "local" ? null : "1.0.0",
365 status: "warning",
366 setup: { canAuthenticate: true },
367 auth: {
368 status: "unauthenticated",
369 type: "agent",
370 label: "Log in with Grok",
371 },
372 message: 'Sign in in provider settings using "Log in with Grok".',
373 });
374 },
375 );
376
377 it.effect("runs the disposable probe only after local inspection is ready", () =>
378 Effect.gen(function* () {
379 const settings = decodeSettings({ agentId: "test-agent" });
380 const environment = { PATH: "/provider/bin" };
381 let receivedEnvironment: NodeJS.ProcessEnv | undefined;
382 const snapshot = yield* checkAcpRegistryProviderStatus(
383 {
384 ...identity,
385 settings,
386 cwd: "/workspace",
387 environment,
388 },
389 (input) =>
390 Effect.sync(() => {
391 receivedEnvironment = input.environment;
392 return {
393 probe: {
394 instanceId: identity.instanceId,
395 ready: true as const,
396 icon: null,
397 authMethods: [],
398 models: [{ id: "agent-model", name: "Agent Model", description: null }],
399 currentModelId: "agent-model",
400 configOptions: [],
401 sessionManagement: noSessionManagement,
402 },
403 slashCommands: [{ name: "review" }],
404 skills: [],
405 };
406 }),
407 ).pipe(
408 Effect.provideService(
409 AcpRegistrySupport.AcpRegistryCatalog,
410 catalogWithInspection({
411 status: "ready",
412 agentId: "test-agent",
413 version: "1.0.0",
414 distribution: "npx",
415 }),
416 ),
417 );
418
419 expect(receivedEnvironment).toBe(environment);
420 expect(snapshot).toMatchObject({
421 auth: { status: "unknown" },
422 models: [{ slug: "agent-model" }],
423 slashCommands: [{ name: "review" }],
424 skills: [],
425 });
426 }),
427 );
428
429 it.effect("publishes concrete local readiness before background ACP discovery", () =>
430 Effect.gen(function* () {
431 const snapshot = yield* checkAcpRegistryProviderReadiness({
432 ...identity,
433 settings: decodeSettings({ agentId: "test-agent" }),
434 environment: { PATH: "/provider/bin" },
435 }).pipe(
436 Effect.provideService(
437 AcpRegistrySupport.AcpRegistryCatalog,
438 catalogWithInspection({
439 status: "ready",
440 agentId: "test-agent",
441 version: "1.0.0",
442 distribution: "npx",
443 }),
444 ),
445 );
446
447 expect(snapshot).toMatchObject({
448 installed: true,
449 status: "ready",
450 version: "1.0.0",
451 auth: { status: "unknown" },
452 message: "Checking ACP authentication, models, and commands in the background...",
453 });
454 }),
455 );
456});