Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2175,7 +2175,10 @@ const layerWsRpc = (
Effect.gen(function* () {
yield* instance.invalidateCaches ?? Effect.void;
const maintenance = yield* instance.snapshot.resolveMaintenance({
fresh: true,
fresh:
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
input.instanceId !== undefined &&
input.cwd !== undefined &&
input.fresh !== true,
});
if (maintenance.packageName)
yield* providerLatestVersions.invalidate(maintenance.packageName);
Expand Down
55 changes: 55 additions & 0 deletions packages/provider-core/src/server/managedProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as PubSub from "effect/PubSub";
import * as Queue from "effect/Queue";
import * as Ref from "effect/Ref";
import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";

import * as ProviderHost from "./ProviderHost.ts";
import { makeManagedServerProvider } from "./managedProvider.ts";
import {
createProviderVersionAdvisory,
makeCachedProviderMaintenanceResolution,
} from "./maintenanceResolver.ts";

const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] });
const fastModeCapabilities = createModelCapabilities({
Expand Down Expand Up @@ -140,6 +145,56 @@ const enrichedSnapshotSecond: ServerProvider = {
};

describe("makeManagedServerProvider", () => {
it.effect(
"rereads installer metadata before explicit refresh while background checks stay cached",
() =>
Effect.scoped(
Effect.gen(function* () {
const latestVersion = yield* Ref.make("1.0.0");
const published = yield* Queue.unbounded<ServerProvider>();
const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
Ref.get(latestVersion).pipe(
Effect.map((version) => ({ ...maintenanceCapabilities, latestVersion: version })),
),
);
const provider = yield* makeManagedServerProvider<TestSettings>({
resolveMaintenance,
getSettings: Effect.succeed({ enabled: true }),
streamSettings: Stream.empty,
haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
initialSnapshot: () => Effect.succeed(initialSnapshot),
checkProvider: Effect.gen(function* () {
const capabilities = yield* resolveMaintenance();
return {
...refreshedSnapshot,
versionAdvisory: createProviderVersionAdvisory({
driver: refreshedSnapshot.driver,
currentVersion: refreshedSnapshot.version,
latestVersion: capabilities.latestVersion ?? null,
maintenanceCapabilities: capabilities,
}),
};
}),
enrichSnapshot: ({ snapshot }) => Queue.offer(published, snapshot).pipe(Effect.asVoid),
refreshInterval: "1 minute",
});
assert.strictEqual((yield* Queue.take(published)).versionAdvisory?.status, "current");

yield* Ref.set(latestVersion, "2.0.0");
yield* TestClock.adjust("1 minute");
assert.strictEqual(
(yield* Queue.take(published)).versionAdvisory?.latestVersion,
"1.0.0",
);

const refreshed = yield* provider.refresh;
assert.strictEqual(refreshed.versionAdvisory?.latestVersion, "2.0.0");
assert.strictEqual(refreshed.versionAdvisory?.status, "behind_latest");
assert.deepStrictEqual(yield* provider.getSnapshot, refreshed);
}),
).pipe(Effect.provide(Layer.mergeAll(layerAlwaysRunTest, TestClock.layer()))),
);

it.effect(
"runs the initial provider check in the background and streams the refreshed snapshot",
() =>
Expand Down
4 changes: 3 additions & 1 deletion packages/provider-core/src/server/managedProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,9 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
return {
resolveMaintenance: input.resolveMaintenance,
getSnapshot: Ref.get(snapshotStateRef).pipe(Effect.map((state) => state.snapshot)),
refresh: refreshSnapshot().pipe(Effect.tapError(Effect.logError), Effect.orDie),
refresh: input
.resolveMaintenance({ fresh: true })
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
.pipe(Effect.andThen(refreshSnapshot), Effect.tapError(Effect.logError), Effect.orDie),
applyUsageLimits,
get streamChanges() {
return Stream.fromPubSub(changesPubSub);
Expand Down
13 changes: 11 additions & 2 deletions packages/provider-muse/src/server/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,16 @@ export const MuseDriver: ProviderDriver<MuseSettings, MuseDriverEnv> = {
return latestVersion !== undefined ? { ...capabilities, latestVersion } : capabilities;
});
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<MuseSettings>>({
resolveMaintenance,
// Status refresh respects update checks; explicit updates use the resolver below.
resolveMaintenance: (options) =>
host.settings.get.pipe(
Effect.orDie,
Effect.flatMap((settings) =>
settings.enableProviderUpdateChecks
? resolveMaintenance(options)
: resolveInstallation(options),
),
),
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
Expand Down Expand Up @@ -176,7 +185,7 @@ export const MuseDriver: ProviderDriver<MuseSettings, MuseDriverEnv> = {
displayName,
accentColor,
enabled,
snapshot,
snapshot: { ...snapshot, resolveMaintenance },
orchestrationAdapter,
textGeneration,
} satisfies ProviderInstance;
Expand Down
97 changes: 96 additions & 1 deletion packages/provider-muse/src/server/maintenance.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
import {
DEFAULT_SERVER_SETTINGS,
ProviderDriverKind,
ProviderInstanceId,
type ServerProvider,
} from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Stream from "effect/Stream";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientResponse from "effect/http/HttpClientResponse";

Expand All @@ -15,6 +24,11 @@ import {
} from "./maintenance.ts";
import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
import { MuseDriver } from "./driver.ts";

const maintenanceCapabilities = {
...makeManualOnlyProviderMaintenanceCapabilities({
Expand All @@ -38,6 +52,87 @@ const snapshot: ServerProvider = {
};

it.layer(NodeServices.layer)("Muse maintenance", (it) => {
it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")(
"refreshes installation metadata without release requests when update checks are disabled",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const directory = yield* fs.makeTempDirectoryScoped({ prefix: "muse-refresh-" });
const binaryPath = `${directory}/muse`;
// A failing version probe keeps the installed version without starting an SDK host.
const source = "#!/bin/sh\nprintf 'muse 1.0.3-R2198.1\\n'\nexit 1\n";
yield* fs.writeFileString(binaryPath, source);
yield* fs.chmod(binaryPath, 0o755);
const requests: string[] = [];
yield* Effect.gen(function* () {
const { snapshot: provider } = yield* MuseDriver.create({
instanceId: ProviderInstanceId.make("muse-refresh"),
displayName: "Muse refresh",
enabled: true,
environment: [{ name: "MUSE_CHANNEL", value: "muse-stable", sensitive: false }],
config: { ...MuseDriver.defaultConfig(), binaryPath },
});
const enriched = provider.streamChanges.pipe(
Stream.filter((current) => current.versionAdvisory !== undefined),
Stream.take(1),
Stream.runCollect,
Effect.map((snapshots) => Array.from(snapshots)[0]!),
);
const current = yield* provider.getSnapshot;
const initial = current.versionAdvisory ? current : yield* enriched;
expect(initial.version).toBe("1.0.3-R2198.1");
expect(initial.versionAdvisory?.canUpdate).toBe(false);
expect(requests).toEqual([]);

yield* fs.writeFileString(
binaryPath,
`${source}# muse-code/launcher-2 MUSE_SYNC_UPDATE\n`,
);
const refreshed = yield* enriched.pipe(Effect.forkChild);
yield* Effect.yieldNow;
yield* provider.refresh;
const latest = yield* Fiber.join(refreshed);
expect(requests).toEqual([]);
expect(latest.versionAdvisory).toMatchObject({
status: "unknown",
latestVersion: null,
canUpdate: true,
});
expect((yield* provider.resolveMaintenance()).update?.executable).toBe(binaryPath);

// Explicit update verification must still fetch its target despite the preference.
const verification = yield* provider.resolveMaintenance({ fresh: true });
expect(verification.latestVersion).toBe("1.1.1-R10.1");
expect(requests).toEqual(["https://api.meta.ai/muse-code/channels/muse-stable"]);
}).pipe(
Effect.provide(
Layer.mergeAll(
TestProviderHost.layer({
runBackgroundWork: false,
settings: { ...DEFAULT_SERVER_SETTINGS, enableProviderUpdateChecks: false },
}),
IdAllocator.layer,
McpProviderSessions.layer,
ProviderLatestVersions.layer,
Layer.succeed(
ProviderEventLoggers.ProviderEventLoggers,
ProviderEventLoggers.NoOpProviderEventLoggers,
),
),
),
Effect.provideService(
HttpClient.HttpClient,
HttpClient.make((request) => {
requests.push(request.url);
return Effect.succeed(
HttpClientResponse.fromWeb(request, Response.json({ version: "1.1.1-R10.1" })),
);
}),
),
);
}).pipe(Effect.scoped),
);

it.effect(
"refreshes a cached channel failure when an explicit update requests verification",
() =>
Expand Down
Loading