1import { describe, expect, it } from "@effect/vitest";
2import * as Schema from "effect/Schema";
3
4import {
5 AcpRegistryImportSessionInput,
6 AcpRegistryManagedBinaryUninstallInput,
7 AcpRegistryManagedBinaryUninstallResult,
8 AcpRegistryOperationError,
9 AcpRegistryPrepareResult,
10 AcpRegistryProbeResult,
11 AcpRegistrySearchAgent,
12 AcpRegistrySearchInput,
13 officialAcpRegistryIconUrlForAgentId,
14 resolveOfficialAcpRegistryIconUrl,
15} from "./acpRegistry.ts";
16
17const decodeSearchInput = Schema.decodeUnknownSync(AcpRegistrySearchInput);
18const decodeImportSessionInput = Schema.decodeUnknownSync(AcpRegistryImportSessionInput);
19const decodePrepareResult = Schema.decodeUnknownSync(AcpRegistryPrepareResult);
20const decodeSearchAgent = Schema.decodeUnknownSync(AcpRegistrySearchAgent);
21const decodeUninstallInput = Schema.decodeUnknownSync(AcpRegistryManagedBinaryUninstallInput);
22const decodeUninstallResult = Schema.decodeUnknownSync(AcpRegistryManagedBinaryUninstallResult);
23const decodeProbeResult = Schema.decodeUnknownSync(AcpRegistryProbeResult);
24const decodeOperationError = Schema.decodeUnknownSync(AcpRegistryOperationError);
25
26describe("ACP Registry contracts", () => {
27 it("accepts only official Registry icon URLs and safe agent IDs", () => {
28 expect(officialAcpRegistryIconUrlForAgentId("antigravity-acp")).toBe(
29 "https://cdn.agentclientprotocol.com/registry/v1/latest/antigravity-acp.svg",
30 );
31 expect(officialAcpRegistryIconUrlForAgentId("../antigravity-acp")).toBeNull();
32 expect(
33 resolveOfficialAcpRegistryIconUrl(
34 "https://cdn.agentclientprotocol.com/registry/v1/latest/pi-acp.svg",
35 ),
36 ).toBe("https://cdn.agentclientprotocol.com/registry/v1/latest/pi-acp.svg");
37 expect(
38 resolveOfficialAcpRegistryIconUrl(
39 "https://cdn.agentclientprotocol.com.evil/registry/v1/latest/pi-acp.svg",
40 ),
41 ).toBeNull();
42 expect(
43 resolveOfficialAcpRegistryIconUrl(
44 "https://user@cdn.agentclientprotocol.com/registry/v1/latest/pi-acp.svg",
45 ),
46 ).toBeNull();
47 });
48
49 it("decodes bounded search and prepare payloads", () => {
50 expect(decodeSearchInput({ query: " codex " })).toEqual({
51 query: "codex",
52 });
53 expect(
54 decodePrepareResult({
55 agentId: "codex-acp",
56 version: "1.2.0",
57 distribution: "npx",
58 prepared: true,
59 }),
60 ).toMatchObject({ agentId: "codex-acp", distribution: "npx", prepared: true });
61 });
62
63 it("rejects oversized queries and result metadata", () => {
64 expect(() => decodeSearchInput({ query: "x".repeat(121) })).toThrow();
65 expect(() =>
66 decodeSearchAgent({
67 id: "agent",
68 name: "Agent",
69 version: "1.0.0",
70 description: "x".repeat(1_025),
71 authors: [],
72 license: null,
73 website: null,
74 repository: null,
75 icon: null,
76 distribution: "binary",
77 integrity: "sha256",
78 }),
79 ).toThrow();
80 });
81
82 it("decodes managed binary uninstall payloads and rejects unsafe agent IDs", () => {
83 expect(decodeUninstallInput({ agentId: " example-agent " })).toEqual({
84 agentId: "example-agent",
85 });
86 expect(decodeUninstallResult({ agentId: "example-agent", removed: false })).toEqual({
87 agentId: "example-agent",
88 removed: false,
89 });
90 expect(() => decodeUninstallInput({ agentId: "../example-agent" })).toThrow();
91 });
92
93 it("decodes bounded native session import metadata", () => {
94 expect(
95 decodeImportSessionInput({
96 instanceId: "acpRegistry_example",
97 projectId: "project-1",
98 sessionId: "native-session-1",
99 title: " Native session ",
100 updatedAt: " 2026-08-23T00:00:00Z ",
101 }),
102 ).toEqual({
103 instanceId: "acpRegistry_example",
104 projectId: "project-1",
105 sessionId: "native-session-1",
106 title: "Native session",
107 updatedAt: "2026-08-23T00:00:00Z",
108 });
109 expect(() =>
110 decodeImportSessionInput({
111 instanceId: "acpRegistry_example",
112 projectId: "project-1",
113 sessionId: "native-session-1",
114 title: "x".repeat(1_025),
115 }),
116 ).toThrow();
117 });
118
119 it("decodes valid probe metadata and rejects oversized or invalid payloads", () => {
120 const authMethod = {
121 id: "oauth",
122 name: "Browser login",
123 description: null,
124 type: "agent",
125 };
126 expect(
127 decodeProbeResult({
128 instanceId: "acpRegistry_example",
129 ready: true,
130 icon: null,
131 authMethods: [authMethod],
132 models: [{ id: "default", name: "Default", description: "Agent-selected model" }],
133 currentModelId: "default",
134 configOptions: [],
135 }),
136 ).toMatchObject({ ready: true, currentModelId: "default" });
137 expect(() =>
138 decodeProbeResult({
139 instanceId: "acpRegistry_example",
140 ready: true,
141 icon: null,
142 authMethods: Array.from({ length: 33 }, () => authMethod),
143 models: [],
144 currentModelId: null,
145 }),
146 ).toThrow();
147 expect(() =>
148 decodeProbeResult({
149 instanceId: "acpRegistry_example",
150 ready: true,
151 icon: null,
152 authMethods: [{ ...authMethod, type: "passive" }],
153 models: [],
154 currentModelId: null,
155 }),
156 ).toThrow();
157 expect(() =>
158 decodeProbeResult({
159 instanceId: "acpRegistry_example",
160 ready: true,
161 icon: null,
162 authMethods: [],
163 models: Array.from({ length: 257 }, () => ({
164 id: "default",
165 name: "Default",
166 description: null,
167 })),
168 currentModelId: null,
169 }),
170 ).toThrow();
171 expect(() =>
172 decodeOperationError({
173 _tag: "AcpRegistryOperationError",
174 reason: "authentication_failed",
175 message: "Authentication required.",
176 authMethods: Array.from({ length: 33 }, () => authMethod),
177 }),
178 ).toThrow();
179 });
180});