Skip to content
Source
Sign inSign up

theme.test.ts

210 lines8.3 KB

1// @effect-diagnostics nodeBuiltinImport:off - CLI integration exercises the filesystem boundary.
2import * as NodeFS from "node:fs";
3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";
5​
6import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
7import * as NodeServices from "@effect/platform-node/NodeServices";
8import * as NetService from "@t3tools/shared/Net";
9import { assert, describe, it } from "@effect/vitest";
10import * as ConfigProvider from "effect/ConfigProvider";
11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
13import * as TestConsole from "effect/testing/TestConsole";
14import { Command } from "effect/cli";
15​
16import { cli } from "../binCli.ts";
17import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
18import * as HostProcess from "@t3tools/shared/HostProcess";
19​
20// Windows chmod cannot force these directory or file permission failures.
21const windowsHost = HostProcess.Platform.defaultValue() === "win32";
22​
23const runCli = (args: ReadonlyArray<string>) =>
24 Command.runWith(cli, { version: "0.0.0" })(args).pipe(
25 Effect.provide(
26 Layer.mergeAll(NodeServices.layer, NodeCrypto.layer, NetService.layer, TestConsole.layer),
27 ),
28 );
29​
30const makeBaseDir = () => NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-theme-cli-"));
31​
32const settingsPathFor = (baseDir: string) => NodePath.join(baseDir, "userdata", "settings.json");
33​
34const NIGHTFALL_THEME_JSON = `${JSON.stringify({
35 name: "Nightfall",
36 appearance: "dark",
37 canvas: "#1a1b26",
38 accent: "#7aa2f7",
39})}\n`;
40const JUNK_THEME_JSON = `${JSON.stringify({ name: "Junk" })}\n`;
41​
42const readSettings = (baseDir: string): Record<string, unknown> => {
43 const raw = NodeFS.readFileSync(settingsPathFor(baseDir), "utf8");
44 return JSON.parse(raw) as Record<string, unknown>;
45};
46​
47const writeSettings = (baseDir: string, settings: Record<string, unknown>) => {
48 NodeFS.mkdirSync(NodePath.dirname(settingsPathFor(baseDir)), { recursive: true });
49 NodeFS.writeFileSync(settingsPathFor(baseDir), `${JSON.stringify(settings, null, 2)}\n`);
50};
51​
52describe("t3 theme", () => {
53 it.effect("preserves settings it does not recognise", () =>
54 Effect.gen(function* () {
55 const baseDir = makeBaseDir();
56 writeSettings(baseDir, {
57 enableProviderUpdateChecks: false,
58 somethingFromANewerBuild: { nested: true },
59 });
60​
61 yield* runCli(["theme", "set", "ocean", "--base-dir", baseDir]);
62​
63 const settings = readSettings(baseDir);
64 assert.equal(settings.defaultTheme, "ocean");
65 assert.equal(settings.enableProviderUpdateChecks, false);
66 assert.deepEqual(settings.somethingFromANewerBuild, { nested: true });
67 }),
68 );
69​
70 it.effect("clears the default back to leaving fresh clients alone", () =>
71 Effect.gen(function* () {
72 const baseDir = makeBaseDir();
73 writeSettings(baseDir, { enableProviderUpdateChecks: false });
74​
75 yield* runCli(["theme", "set", "ocean", "--base-dir", baseDir]);
76 yield* runCli(["theme", "clear", "--base-dir", baseDir]);
77​
78 const settings = readSettings(baseDir);
79 assert.equal(Object.hasOwn(settings, "defaultTheme"), false);
80 assert.equal(settings.enableProviderUpdateChecks, false);
81 }),
82 );
83​
84 it.effect("sets the theme of a dev server running in the default home", () =>
85 Effect.gen(function* () {
86 const home = makeBaseDir();
87 const devStateDir = NodePath.join(home, ".t3", "dev");
88 NodeFS.mkdirSync(devStateDir, { recursive: true });
89 NodeFS.writeFileSync(
90 NodePath.join(devStateDir, "server-runtime.json"),
91 `${JSON.stringify({
92 version: 1,
93 pid: process.pid,
94 port: 4888,
95 origin: "http://127.0.0.1:4888",
96 devUrl: "http://127.0.0.1:5999",
97 startedAt: "1970-01-01T00:00:00.000Z",
98 })}\n`,
99 );
100​
101 const result = yield* runCli(["theme", "set", "ocean"]).pipe(
102 Effect.provideService(HostProcess.HomeDirectory, home),
103 Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))),
104 Effect.result,
105 );
106​
107 assert.equal(
108 NodeFS.existsSync(NodePath.join(home, ".t3", "userdata", "settings.json")),
109 false,
110 );
111 if (result._tag === "Success") {
112 const devSettings = NodePath.join(devStateDir, "settings.json");
113 assert.equal(JSON.parse(NodeFS.readFileSync(devSettings, "utf8")).defaultTheme, "ocean");
114 }
115 }),
116 );
117​
118 it.effect("publishes a theme file under its filename and sets it", () =>
119 Effect.gen(function* () {
120 const baseDir = makeBaseDir();
121 const themeFile = NodePath.join(baseDir, "nightfall.json");
122 NodeFS.writeFileSync(themeFile, NIGHTFALL_THEME_JSON);
123​
124 yield* runCli(["theme", "set", themeFile, "--base-dir", baseDir]);
125​
126 const published = NodePath.join(baseDir, "userdata", "themes", "nightfall.json");
127 assert.equal(NodeFS.existsSync(published), true);
128 assert.equal(readSettings(baseDir).defaultTheme, "nightfall");
129 const residue = NodeFS.readdirSync(NodePath.dirname(published)).filter(
130 (entry) => !entry.endsWith(".json"),
131 );
132 assert.deepEqual(residue, []);
133 }),
134 );
135​
136 it.effect("rejects a file that is not a theme and sets nothing", () =>
137 Effect.gen(function* () {
138 const baseDir = makeBaseDir();
139 const themeFile = NodePath.join(baseDir, "junk.json");
140 NodeFS.writeFileSync(themeFile, JUNK_THEME_JSON);
141​
142 const failure = yield* runCli(["theme", "set", themeFile, "--base-dir", baseDir]).pipe(
143 Effect.flip,
144 );
145​
146 assert.include(String(failure), "not a valid theme file");
147 assert.equal(NodeFS.existsSync(NodePath.join(baseDir, "userdata", "themes")), false);
148 assert.equal(NodeFS.existsSync(settingsPathFor(baseDir)), false);
149 }),
150 );
151​
152 it.effect.skipIf(!symlinksSupported)("never writes through a symlink at the staging path", () =>
153 Effect.gen(function* () {
154 const baseDir = makeBaseDir();
155 const themesDir = NodePath.join(baseDir, "userdata", "themes");
156 NodeFS.mkdirSync(themesDir, { recursive: true });
157 const victim = NodePath.join(baseDir, "victim.txt");
158 NodeFS.writeFileSync(victim, "precious");
159 NodeFS.symlinkSync(victim, NodePath.join(themesDir, `nightfall.json.staging-${process.pid}`));
160 const themeFile = NodePath.join(baseDir, "nightfall.json");
161 NodeFS.writeFileSync(themeFile, NIGHTFALL_THEME_JSON);
162​
163 yield* runCli(["theme", "set", themeFile, "--base-dir", baseDir]);
164​
165 assert.equal(NodeFS.readFileSync(victim, "utf8"), "precious");
166 assert.equal(readSettings(baseDir).defaultTheme, "nightfall");
167 }),
168 );
169​
170 it.effect.skipIf(windowsHost)("restores the previous theme when a re-publish fails to set", () =>
171 Effect.gen(function* () {
172 const baseDir = makeBaseDir();
173 writeSettings(baseDir, {});
174 const userdataDir = NodePath.dirname(settingsPathFor(baseDir));
175 const themesDir = NodePath.join(userdataDir, "themes");
176 NodeFS.mkdirSync(themesDir, { recursive: true });
177 const publishedPath = NodePath.join(themesDir, "nightfall.json");
178 const previous =
179 '{ "name": "Old Nightfall", "appearance": "dark", "canvas": "#000000", "accent": "#ffffff" }\n';
180 NodeFS.writeFileSync(publishedPath, previous);
181 const themeFile = NodePath.join(baseDir, "nightfall.json");
182 NodeFS.writeFileSync(themeFile, NIGHTFALL_THEME_JSON);
183​
184 NodeFS.chmodSync(userdataDir, 0o555);
185 try {
186 yield* runCli(["theme", "set", themeFile, "--base-dir", baseDir]).pipe(Effect.flip);
187 assert.equal(NodeFS.readFileSync(publishedPath, "utf8"), previous);
188 } finally {
189 NodeFS.chmodSync(userdataDir, 0o755);
190 }
191 }),
192 );
193​
194 it.effect.skipIf(windowsHost)("refuses to write when the settings file cannot be read", () =>
195 Effect.gen(function* () {
196 const baseDir = makeBaseDir();
197 writeSettings(baseDir, { enableProviderUpdateChecks: false });
198 NodeFS.chmodSync(settingsPathFor(baseDir), 0o000);
199​
200 const failure = yield* runCli(["theme", "set", "ocean", "--base-dir", baseDir]).pipe(
201 Effect.flip,
202 );
203​
204 NodeFS.chmodSync(settingsPathFor(baseDir), 0o644);
205 assert.include(String(failure), "Could not read");
206 assert.equal(readSettings(baseDir).enableProviderUpdateChecks, false);
207 assert.equal(Object.hasOwn(readSettings(baseDir), "defaultTheme"), false);
208 }),
209 );
210});