1import { AuthOrchestrationOperateScope, ServerProvider } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
3import { afterEach, describe, expect, it, vi } from "vite-plus/test";
4
5import {
6 canMaintainEnvironment,
7 canUpdateEnvironmentProvider,
8 findEnvironmentUpdate,
9 supportsEnvironmentUpdate,
10} from "./environment-maintenance";
11
12const provider = Schema.decodeUnknownSync(ServerProvider)({
13 instanceId: "codex-personal",
14 driver: "codex",
15 enabled: true,
16 installed: true,
17 version: "1.0.0",
18 status: "ready",
19 auth: { status: "authenticated" },
20 checkedAt: "2026-09-23T00:00:00.000Z",
21 models: [],
22 versionAdvisory: {
23 status: "behind_latest",
24 currentVersion: "1.0.0",
25 latestVersion: "1.1.0",
26 canUpdate: true,
27 updateCommand: null,
28 checkedAt: null,
29 message: null,
30 },
31});
32
33afterEach(() => vi.unstubAllGlobals());
34
35describe("environment maintenance access", () => {
36 it("requires a connected authenticated session with operate permission", () => {
37 const session = {
38 authenticated: true,
39 auth: {
40 policy: "remote-reachable" as const,
41 bootstrapMethods: [],
42 sessionMethods: [],
43 sessionCookieName: "session",
44 },
45 scopes: [AuthOrchestrationOperateScope],
46 };
47 expect(canMaintainEnvironment(session, true)).toBe(true);
48 expect(canMaintainEnvironment(session, false)).toBe(false);
49 expect(canMaintainEnvironment({ ...session, authenticated: false }, true)).toBe(false);
50 expect(canMaintainEnvironment({ ...session, scopes: [] }, true)).toBe(false);
51 const { scopes: _, ...legacy } = session;
52 expect(canMaintainEnvironment(legacy, true)).toBe(false);
53 expect(canMaintainEnvironment(null, true)).toBe(false);
54 });
55
56 it.each([
57 { scopes: ["environment:maintain"], expected: true },
58 { scopes: ["orchestration:operate"], expected: false },
59 { scopes: ["providers:manage"], expected: false },
60 ] as const)("uses the advertised update permission for $scopes", ({ scopes, expected }) => {
61 expect(
62 canMaintainEnvironment(
63 {
64 authenticated: true,
65 auth: {
66 policy: "remote-reachable",
67 bootstrapMethods: [],
68 sessionMethods: [],
69 sessionCookieName: "session",
70 serverUpdateScope: "environment:maintain",
71 },
72 scopes,
73 },
74 true,
75 ),
76 ).toBe(expected);
77 });
78
79 it.each([
80 { permissions: ["environment:maintain"], expected: true },
81 { permissions: ["providers:manage"], expected: false },
82 { permissions: [], expected: false },
83 ] as const)(
84 "honors exact permissions over legacy scopes: $permissions",
85 ({ permissions, expected }) => {
86 expect(
87 canMaintainEnvironment(
88 {
89 authenticated: true,
90 auth: {
91 policy: "remote-reachable",
92 bootstrapMethods: [],
93 sessionMethods: [],
94 sessionCookieName: "session",
95 serverUpdateScope: "environment:maintain",
96 },
97 scopes: ["orchestration:operate"],
98 permissions,
99 },
100 true,
101 ),
102 ).toBe(expected);
103 },
104 );
105
106 it("requires remote desktop update support for desktop hosts", () => {
107 expect(supportsEnvironmentUpdate({})).toBe(false);
108 expect(supportsEnvironmentUpdate({ serverSelfUpdate: "respawn" })).toBe(true);
109 expect(supportsEnvironmentUpdate({ serverSelfUpdate: "desktop-managed" })).toBe(false);
110 expect(
111 supportsEnvironmentUpdate({ serverSelfUpdate: "desktop-managed", desktopAppUpdate: true }),
112 ).toBe(true);
113 });
114
115 it("excludes unavailable, manual, busy, and incompatible provider updates", () => {
116 expect(canUpdateEnvironmentProvider(provider)).toBe(true);
117 expect(canUpdateEnvironmentProvider({ ...provider, installed: false })).toBe(false);
118 expect(canUpdateEnvironmentProvider({ ...provider, availability: "unavailable" })).toBe(false);
119 expect(canUpdateEnvironmentProvider({ ...provider, versionAdvisory: undefined })).toBe(false);
120 for (const latestVersionStatus of ["broken", "unsupported"] as const) {
121 expect(
122 canUpdateEnvironmentProvider({
123 ...provider,
124 compatibilityAdvisory: {
125 status: "supported",
126 latestVersionStatus,
127 message: null,
128 recommendedVersion: null,
129 recommendedRange: null,
130 },
131 }),
132 ).toBe(false);
133 }
134 for (const status of ["queued", "running"] as const) {
135 expect(
136 canUpdateEnvironmentProvider({
137 ...provider,
138 updateState: {
139 status,
140 startedAt: null,
141 finishedAt: null,
142 message: null,
143 output: null,
144 },
145 }),
146 ).toBe(false);
147 }
148 expect(
149 canUpdateEnvironmentProvider({
150 ...provider,
151 versionAdvisory: {
152 ...provider.versionAdvisory!,
153 canUpdate: false,
154 },
155 }),
156 ).toBe(false);
157 });
158});
159
160describe("environment release checks", () => {
161 const signal = new AbortController().signal;
162
163 it("keeps stable hosts on stable releases and ignores drafts", async () => {
164 vi.stubGlobal(
165 "fetch",
166 vi
167 .fn()
168 .mockImplementation(async () =>
169 Response.json([
170 { tag_name: "v2.0.0-nightly.20260923.1" },
171 { tag_name: "v1.2.0", draft: true },
172 { tag_name: "v1.1.0" },
173 ]),
174 ),
175 );
176 expect(await findEnvironmentUpdate("1.0.0", signal)).toBe("1.1.0");
177 expect(await findEnvironmentUpdate("1.1.0", signal)).toBeNull();
178 expect(await findEnvironmentUpdate("1.3.0", signal)).toBeNull();
179 });
180
181 it("walks release pages to find the host's channel", async () => {
182 const fetchMock = vi
183 .fn()
184 .mockResolvedValueOnce(
185 Response.json(Array.from({ length: 100 }, () => ({ tag_name: "v2.0.0" }))),
186 )
187 .mockResolvedValueOnce(Response.json([{ tag_name: "v1.0.0-preview.20260923.2" }]));
188 vi.stubGlobal("fetch", fetchMock);
189 expect(await findEnvironmentUpdate("1.0.0-preview.20260923.1", signal)).toBe(
190 "1.0.0-preview.20260923.2",
191 );
192 expect(fetchMock.mock.calls[1]?.[0]).toContain("page=2");
193 expect(fetchMock.mock.calls[1]?.[1]).toEqual({ signal });
194 });
195
196 it("reports failed checks instead of claiming the server is current", async () => {
197 vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(null, { status: 403 })));
198 await expect(findEnvironmentUpdate("1.0.0", signal)).rejects.toThrow("403");
199 vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json([])));
200 await expect(findEnvironmentUpdate("1.0.0", signal)).rejects.toThrow("No stable release");
201 vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json({ error: "invalid" })));
202 await expect(findEnvironmentUpdate("1.0.0", signal)).rejects.toThrow();
203 });
204});