1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";
3import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as HttpClient from "effect/http/HttpClient";
7import * as HttpClientResponse from "effect/http/HttpClientResponse";
8
9import {
10 compareMuseVersions,
11 enrichMuseSnapshot,
12 museMaintenance,
13 latestMuseVersion,
14 parseMuseVersion,
15} from "./maintenance.ts";
16import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
17import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver";
18
19const maintenanceCapabilities = {
20 ...makeManualOnlyProviderMaintenanceCapabilities({
21 provider: ProviderDriverKind.make("muse"),
22 packageName: null,
23 }),
24 compareVersions: compareMuseVersions,
25};
26const snapshot: ServerProvider = {
27 instanceId: ProviderInstanceId.make("muse"),
28 driver: ProviderDriverKind.make("muse"),
29 enabled: true,
30 installed: true,
31 version: "1.0.3",
32 status: "ready",
33 auth: { status: "unknown" },
34 checkedAt: "2026-09-10T00:00:00.000Z",
35 models: [],
36 skills: [],
37 slashCommands: [],
38};
39
40it.layer(NodeServices.layer)("Muse maintenance", (it) => {
41 it.effect(
42 "refreshes a cached channel failure when an explicit update requests verification",
43 () =>
44 Effect.gen(function* () {
45 let requests = 0;
46 const check = Effect.gen(function* () {
47 expect(yield* latestMuseVersion({})).toBeNull();
48 expect(yield* latestMuseVersion({})).toBeNull();
49 expect(yield* latestMuseVersion({}, { fresh: true })).toBe("1.1.1-R10.1");
50 });
51 yield* check.pipe(
52 Effect.provide(ProviderLatestVersions.layer),
53 Effect.provideService(
54 HttpClient.HttpClient,
55 HttpClient.make((request) => {
56 requests += 1;
57 return Effect.succeed(
58 HttpClientResponse.fromWeb(
59 request,
60 requests === 1
61 ? new Response("unavailable", { status: 503 })
62 : Response.json({ version: "1.1.1-R10.1" }),
63 ),
64 );
65 }),
66 ),
67 );
68 expect(requests).toBe(2);
69 }),
70 );
71 it.effect(
72 "compares numeric native release revisions and preserves the CLI release identity",
73 () =>
74 Effect.sync(() => {
75 expect(parseMuseVersion("Muse Code 1.1.1 (1.1.1-R2514.1)")).toBe("1.1.1-R2514.1");
76 expect(compareMuseVersions("1.1.1-R9.1", "1.1.1-R10.1")).toBeLessThan(0);
77 expect(compareMuseVersions("1.1.1-R2514.1", "1.1.1-R2514.2")).toBeLessThan(0);
78 expect(compareMuseVersions("1.2.0-R1.1", "1.1.1-R2514.2")).toBeGreaterThan(0);
79 // A CLI that prints no revision compares by release, never as an older prerelease.
80 expect(compareMuseVersions("1.4.3", "1.4.3-R5018.1")).toBe(0);
81 expect(compareMuseVersions("1.4.2", "1.4.3-R5018.1")).toBeLessThan(0);
82 }),
83 );
84 it.effect(
85 "finds releases from the instance's channel and caches the native version, not the SDK version",
86 () =>
87 Effect.gen(function* () {
88 const requests: string[] = [];
89 const httpClient = HttpClient.make((request) => {
90 requests.push(request.url);
91 return Effect.succeed(
92 HttpClientResponse.fromWeb(request, Response.json({ version: "1.1.1-R2514.1" })),
93 );
94 });
95 const check = enrichMuseSnapshot({
96 snapshot,
97 maintenanceCapabilities,
98 enableProviderUpdateChecks: true,
99 environment: { MUSE_CHANNEL: "muse-canary" },
100 });
101 const [first, second] = yield* Effect.all([check, check], { concurrency: 1 }).pipe(
102 Effect.provideService(HttpClient.HttpClient, httpClient),
103 Effect.provide(ProviderLatestVersions.layer),
104 );
105 expect(first.versionAdvisory).toMatchObject({
106 status: "behind_latest",
107 currentVersion: "1.0.3",
108 latestVersion: "1.1.1-R2514.1",
109 canUpdate: false,
110 });
111 expect(second.versionAdvisory?.latestVersion).toBe("1.1.1-R2514.1");
112 expect(requests).toEqual(["https://api.meta.ai/muse-code/channels/muse-canary"]);
113 }),
114 );
115
116 it.effect("reports native release R9 behind R10 and R10 current against R9", () =>
117 Effect.gen(function* () {
118 for (const [current, latest, status] of [
119 ["1.4.2-R9.1", "1.4.2-R10.1", "behind_latest"],
120 ["1.4.2-R10.1", "1.4.2-R9.1", "current"],
121 ] as const) {
122 const result = yield* enrichMuseSnapshot({
123 snapshot: { ...snapshot, version: current },
124 maintenanceCapabilities,
125 enableProviderUpdateChecks: true,
126 environment: {},
127 }).pipe(
128 Effect.provideService(
129 HttpClient.HttpClient,
130 HttpClient.make((request) =>
131 Effect.succeed(
132 HttpClientResponse.fromWeb(request, Response.json({ version: latest })),
133 ),
134 ),
135 ),
136 Effect.provide(ProviderLatestVersions.layer),
137 );
138 expect(result.versionAdvisory?.status).toBe(status);
139 }
140 }),
141 );
142
143 it.effect("skips network checks when disabled, uninstalled or disabled in settings", () =>
144 Effect.gen(function* () {
145 for (const input of [
146 { snapshot, enableProviderUpdateChecks: false },
147 { snapshot: { ...snapshot, enabled: false }, enableProviderUpdateChecks: true },
148 { snapshot: { ...snapshot, installed: false }, enableProviderUpdateChecks: true },
149 ]) {
150 const result = yield* enrichMuseSnapshot({
151 ...input,
152 maintenanceCapabilities,
153 environment: {},
154 }).pipe(
155 Effect.provide(ProviderLatestVersions.layer),
156 Effect.provideService(
157 HttpClient.HttpClient,
158 HttpClient.make(() => Effect.die("Unexpected release request")),
159 ),
160 );
161 expect(result.versionAdvisory?.status).toBe("unknown");
162 }
163 }),
164 );
165
166 it.effect("tolerates invalid release manifests and unavailable channels", () =>
167 Effect.gen(function* () {
168 for (const response of [
169 Response.json({ version: 12 }),
170 Response.json({ version: "1.1.1" }),
171 new Response("unavailable", { status: 503 }),
172 ]) {
173 const result = yield* enrichMuseSnapshot({
174 snapshot,
175 maintenanceCapabilities,
176 enableProviderUpdateChecks: true,
177 environment: {},
178 }).pipe(
179 Effect.provideService(
180 HttpClient.HttpClient,
181 HttpClient.make((request) =>
182 Effect.succeed(HttpClientResponse.fromWeb(request, response)),
183 ),
184 ),
185 Effect.provide(ProviderLatestVersions.layer),
186 );
187 expect(result.status).toBe("ready");
188 expect(result.versionAdvisory?.status).toBe("unknown");
189 }
190 }),
191 );
192
193 it.effect("updates only a recognized launcher at the resolved instance path", () =>
194 Effect.scoped(
195 Effect.gen(function* () {
196 const fs = yield* FileSystem.FileSystem;
197 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "muse-maintenance-" });
198 const binaryPath = `${directory}/custom muse`;
199 const context = {
200 binaryPath,
201 resolvedCommandPath: binaryPath,
202 realCommandPath: binaryPath,
203 platform: "linux" as const,
204 env: { MUSE_CHANNEL: "muse-canary" },
205 };
206 yield* fs.writeFileString(
207 binaryPath,
208 "#!/bin/bash\n# muse-code/launcher-2\n# MUSE_SYNC_UPDATE\n",
209 );
210 const result = yield* museMaintenance.resolve(context);
211 expect(result.update).toMatchObject({
212 executable: binaryPath,
213 args: ["--version"],
214 lockKey: `muse:${binaryPath}`,
215 env: {
216 MUSE_CHANNEL: "muse-canary",
217 MUSE_NO_AUTO_UPDATE: "0",
218 MUSE_SYNC_UPDATE: "1",
219 MUSE_UPDATE_INTERVAL_SECONDS: "0",
220 },
221 });
222 expect(result.update?.command).toBe(
223 `MUSE_CHANNEL=muse-canary MUSE_NO_AUTO_UPDATE=0 MUSE_SYNC_UPDATE=1 MUSE_UPDATE_INTERVAL_SECONDS=0 '${binaryPath}' --version`,
224 );
225 expect(result.compareVersions).toBe(compareMuseVersions);
226 yield* fs.writeFileString(binaryPath, "standalone binary");
227 expect((yield* museMaintenance.resolve(context)).update).toBeNull();
228 expect((yield* museMaintenance.resolve(null)).update).toBeNull();
229 }),
230 ),
231 );
232});