Skip to content
Open
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
54 changes: 53 additions & 1 deletion apps/server/src/provider/ModelManifest.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { assert, describe, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { ProviderDriverKind, type ServerProviderModel } from "@t3tools/contracts";
import {
ProviderDriverKind,
ServerSettingsError,
type ServerProviderModel,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
Expand Down Expand Up @@ -119,6 +123,25 @@ const serviceLayers = (input: {
Layer.provideMerge(httpClientLayer(input.response)),
);

/**
* Settings that cannot be read at all. `getSettings` carries a typed
* `ServerSettingsError`, and the live implementation reaches disk and the
* secret store, so an unreadable secret or a corrupt settings file lands here.
*/
const unreadableSettingsLayer = Layer.effect(
ServerSettings.ServerSettingsService,
Effect.map(ServerSettings.ServerSettingsService, (base) => ({
...base,
getSettings: Effect.fail(
new ServerSettingsError({
settingsPath: "/nonexistent/settings.json",
operation: "read-secret",
cause: new Error("settings unreadable"),
}),
),
})),
).pipe(Layer.provide(ServerSettings.layerTest({})));

describe("ModelManifest service", () => {
it.live("prefers a fetched manifest over the bundle and caches it to disk", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -182,4 +205,33 @@ describe("ModelManifest service", () => {
),
),
);

it.live("does not fetch when the opt-out setting cannot be read", () =>
Effect.gen(function* () {
let fetchCount = 0;
const service = yield* make.pipe(
Effect.provide(
httpClientLayer(() => {
fetchCount += 1;
return Response.json(REMOTE_MANIFEST);
}),
),
);
assert.deepStrictEqual(yield* service.refresh, BUNDLED_MODEL_MANIFEST);
// `enableProviderUpdateChecks` is a don't-phone-home switch, so failing
// to read it has to resolve to not contacting the host.
assert.strictEqual(fetchCount, 0);
}).pipe(
Effect.scoped,
Effect.provide(
ServerConfig.layerTest(process.cwd(), {
prefix: "model-manifest-settings-unreadable-test",
}).pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(unreadableSettingsLayer),
Layer.provideMerge(httpClientLayer(() => Response.json(REMOTE_MANIFEST))),
),
),
),
);
});
6 changes: 5 additions & 1 deletion apps/server/src/provider/ModelManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,14 @@ export const make = Effect.gen(function* () {
// fetches only: a manifest already cached on disk from an earlier fetch
// stays in effect, since the setting is about phoning home, not about
// discarding data the server already holds.
//
// Unreadable settings skip the fetch too. This is a don't-phone-home
// switch, so "we could not tell whether the user opted out" has to resolve
// to not making the request.
const settings = yield* settingsService.getSettings.pipe(
Effect.catchCause(() => Effect.succeed(null)),
);
if (settings !== null && !settings.enableProviderUpdateChecks) return manifest;
if (settings === null || !settings.enableProviderUpdateChecks) return manifest;

lastAttemptMs = now;
const fetched = yield* httpClient.get(MODEL_MANIFEST_URL).pipe(
Expand Down
Loading