源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
OPENWORK_DESKTOP_ID,
|
||||
buildOpenworkDesktopEntry,
|
||||
createLinuxDesktopIntegration,
|
||||
quoteDesktopExec,
|
||||
} from "./linux-desktop-integration.mjs";
|
||||
|
||||
const temporaryRoots = [];
|
||||
const iconSizes = [16, 24, 32, 48, 64, 96, 128, 256, 512];
|
||||
|
||||
async function createHarness(options = {}) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "openwork-appimage-integration-"));
|
||||
temporaryRoots.push(root);
|
||||
const homeDir = path.join(root, "home");
|
||||
const dataHome = path.join(root, "data");
|
||||
const configHome = path.join(root, "config");
|
||||
const resourcesPath = path.join(root, "resources");
|
||||
const appImagePath = options.appImagePath ?? path.join(root, "OpenWork AppImage");
|
||||
await mkdir(path.join(resourcesPath, "icons", "linux"), { recursive: true });
|
||||
await Promise.all(iconSizes.map((size) => (
|
||||
writeFile(path.join(resourcesPath, "icons", "linux", `${size}x${size}.png`), String(size))
|
||||
)));
|
||||
await writeFile(appImagePath, "appimage");
|
||||
|
||||
let defaultHandler = options.defaultHandler ?? null;
|
||||
const commands = [];
|
||||
const runCommand = async (command, args) => {
|
||||
commands.push([command, args]);
|
||||
if (command === "xdg-mime" && args[0] === "query") {
|
||||
return { ok: true, stdout: defaultHandler ? `${defaultHandler}\n` : "", stderr: "" };
|
||||
}
|
||||
if (command === "xdg-mime" && args[0] === "default") {
|
||||
defaultHandler = args[1];
|
||||
}
|
||||
return { ok: true, stdout: "", stderr: "" };
|
||||
};
|
||||
const dialogResponses = [...(options.dialogResponses ?? [])];
|
||||
const dialogs = [];
|
||||
const dialog = {
|
||||
async showMessageBox(_window, dialogOptions) {
|
||||
dialogs.push(dialogOptions);
|
||||
return dialogResponses.shift() ?? { response: 0, checkboxChecked: false };
|
||||
},
|
||||
};
|
||||
const app = {
|
||||
isPackaged: options.isPackaged ?? true,
|
||||
getVersion: () => options.version ?? "0.18.7",
|
||||
};
|
||||
const integration = createLinuxDesktopIntegration({
|
||||
app,
|
||||
dialog,
|
||||
appName: "OpenWork",
|
||||
distribution: "public",
|
||||
env: {
|
||||
APPIMAGE: appImagePath,
|
||||
XDG_DATA_HOME: dataHome,
|
||||
XDG_CONFIG_HOME: configHome,
|
||||
XDG_DATA_DIRS: path.join(root, "system-data"),
|
||||
},
|
||||
platform: options.platform ?? "linux",
|
||||
homeDir,
|
||||
resourcesPath,
|
||||
runCommand,
|
||||
});
|
||||
return {
|
||||
appImagePath,
|
||||
commands,
|
||||
configHome,
|
||||
dataHome,
|
||||
dialogs,
|
||||
getDefaultHandler: () => defaultHandler,
|
||||
integration,
|
||||
root,
|
||||
};
|
||||
}
|
||||
|
||||
// Simulates the next launch of the same install: same XDG dirs and state, but a
|
||||
// possibly different AppImage path and app version.
|
||||
function relaunchAt(harness, appImagePath, options = {}) {
|
||||
let defaultHandler = options.defaultHandler ?? OPENWORK_DESKTOP_ID;
|
||||
const dialogs = [];
|
||||
const integration = createLinuxDesktopIntegration({
|
||||
app: { isPackaged: true, getVersion: () => options.version ?? "0.18.7" },
|
||||
dialog: {
|
||||
async showMessageBox(_window, dialogOptions) {
|
||||
dialogs.push(dialogOptions);
|
||||
return { response: 0, checkboxChecked: false };
|
||||
},
|
||||
},
|
||||
appName: "OpenWork",
|
||||
distribution: "public",
|
||||
env: {
|
||||
APPIMAGE: appImagePath,
|
||||
XDG_DATA_HOME: harness.dataHome,
|
||||
XDG_CONFIG_HOME: harness.configHome,
|
||||
XDG_DATA_DIRS: path.join(harness.root, "system-data"),
|
||||
},
|
||||
platform: "linux",
|
||||
resourcesPath: path.join(harness.root, "resources"),
|
||||
runCommand: async (command, args) => {
|
||||
if (command === "xdg-mime" && args[0] === "query") {
|
||||
return { ok: true, stdout: defaultHandler ? `${defaultHandler}\n` : "", stderr: "" };
|
||||
}
|
||||
if (command === "xdg-mime" && args[0] === "default") defaultHandler = args[1];
|
||||
return { ok: true, stdout: "", stderr: "" };
|
||||
},
|
||||
});
|
||||
return { dialogs, getDefaultHandler: () => defaultHandler, integration };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("Linux AppImage desktop integration", () => {
|
||||
it("quotes AppImage paths for Desktop Entry Exec fields", () => {
|
||||
const appImagePath = String.raw`/home/alice/Applications/Open Work "daily"$%draft\archive`;
|
||||
assert.equal(
|
||||
quoteDesktopExec(appImagePath),
|
||||
String.raw`"/home/alice/Applications/Open Work \"daily\"\$%%draft\\archive"`,
|
||||
);
|
||||
assert.equal(quoteDesktopExec("`"), '"\\`"');
|
||||
const entry = buildOpenworkDesktopEntry({
|
||||
appImagePath,
|
||||
appName: "OpenWork",
|
||||
appVersion: "0.18.7",
|
||||
distribution: "public",
|
||||
});
|
||||
assert.match(entry, /^Exec=".*" %U$/m);
|
||||
assert.match(entry, /^MimeType=x-scheme-handler\/openwork;$/m);
|
||||
assert.match(entry, /^StartupWMClass=com\.differentai\.openwork$/m);
|
||||
});
|
||||
|
||||
it("is unavailable outside a packaged Linux AppImage", async () => {
|
||||
const harness = await createHarness({ isPackaged: false });
|
||||
const status = await harness.integration.getStatus();
|
||||
assert.equal(status.supported, false);
|
||||
assert.equal(status.state, "unsupported");
|
||||
});
|
||||
|
||||
it("installs its launcher, icons, and openwork handler", async () => {
|
||||
const harness = await createHarness();
|
||||
const result = await harness.integration.install();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.status.state, "integrated");
|
||||
assert.equal(harness.getDefaultHandler(), OPENWORK_DESKTOP_ID);
|
||||
|
||||
const desktopEntry = await readFile(harness.integration.paths.desktopEntryPath, "utf8");
|
||||
assert.match(desktopEntry, new RegExp(`^Exec=${quoteDesktopExec(harness.appImagePath)} %U$`, "m"));
|
||||
assert.match(desktopEntry, /^X-OpenWork-Managed=true$/m);
|
||||
for (const size of iconSizes) {
|
||||
assert.equal(await readFile(harness.integration.paths.iconPaths[size], "utf8"), String(size));
|
||||
}
|
||||
});
|
||||
|
||||
it("detects a moved AppImage and repairs the launcher for the new path", async () => {
|
||||
const first = await createHarness();
|
||||
assert.equal((await first.integration.install()).ok, true);
|
||||
const movedPath = path.join(first.root, "Applications", "OpenWork.AppImage");
|
||||
await mkdir(path.dirname(movedPath), { recursive: true });
|
||||
await rename(first.appImagePath, movedPath);
|
||||
|
||||
const moved = createLinuxDesktopIntegration({
|
||||
app: { isPackaged: true, getVersion: () => "0.18.7" },
|
||||
dialog: { showMessageBox: async () => ({ response: 0, checkboxChecked: false }) },
|
||||
appName: "OpenWork",
|
||||
distribution: "public",
|
||||
env: {
|
||||
APPIMAGE: movedPath,
|
||||
XDG_DATA_HOME: first.dataHome,
|
||||
XDG_CONFIG_HOME: first.configHome,
|
||||
XDG_DATA_DIRS: path.join(first.root, "system-data"),
|
||||
},
|
||||
platform: "linux",
|
||||
resourcesPath: path.join(first.root, "resources"),
|
||||
runCommand: async (command, args) => {
|
||||
if (command === "xdg-mime" && args[0] === "query") {
|
||||
return { ok: true, stdout: `${OPENWORK_DESKTOP_ID}\n`, stderr: "" };
|
||||
}
|
||||
return { ok: true, stdout: "", stderr: "" };
|
||||
},
|
||||
});
|
||||
const before = await moved.getStatus();
|
||||
assert.equal(before.state, "needs_repair");
|
||||
assert.deepEqual(before.issues, ["appimage-path"]);
|
||||
assert.equal((await moved.install()).ok, true);
|
||||
assert.match(
|
||||
await readFile(moved.paths.desktopEntryPath, "utf8"),
|
||||
new RegExp(`^TryExec=${movedPath}$`, "m"),
|
||||
);
|
||||
});
|
||||
|
||||
it("silently repairs an owned launcher after a self-update lands a new filename", async () => {
|
||||
const harness = await createHarness();
|
||||
assert.equal((await harness.integration.install()).ok, true);
|
||||
|
||||
// Linux artifacts carry the version in their filename, so electron-updater
|
||||
// installs each release at a new path and stales the launcher.
|
||||
const updatedPath = path.join(harness.root, "openwork-linux-x86_64-0.18.8.AppImage");
|
||||
await rename(harness.appImagePath, updatedPath);
|
||||
const relaunched = relaunchAt(harness, updatedPath, { version: "0.18.8" });
|
||||
assert.equal((await relaunched.integration.getStatus()).state, "needs_repair");
|
||||
|
||||
const status = await relaunched.integration.maybePrompt({});
|
||||
assert.equal(status.state, "integrated");
|
||||
assert.equal(relaunched.dialogs.length, 0);
|
||||
const entry = await readFile(relaunched.integration.paths.desktopEntryPath, "utf8");
|
||||
assert.match(entry, new RegExp(`^TryExec=${updatedPath}$`, "m"));
|
||||
assert.match(entry, /^X-OpenWork-Version=0\.18\.8$/m);
|
||||
});
|
||||
|
||||
it("silently refreshes an owned launcher when only the version changed", async () => {
|
||||
const harness = await createHarness();
|
||||
assert.equal((await harness.integration.install()).ok, true);
|
||||
|
||||
const relaunched = relaunchAt(harness, harness.appImagePath, { version: "0.18.8" });
|
||||
assert.deepEqual((await relaunched.integration.getStatus()).issues, ["version"]);
|
||||
|
||||
const status = await relaunched.integration.maybePrompt({});
|
||||
assert.equal(status.state, "integrated");
|
||||
assert.equal(relaunched.dialogs.length, 0);
|
||||
});
|
||||
|
||||
it("keeps a failed silent repair quiet and leaves it to Settings", async () => {
|
||||
const harness = await createHarness();
|
||||
assert.equal((await harness.integration.install()).ok, true);
|
||||
|
||||
const movedPath = path.join(harness.root, "openwork-linux-x86_64-0.18.8.AppImage");
|
||||
await rename(harness.appImagePath, movedPath);
|
||||
const relaunched = relaunchAt(harness, movedPath, { version: "0.18.8" });
|
||||
await rm(path.join(harness.root, "resources"), { recursive: true, force: true });
|
||||
|
||||
const status = await relaunched.integration.maybePrompt({});
|
||||
assert.equal(status.state, "needs_repair");
|
||||
assert.equal(relaunched.dialogs.length, 0);
|
||||
});
|
||||
|
||||
it("does not prompt after a remembered Not now response", async () => {
|
||||
const harness = await createHarness({
|
||||
dialogResponses: [{ response: 0, checkboxChecked: true }],
|
||||
});
|
||||
await harness.integration.maybePrompt({});
|
||||
await harness.integration.maybePrompt({});
|
||||
assert.equal(harness.dialogs.length, 1);
|
||||
assert.equal(harness.dialogs[0].buttons[1], "Integrate");
|
||||
});
|
||||
|
||||
it("integrates from the first-run prompt and points to the matching Settings control", async () => {
|
||||
const harness = await createHarness({
|
||||
dialogResponses: [{ response: 1, checkboxChecked: false }],
|
||||
});
|
||||
const status = await harness.integration.maybePrompt({});
|
||||
assert.equal(status.state, "integrated");
|
||||
assert.equal(harness.dialogs.length, 1);
|
||||
assert.match(harness.dialogs[0].detail, /Settings → Preferences → AppImage desktop integration/);
|
||||
});
|
||||
|
||||
it("recognizes an external manager and does not duplicate its integration", async () => {
|
||||
const managerDesktopId = "it.mijorus.gearlever.openwork.desktop";
|
||||
const harness = await createHarness({ defaultHandler: managerDesktopId });
|
||||
const applications = path.join(harness.dataHome, "applications");
|
||||
await mkdir(applications, { recursive: true });
|
||||
await writeFile(path.join(applications, managerDesktopId), `[Desktop Entry]
|
||||
Type=Application
|
||||
Name=OpenWork
|
||||
Exec=${quoteDesktopExec(harness.appImagePath)} %U
|
||||
TryExec=${harness.appImagePath}
|
||||
MimeType=x-scheme-handler/openwork;
|
||||
`);
|
||||
|
||||
const status = await harness.integration.getStatus();
|
||||
assert.equal(status.state, "managed_externally");
|
||||
assert.equal(status.ownership, "external");
|
||||
assert.equal((await harness.integration.install()).ok, false);
|
||||
await harness.integration.maybePrompt({});
|
||||
assert.equal(harness.dialogs.length, 0);
|
||||
});
|
||||
|
||||
it("registers an externally managed canonical launcher without rewriting it", async () => {
|
||||
const harness = await createHarness({ defaultHandler: OPENWORK_DESKTOP_ID });
|
||||
await mkdir(path.dirname(harness.integration.paths.desktopEntryPath), { recursive: true });
|
||||
const original = `[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Manager-owned OpenWork
|
||||
Exec=${quoteDesktopExec(harness.appImagePath)} %U
|
||||
TryExec=${harness.appImagePath}
|
||||
MimeType=x-scheme-handler/openwork;
|
||||
`;
|
||||
await writeFile(harness.integration.paths.desktopEntryPath, original);
|
||||
const result = await harness.integration.install({ useExternalLauncher: true });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(await readFile(harness.integration.paths.desktopEntryPath, "utf8"), original);
|
||||
});
|
||||
|
||||
it("never overwrites a canonical launcher it does not own", async () => {
|
||||
const harness = await createHarness();
|
||||
await mkdir(path.dirname(harness.integration.paths.desktopEntryPath), { recursive: true });
|
||||
const original = "[Desktop Entry]\nType=Application\nName=Unrelated launcher\nExec=/usr/bin/false\n";
|
||||
await writeFile(harness.integration.paths.desktopEntryPath, original);
|
||||
const result = await harness.integration.install();
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error, /will not be overwritten/);
|
||||
assert.equal(await readFile(harness.integration.paths.desktopEntryPath, "utf8"), original);
|
||||
});
|
||||
|
||||
it("registers an existing manager launcher without creating a duplicate", async () => {
|
||||
const managerDesktopId = "appimagelauncher-openwork.desktop";
|
||||
const harness = await createHarness({ defaultHandler: "firefox.desktop" });
|
||||
const managerPath = path.join(harness.dataHome, "applications", managerDesktopId);
|
||||
await mkdir(path.dirname(managerPath), { recursive: true });
|
||||
const managerEntry = `[Desktop Entry]
|
||||
Type=Application
|
||||
Name=OpenWork
|
||||
Exec=${quoteDesktopExec(harness.appImagePath)} %U
|
||||
TryExec=${harness.appImagePath}
|
||||
MimeType=x-scheme-handler/openwork;
|
||||
`;
|
||||
await writeFile(managerPath, managerEntry);
|
||||
|
||||
const before = await harness.integration.getStatus();
|
||||
assert.equal(before.state, "needs_repair");
|
||||
assert.equal(before.ownership, "external");
|
||||
const result = await harness.integration.install({ useExternalLauncher: true });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.status.state, "managed_externally");
|
||||
assert.equal(harness.getDefaultHandler(), managerDesktopId);
|
||||
assert.equal(await readFile(managerPath, "utf8"), managerEntry);
|
||||
await assert.rejects(readFile(harness.integration.paths.desktopEntryPath, "utf8"));
|
||||
});
|
||||
|
||||
it("removes only owned files and restores the previous protocol handler", async () => {
|
||||
const previousHandler = "firefox.desktop";
|
||||
const harness = await createHarness({ defaultHandler: previousHandler });
|
||||
const systemApplications = path.join(harness.root, "system-data", "applications");
|
||||
await mkdir(systemApplications, { recursive: true });
|
||||
await writeFile(path.join(systemApplications, previousHandler), "[Desktop Entry]\nType=Application\n");
|
||||
|
||||
assert.equal((await harness.integration.install()).ok, true);
|
||||
const result = await harness.integration.remove();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(harness.getDefaultHandler(), previousHandler);
|
||||
assert.equal(result.status.state, "not_integrated");
|
||||
assert.equal(
|
||||
harness.commands.some(([command, args]) => (
|
||||
command === "xdg-mime"
|
||||
&& args[0] === "default"
|
||||
&& args[1] === previousHandler
|
||||
)),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user