Skip to content
Source
Sign inSign up

npmRelease.test.ts

168 lines5.7 KB

1import * as NodeAssert from "node:assert/strict";
2​
3import { EnvironmentId, ServerConfig, ServerNpmReleaseError } from "@t3tools/contracts";
4import { expect, it } from "@effect/vitest";
5import * as Cause from "effect/Cause";
6import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
7import * as Effect from "effect/Effect";
8import * as Schema from "effect/Schema";
9import { vi } from "vite-plus/test";
10​
11import { npmReleaseAtom, npmReleaseFailureMessage } from "./npmRelease";
12import { primaryEnvironmentIdAtom } from "./primaryEnvironment";
13import { serverEnvironment } from "./server";
14​
15vi.mock("./server", () => ({
16 serverEnvironment: { npmRelease: vi.fn(), configValueAtom: vi.fn() },
17}));
18vi.mock("./presentation", () => ({
19 environmentSummaries: { connectedEnvironmentIdsAtom: Atom.make(["first", "second"]) },
20}));
21vi.mock("./primaryEnvironment", () => ({
22 primaryEnvironmentIdAtom: Atom.make<EnvironmentId | null>(null),
23}));
24​
25const version = "0.0.46-nightly.20261012.3021";
26const first = EnvironmentId.make("first");
27const second = EnvironmentId.make("second");
28const decodeServerConfig = Schema.decodeUnknownSync(ServerConfig);
29​
30const configFor = (environmentId: EnvironmentId | null, supported = true) =>
31 decodeServerConfig({
32 environment: {
33 environmentId,
34 label: environmentId,
35 platform: { os: "linux", arch: "x64" },
36 serverVersion: version,
37 capabilities: {
38 repositoryIdentity: true,
39 ...(supported ? { npmReleaseLookup: true } : {}),
40 },
41 },
42 auth: {
43 policy: "loopback-browser",
44 bootstrapMethods: [],
45 sessionMethods: [],
46 sessionCookieName: "test",
47 },
48 cwd: "/test",
49 keybindingsConfigPath: "/test/keybindings.json",
50 keybindings: [],
51 issues: [],
52 providers: [],
53 availableEditors: [],
54 observability: {
55 logsDirectoryPath: "/test/logs",
56 localTracingEnabled: false,
57 otlpTracesEnabled: false,
58 otlpMetricsEnabled: false,
59 },
60 settings: {},
61 });
62​
63function setup(supported = true) {
64 vi.mocked(serverEnvironment.configValueAtom)
65 .mockReset()
66 .mockImplementation((environmentId) => Atom.make(configFor(environmentId, supported)));
67 return vi.mocked(serverEnvironment.npmRelease).mockReset();
69​
70it.effect(
71 "resolves track metadata through the primary environment and follows primary changes",
72 () =>
73 Effect.gen(function* () {
74 const query = setup();
75 query.mockImplementation(({ environmentId }) =>
76 Atom.make(
77 AsyncResult.success({ version, databaseMigration: environmentId === first ? 42 : 43 }),
78 ),
79 );
80 const registry = AtomRegistry.make();
81 const release = npmReleaseAtom("nightly");
82 NodeAssert.ok(
83 Atom.isWritable<EnvironmentId | null, EnvironmentId | null>(primaryEnvironmentIdAtom),
84 "Expected the mocked primary environment atom to be writable.",
85 );
86 registry.set(primaryEnvironmentIdAtom, first);
87 const unmount = registry.mount(release);
88 try {
89 expect(
90 yield* AtomRegistry.getResult(registry, release, { suspendOnWaiting: true }),
91 ).toEqual({
92 version,
93 databaseMigration: 42,
94 });
95 registry.set(primaryEnvironmentIdAtom, second);
96 expect(
97 yield* AtomRegistry.getResult(registry, release, { suspendOnWaiting: true }),
98 ).toEqual({
99 version,
100 databaseMigration: 43,
101 });
102 } finally {
103 unmount();
104 registry.dispose();
105 }
106 }),
107);
108​
109it.effect("resolves track metadata through a supporting server when there is no primary", () =>
110 Effect.gen(function* () {
111 const query = setup();
112 vi.mocked(serverEnvironment.configValueAtom).mockImplementation((environmentId) =>
113 Atom.make(
114 environmentId === first ? configFor(environmentId, false) : configFor(environmentId),
115 ),
116 );
117 query.mockReturnValue(Atom.make(AsyncResult.success({ version })));
118 const registry = AtomRegistry.make();
119 const release = npmReleaseAtom("nightly");
120 const unmount = registry.mount(release);
121 try {
122 expect(yield* AtomRegistry.getResult(registry, release, { suspendOnWaiting: true })).toEqual({
123 version,
124 databaseMigration: undefined,
125 });
126 expect(query).toHaveBeenCalledWith({ environmentId: second, input: { spec: "nightly" } });
127 } finally {
128 unmount();
129 registry.dispose();
130 }
131 }),
132);
133​
134it.effect("does not call an unsupported RPC or fetch public npm for an older primary server", () =>
135 Effect.gen(function* () {
136 const query = setup(false);
137 using fetch = vi
138 .spyOn(globalThis, "fetch")
139 .mockRejectedValue(new Error("Unexpected browser fetch"));
140 const registry = AtomRegistry.make();
141 NodeAssert.ok(
142 Atom.isWritable<EnvironmentId | null, EnvironmentId | null>(primaryEnvironmentIdAtom),
143 "Expected the mocked primary environment atom to be writable.",
144 );
145 registry.set(primaryEnvironmentIdAtom, first);
146 const release = npmReleaseAtom("latest");
147 const unmount = registry.mount(release);
148 try {
149 const error = yield* AtomRegistry.getResult(registry, release).pipe(Effect.flip);
150 expect(npmReleaseFailureMessage(Cause.fail(error))).toContain(
151 "Update it before switching tracks",
152 );
153 expect(query).not.toHaveBeenCalled();
154 expect(fetch).not.toHaveBeenCalled();
155 } finally {
156 unmount();
157 registry.dispose();
158 }
159 }),
160);
161​
162it("only displays the safe release error, not arbitrary transport diagnostics", () => {
163 const error = new ServerNpmReleaseError({ reason: "Install npm on the server's PATH." });
164 expect(npmReleaseFailureMessage(Cause.fail(error))).toBe(error.message);
165 expect(
166 npmReleaseFailureMessage(Cause.fail(new Error("sensitive transport details"))),
167 ).not.toContain("sensitive");
168});