import { assert, describe, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as PlatformError from "effect/PlatformError";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as DesktopEnvironment from "./DesktopEnvironment.ts";import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts"; interface RecordedRegistration { readonly directories: string[]; readonly files: Array<{ readonly path: string; readonly content: string }>; readonly commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }>;} const makeEnvironment = (overrides: Record<string, unknown> = {}) => DesktopEnvironment.DesktopEnvironment.of({ platform: "linux", isPackaged: true, isDevelopment: false, displayName: "T3 Code (Alpha)", linuxDesktopEntryName: "com.t3tools.T3Code.desktop", linuxWmClass: "t3code", linuxApplicationsDir: "/home/alice/.local/share/applications", appImagePath: Option.some("/home/alice/Applications/T3-Code.AppImage"), path: { join: (...parts: ReadonlyArray<string>) => parts.join("/") }, ...overrides, } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]); const mockProcess = (exitCode: number) => ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)), isRunning: Effect.succeed(false), kill: () => Effect.void, unref: Effect.succeed(Effect.void), stdin: Sink.drain, stdout: Stream.empty, stderr: Stream.empty, all: Stream.empty, getInputFd: () => Sink.drain, getOutputFd: () => Stream.empty, }); const makeHandlerLayer = ( recorded: RecordedRegistration, input: { readonly environment?: Record<string, unknown>; readonly xdgMimeExitCode?: number; readonly writeError?: PlatformError.PlatformError; readonly existingEntry?: string; } = {},) => DesktopLinuxUrlHandler.layer.pipe( Layer.provide( Layer.mergeAll( Layer.succeed(DesktopEnvironment.DesktopEnvironment, makeEnvironment(input.environment)), FileSystem.layerNoop({ readFileString: () => Effect.succeed(input.existingEntry ?? ""), makeDirectory: (path) => Effect.sync(() => { recorded.directories.push(path); }), writeFileString: (path, content) => input.writeError ? Effect.fail(input.writeError) : Effect.sync(() => { recorded.files.push({ path, content }); }), }), Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { const childProcess = command as unknown as { readonly command: string; readonly args: ReadonlyArray<string>; }; recorded.commands.push({ command: childProcess.command, args: childProcess.args, }); return Effect.succeed(mockProcess(input.xdgMimeExitCode ?? 0)); }), ), ), ), ); const runRegister = ( recorded: RecordedRegistration, input: Parameters<typeof makeHandlerLayer>[1] = {},) => Effect.gen(function* () { const handler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler; yield* handler.register; }).pipe(Effect.provide(makeHandlerLayer(recorded, input))); const emptyRecording = (): RecordedRegistration => ({ directories: [], files: [], commands: [],}); describe("DesktopLinuxUrlHandler", () => { it("renders a scheme-handler desktop entry with freedesktop Exec quoting", () => { const entry = DesktopLinuxUrlHandler.renderUrlHandlerDesktopEntry({ displayName: "T3 Code (Nightly)", execTarget: '/home/al ice/Apps/T3 "100%" $HOME\\x.AppImage', scheme: "t3code", }); assert.include(entry, "[Desktop Entry]"); assert.include(entry, "Name=T3 Code (Nightly)"); // Exec composes both escaping layers: a literal backslash becomes four // backslashes in the file, a quote three characters, a dollar sign two // backslashes plus the sign. assert.include( entry, 'Exec="/home/al ice/Apps/T3 \\\\"100%%\\\\" \\\\$HOME\\\\\\\\x.AppImage" %U', ); assert.include(entry, "NoDisplay=true"); assert.notInclude(entry, "StartupWMClass="); assert.include(entry, "MimeType=x-scheme-handler/t3code;"); }); it("carries structured context on registration errors", () => { const writeError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({ step: "write-desktop-entry", scheme: "t3code", desktopEntryPath: "/home/alice/.local/share/applications/com.t3tools.T3Code.desktop", cause: new Error("boom"), }); assert.equal( writeError.message, "Failed to register the t3code:// URL handler (step: write-desktop-entry).", ); assert.equal( writeError.desktopEntryPath, "/home/alice/.local/share/applications/com.t3tools.T3Code.desktop", ); const exitError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({ step: "set-default-handler", scheme: "t3code", exitCode: 4, }); assert.equal( exitError.message, "Failed to register the t3code:// URL handler (step: set-default-handler, xdg-mime exit code 4).", ); }); it.effect("writes the handler entry and claims the scheme default via xdg-mime", () => { const recorded = emptyRecording(); return Effect.gen(function* () { yield* runRegister(recorded); assert.deepEqual(recorded.directories, ["/home/alice/.local/share/applications"]); assert.equal(recorded.files.length, 1); assert.equal( recorded.files[0]?.path, "/home/alice/.local/share/applications/com.t3tools.T3Code.desktop", ); assert.include( recorded.files[0]?.content, 'Exec="/home/alice/Applications/T3-Code.AppImage" %U', ); assert.include(recorded.files[0]?.content, "MimeType=x-scheme-handler/t3code;"); assert.deepEqual(recorded.commands, [ { command: "xdg-mime", args: ["default", "com.t3tools.T3Code.desktop", "x-scheme-handler/t3code"], }, ]); }); }); it.effect("falls back to the process executable outside an AppImage", () => { const recorded = emptyRecording(); return Effect.gen(function* () { yield* runRegister(recorded, { environment: { appImagePath: Option.none() } }); assert.include( recorded.files[0]?.content, `Exec=${DesktopLinuxUrlHandler.escapeDesktopEntryExecArgument(process.execPath)} %U`, ); }); }); it.effect("does not rewrite the pre-ready entry while the portal can be reading it", () => { const recorded = emptyRecording(); return Effect.gen(function* () { yield* runRegister(recorded, { existingEntry: DesktopLinuxUrlHandler.renderUrlHandlerDesktopEntry({ displayName: "T3 Code (Alpha)", execTarget: "/home/alice/Applications/T3-Code.AppImage", scheme: "t3code", }), }); assert.deepEqual(recorded.files, []); assert.deepEqual(recorded.directories, []); assert.equal(recorded.commands.length, 1); }); }); it.effect("writes the portal identity without claiming the URL scheme in development", () => { const nonLinux = emptyRecording(); const unpackaged = emptyRecording(); return Effect.gen(function* () { yield* runRegister(nonLinux, { environment: { platform: "darwin" } }); yield* runRegister(unpackaged, { environment: { isPackaged: false, linuxDesktopEntryName: "com.t3tools.T3Code.Development.desktop", }, }); assert.deepEqual(nonLinux.files, []); assert.equal( unpackaged.files[0]?.path, "/home/alice/.local/share/applications/com.t3tools.T3Code.Development.desktop", ); assert.deepEqual(unpackaged.commands, []); }); }); it.effect("never fails startup when registration cannot complete", () => { const xdgMimeFailed = emptyRecording(); const writeFailed = emptyRecording(); return Effect.gen(function* () { yield* runRegister(xdgMimeFailed, { xdgMimeExitCode: 1 }); yield* runRegister(writeFailed, { writeError: PlatformError.systemError({ _tag: "PermissionDenied", module: "FileSystem", method: "writeFileString", description: "read-only filesystem", pathOrDescriptor: "/home/alice/.local/share/applications/com.t3tools.T3Code.desktop", }), }); assert.equal(xdgMimeFailed.files.length, 1); assert.deepEqual(writeFailed.commands, []); }); });});
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as PlatformError from "effect/PlatformError";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
interface RecordedRegistration {
readonly directories: string[];
readonly files: Array<{ readonly path: string; readonly content: string }>;
readonly commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }>;
}
const makeEnvironment = (overrides: Record<string, unknown> = {}) =>
DesktopEnvironment.DesktopEnvironment.of({
platform: "linux",
isPackaged: true,
isDevelopment: false,
displayName: "T3 Code (Alpha)",
linuxDesktopEntryName: "com.t3tools.T3Code.desktop",
linuxWmClass: "t3code",
linuxApplicationsDir: "/home/alice/.local/share/applications",
appImagePath: Option.some("/home/alice/Applications/T3-Code.AppImage"),
path: { join: (...parts: ReadonlyArray<string>) => parts.join("/") },
...overrides,
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
const mockProcess = (exitCode: number) =>
ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(1),
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
isRunning: Effect.succeed(false),
kill: () => Effect.void,
unref: Effect.succeed(Effect.void),
stdin: Sink.drain,
stdout: Stream.empty,
stderr: Stream.empty,
all: Stream.empty,
getInputFd: () => Sink.drain,
getOutputFd: () => Stream.empty,
});
const makeHandlerLayer = (
recorded: RecordedRegistration,
input: {
readonly environment?: Record<string, unknown>;
readonly xdgMimeExitCode?: number;
readonly writeError?: PlatformError.PlatformError;
readonly existingEntry?: string;
} = {},
) =>
DesktopLinuxUrlHandler.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.succeed(DesktopEnvironment.DesktopEnvironment, makeEnvironment(input.environment)),
FileSystem.layerNoop({
readFileString: () => Effect.succeed(input.existingEntry ?? ""),
makeDirectory: (path) =>
Effect.sync(() => {
recorded.directories.push(path);
}),
writeFileString: (path, content) =>
input.writeError
? Effect.fail(input.writeError)
: Effect.sync(() => {
recorded.files.push({ path, content });
}),
}),
Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) => {
const childProcess = command as unknown as {
readonly command: string;
readonly args: ReadonlyArray<string>;
};
recorded.commands.push({
command: childProcess.command,
args: childProcess.args,
});
return Effect.succeed(mockProcess(input.xdgMimeExitCode ?? 0));
}),
),
),
),
);
const runRegister = (
recorded: RecordedRegistration,
input: Parameters<typeof makeHandlerLayer>[1] = {},
) =>
Effect.gen(function* () {
const handler = yield* DesktopLinuxUrlHandler.DesktopLinuxUrlHandler;
yield* handler.register;
}).pipe(Effect.provide(makeHandlerLayer(recorded, input)));
const emptyRecording = (): RecordedRegistration => ({
directories: [],
files: [],
commands: [],
});
describe("DesktopLinuxUrlHandler", () => {
it("renders a scheme-handler desktop entry with freedesktop Exec quoting", () => {
const entry = DesktopLinuxUrlHandler.renderUrlHandlerDesktopEntry({
displayName: "T3 Code (Nightly)",
execTarget: '/home/al ice/Apps/T3 "100%" $HOME\\x.AppImage',
scheme: "t3code",
});
assert.include(entry, "[Desktop Entry]");
assert.include(entry, "Name=T3 Code (Nightly)");
// Exec composes both escaping layers: a literal backslash becomes four
// backslashes in the file, a quote three characters, a dollar sign two
// backslashes plus the sign.
assert.include(
entry,
'Exec="/home/al ice/Apps/T3 \\\\"100%%\\\\" \\\\$HOME\\\\\\\\x.AppImage" %U',
);
assert.include(entry, "NoDisplay=true");
assert.notInclude(entry, "StartupWMClass=");
assert.include(entry, "MimeType=x-scheme-handler/t3code;");
});
it("carries structured context on registration errors", () => {
const writeError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({
step: "write-desktop-entry",
scheme: "t3code",
desktopEntryPath: "/home/alice/.local/share/applications/com.t3tools.T3Code.desktop",
cause: new Error("boom"),
});
assert.equal(
writeError.message,
"Failed to register the t3code:// URL handler (step: write-desktop-entry).",
);
assert.equal(
writeError.desktopEntryPath,
"/home/alice/.local/share/applications/com.t3tools.T3Code.desktop",
);
const exitError = new DesktopLinuxUrlHandler.DesktopLinuxUrlHandlerRegistrationError({
step: "set-default-handler",
scheme: "t3code",
exitCode: 4,
});
assert.equal(
exitError.message,
"Failed to register the t3code:// URL handler (step: set-default-handler, xdg-mime exit code 4).",
);
});
it.effect("writes the handler entry and claims the scheme default via xdg-mime", () => {
const recorded = emptyRecording();
return Effect.gen(function* () {
yield* runRegister(recorded);
assert.deepEqual(recorded.directories, ["/home/alice/.local/share/applications"]);
assert.equal(recorded.files.length, 1);
assert.equal(
recorded.files[0]?.path,
"/home/alice/.local/share/applications/com.t3tools.T3Code.desktop",
);
assert.include(
recorded.files[0]?.content,
'Exec="/home/alice/Applications/T3-Code.AppImage" %U',
);
assert.include(recorded.files[0]?.content, "MimeType=x-scheme-handler/t3code;");
assert.deepEqual(recorded.commands, [
{
command: "xdg-mime",
args: ["default", "com.t3tools.T3Code.desktop", "x-scheme-handler/t3code"],
},
]);
});
});
it.effect("falls back to the process executable outside an AppImage", () => {
const recorded = emptyRecording();
return Effect.gen(function* () {
yield* runRegister(recorded, { environment: { appImagePath: Option.none() } });
assert.include(
recorded.files[0]?.content,
`Exec=${DesktopLinuxUrlHandler.escapeDesktopEntryExecArgument(process.execPath)} %U`,
);
});
});
it.effect("does not rewrite the pre-ready entry while the portal can be reading it", () => {
const recorded = emptyRecording();
return Effect.gen(function* () {
yield* runRegister(recorded, {
existingEntry: DesktopLinuxUrlHandler.renderUrlHandlerDesktopEntry({
displayName: "T3 Code (Alpha)",
execTarget: "/home/alice/Applications/T3-Code.AppImage",
scheme: "t3code",
}),
});
assert.deepEqual(recorded.files, []);
assert.deepEqual(recorded.directories, []);
assert.equal(recorded.commands.length, 1);
});
});
it.effect("writes the portal identity without claiming the URL scheme in development", () => {
const nonLinux = emptyRecording();
const unpackaged = emptyRecording();
return Effect.gen(function* () {
yield* runRegister(nonLinux, { environment: { platform: "darwin" } });
yield* runRegister(unpackaged, {
environment: {
isPackaged: false,
linuxDesktopEntryName: "com.t3tools.T3Code.Development.desktop",
},
});
assert.deepEqual(nonLinux.files, []);
assert.equal(
unpackaged.files[0]?.path,
"/home/alice/.local/share/applications/com.t3tools.T3Code.Development.desktop",
);
assert.deepEqual(unpackaged.commands, []);
});
});
it.effect("never fails startup when registration cannot complete", () => {
const xdgMimeFailed = emptyRecording();
const writeFailed = emptyRecording();
return Effect.gen(function* () {
yield* runRegister(xdgMimeFailed, { xdgMimeExitCode: 1 });
yield* runRegister(writeFailed, {
writeError: PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "writeFileString",
description: "read-only filesystem",
pathOrDescriptor: "/home/alice/.local/share/applications/com.t3tools.T3Code.desktop",
}),
});
assert.equal(xdgMimeFailed.files.length, 1);
assert.deepEqual(writeFailed.commands, []);
});
});
});