mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* Add Langfuse telemetry relay * Configure telemetry worker custom domain * Add telemetry relay health check * Harden telemetry relay config
301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { posix } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import type { ToolPackConfig } from "../src/config.js";
|
|
import {
|
|
buildDockerArgs,
|
|
matchesAppImageProcess,
|
|
renderDesktopTemplate,
|
|
sanitizeNamespace,
|
|
} from "../src/linux.js";
|
|
|
|
function makeConfig(): ToolPackConfig {
|
|
return {
|
|
containerized: true,
|
|
electronBuilderCliPath: "/x/electron-builder/cli.js",
|
|
electronDistPath: "/x/electron/dist",
|
|
electronVersion: "41.3.0",
|
|
macCompression: "normal",
|
|
namespace: "default",
|
|
platform: "linux",
|
|
portable: false,
|
|
removeData: false,
|
|
removeLogs: false,
|
|
removeProductUserData: false,
|
|
removeSidecars: false,
|
|
roots: {
|
|
output: {
|
|
appBuilderRoot: "/work/.tmp/tools-pack/out/linux/namespaces/default/builder",
|
|
namespaceRoot: "/work/.tmp/tools-pack/out/linux/namespaces/default",
|
|
platformRoot: "/work/.tmp/tools-pack/out/linux",
|
|
root: "/work/.tmp/tools-pack/out",
|
|
},
|
|
runtime: {
|
|
namespaceBaseRoot: "/work/.tmp/tools-pack/runtime/linux/namespaces",
|
|
namespaceRoot: "/work/.tmp/tools-pack/runtime/linux/namespaces/default",
|
|
},
|
|
cacheRoot: "/work/.tmp/tools-pack/cache",
|
|
toolPackRoot: "/work/.tmp/tools-pack",
|
|
},
|
|
silent: true,
|
|
signed: false,
|
|
to: "all",
|
|
webOutputMode: "server",
|
|
workspaceRoot: "/work",
|
|
};
|
|
}
|
|
|
|
describe("buildDockerArgs", () => {
|
|
it("returns the expected docker argv array", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
expect(args[0]).toBe("run");
|
|
expect(args).toContain("--rm");
|
|
expect(args).toContain("--user");
|
|
expect(args).toContain("1000:1000");
|
|
expect(args).toContain("electronuserland/builder:base");
|
|
});
|
|
|
|
it("mounts the workspace at /project", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
expect(args).toContain("-v");
|
|
expect(args).toContain("/work:/project");
|
|
});
|
|
|
|
it("mounts docker home and electron caches under .tmp/tools-pack/.docker-*", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
expect(args).toContain(`${posix.join("/work/.tmp/tools-pack", ".docker-home")}:/home/builder`);
|
|
expect(args).toContain(`${posix.join("/work/.tmp/tools-pack", ".docker-cache", "electron")}:/home/builder/.cache/electron`);
|
|
expect(args).toContain(
|
|
`${posix.join("/work/.tmp/tools-pack", ".docker-cache", "electron-builder")}:/home/builder/.cache/electron-builder`,
|
|
);
|
|
});
|
|
|
|
it("mounts the tool-pack root at /tools-pack so inner build writes to host-visible output dir", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
expect(args).toContain("/work/.tmp/tools-pack:/tools-pack");
|
|
});
|
|
|
|
it("sets HOME and ELECTRON_CACHE env vars", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
expect(args).toContain("HOME=/home/builder");
|
|
expect(args).toContain("ELECTRON_CACHE=/home/builder/.cache/electron");
|
|
expect(args).toContain("ELECTRON_BUILDER_CACHE=/home/builder/.cache/electron-builder");
|
|
});
|
|
|
|
it("passes the telemetry relay URL into containerized builds when configured", () => {
|
|
const args = buildDockerArgs(
|
|
{
|
|
...makeConfig(),
|
|
telemetryRelayUrl: "https://telemetry.open-design.ai/api/langfuse",
|
|
},
|
|
{ uid: 1000, gid: 1000 },
|
|
);
|
|
expect(args).toContain("OPEN_DESIGN_TELEMETRY_RELAY_URL=https://telemetry.open-design.ai/api/langfuse");
|
|
});
|
|
|
|
it("re-invokes pnpm tools-pack linux build inside the container without --containerized", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
const last = args[args.length - 1];
|
|
expect(last).toMatch(/npx --yes pnpm@\d+\.\d+\.\d+ install --frozen-lockfile/);
|
|
expect(last).toMatch(/npx --yes pnpm@\d+\.\d+\.\d+ tools-pack linux build --to all --namespace default/);
|
|
expect(last).not.toMatch(/--containerized/);
|
|
});
|
|
|
|
it("invokes pnpm via `npx --yes pnpm@<version>` (electronuserland/builder:base strips corepack, and the non-root container can't write Node shim dir)", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
const last = args[args.length - 1];
|
|
expect(last).not.toMatch(/corepack/);
|
|
expect(last).toMatch(/npx --yes pnpm@/);
|
|
});
|
|
|
|
it("hardcoded pnpm version stays in lockstep with root package.json `packageManager`", () => {
|
|
// Guard against silent drift: if someone bumps packageManager in the
|
|
// root package.json but forgets to update PNPM_VERSION in linux.ts,
|
|
// the Linux container build would silently keep using the old pnpm.
|
|
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
const rootPkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf-8")) as {
|
|
packageManager?: string;
|
|
};
|
|
const match = String(rootPkg.packageManager ?? "").match(/^pnpm@(\d+\.\d+\.\d+)$/);
|
|
expect(match, `expected root packageManager "pnpm@x.y.z", got ${rootPkg.packageManager}`).not.toBeNull();
|
|
const expectedVersion = match![1];
|
|
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
const last = args[args.length - 1];
|
|
expect(last).toContain(`npx --yes pnpm@${expectedVersion}`);
|
|
});
|
|
|
|
it("forwards --dir /tools-pack so inner build output lands under the mounted host dir", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
const last = args[args.length - 1];
|
|
expect(last).toMatch(/--dir \/tools-pack/);
|
|
});
|
|
|
|
it("forwards --portable when config.portable is true", () => {
|
|
const args = buildDockerArgs({ ...makeConfig(), portable: true }, { uid: 1000, gid: 1000 });
|
|
const last = args[args.length - 1];
|
|
expect(last).toMatch(/--portable/);
|
|
});
|
|
|
|
it("omits --portable when config.portable is false", () => {
|
|
const args = buildDockerArgs(makeConfig(), { uid: 1000, gid: 1000 });
|
|
const last = args[args.length - 1];
|
|
expect(last).not.toMatch(/--portable/);
|
|
});
|
|
|
|
it("forwards a shell-quoted --app-version to the inner build", () => {
|
|
const args = buildDockerArgs(
|
|
{ ...makeConfig(), appVersion: "0.5.0-beta.1;echo-nope" },
|
|
{ uid: 1000, gid: 1000 },
|
|
);
|
|
const last = args[args.length - 1];
|
|
expect(last).toContain("--app-version '0.5.0-beta.1;echo-nope'");
|
|
});
|
|
|
|
it("shell-quotes apostrophes in --app-version", () => {
|
|
const args = buildDockerArgs(
|
|
{ ...makeConfig(), appVersion: "0.5.0-beta.1'quoted" },
|
|
{ uid: 1000, gid: 1000 },
|
|
);
|
|
const last = args[args.length - 1];
|
|
expect(last).toContain("--app-version '0.5.0-beta.1'\\''quoted'");
|
|
});
|
|
});
|
|
|
|
describe("renderDesktopTemplate", () => {
|
|
const template = `[Desktop Entry]
|
|
Type=Application
|
|
Name=Open Design (@@NAMESPACE@@)
|
|
Exec=env OD_PACKAGED_NAMESPACE=@@NAMESPACE@@ @@EXEC_PATH@@ --appimage-extract-and-run %U
|
|
Icon=@@ICON_PATH@@
|
|
MimeType=x-scheme-handler/od;
|
|
`;
|
|
|
|
it("substitutes all @@TOKEN@@ placeholders", () => {
|
|
const out = renderDesktopTemplate(template, {
|
|
namespace: "default",
|
|
execPath: "/home/u/.local/bin/Open-Design.default.AppImage",
|
|
iconName: "open-design-default",
|
|
});
|
|
expect(out).toContain("Name=Open Design (default)");
|
|
expect(out).toContain(
|
|
"Exec=env OD_PACKAGED_NAMESPACE=default /home/u/.local/bin/Open-Design.default.AppImage --appimage-extract-and-run %U",
|
|
);
|
|
expect(out).toContain("Icon=open-design-default");
|
|
});
|
|
|
|
it("uses OD_PACKAGED_NAMESPACE (not OD_NAMESPACE) so apps/packaged actually picks up the namespace override", () => {
|
|
const out = renderDesktopTemplate(template, {
|
|
namespace: "ns",
|
|
execPath: "/x",
|
|
iconName: "open-design-ns",
|
|
});
|
|
expect(out).toMatch(/^Exec=env OD_PACKAGED_NAMESPACE=ns /m);
|
|
expect(out).not.toMatch(/OD_NAMESPACE=/);
|
|
});
|
|
|
|
it("preserves --appimage-extract-and-run on the Exec= line so menu launches bypass FUSE", () => {
|
|
const out = renderDesktopTemplate(template, {
|
|
namespace: "ns",
|
|
execPath: "/x",
|
|
iconName: "open-design-ns",
|
|
});
|
|
expect(out).toMatch(/^Exec=.*--appimage-extract-and-run .*%U$/m);
|
|
});
|
|
|
|
it("leaves no @@...@@ tokens unsubstituted", () => {
|
|
const out = renderDesktopTemplate(template, {
|
|
namespace: "ns",
|
|
execPath: "/x",
|
|
iconName: "open-design-ns",
|
|
});
|
|
expect(out).not.toMatch(/@@[A-Z_]+@@/);
|
|
});
|
|
|
|
it("preserves the MimeType=x-scheme-handler/od; line", () => {
|
|
const out = renderDesktopTemplate(template, {
|
|
namespace: "ns",
|
|
execPath: "/x",
|
|
iconName: "open-design-ns",
|
|
});
|
|
expect(out).toContain("MimeType=x-scheme-handler/od;");
|
|
});
|
|
});
|
|
|
|
describe("sanitizeNamespace", () => {
|
|
it("replaces non-alphanumeric chars with hyphens", () => {
|
|
expect(sanitizeNamespace("a/b c")).toBe("a-b-c");
|
|
});
|
|
});
|
|
|
|
describe("matchesAppImageProcess", () => {
|
|
const installPath = "/home/u/.local/bin/Open-Design.default.AppImage";
|
|
|
|
it("matches FUSE-mode (executable === installPath)", () => {
|
|
const ok = matchesAppImageProcess(
|
|
{ pid: 1234, executable: installPath, env: {} },
|
|
installPath,
|
|
);
|
|
expect(ok).toBe(true);
|
|
});
|
|
|
|
it("matches extracted-mode (env.APPIMAGE === installPath, executable matches /tmp/.mount_*/AppRun)", () => {
|
|
const ok = matchesAppImageProcess(
|
|
{ pid: 1234, executable: "/tmp/.mount_abc123/AppRun", env: { APPIMAGE: installPath } },
|
|
installPath,
|
|
);
|
|
expect(ok).toBe(true);
|
|
});
|
|
|
|
it("rejects unrelated processes", () => {
|
|
const ok = matchesAppImageProcess(
|
|
{ pid: 9999, executable: "/usr/bin/node", env: {} },
|
|
installPath,
|
|
);
|
|
expect(ok).toBe(false);
|
|
});
|
|
|
|
it("rejects extracted-mode with mismatched APPIMAGE env", () => {
|
|
const ok = matchesAppImageProcess(
|
|
{ pid: 1234, executable: "/tmp/.mount_abc/AppRun", env: { APPIMAGE: "/other/path.AppImage" } },
|
|
installPath,
|
|
);
|
|
expect(ok).toBe(false);
|
|
});
|
|
|
|
it("rejects extracted-mode when APPIMAGE env is missing", () => {
|
|
const ok = matchesAppImageProcess(
|
|
{ pid: 1234, executable: "/tmp/.mount_abc123/AppRun", env: {} },
|
|
installPath,
|
|
);
|
|
expect(ok).toBe(false);
|
|
});
|
|
|
|
it("matches --appimage-extract-and-run mode (executable in /tmp/appimage_extracted_*/<binary>)", () => {
|
|
const ok = matchesAppImageProcess(
|
|
{
|
|
pid: 1234,
|
|
executable: "/tmp/appimage_extracted_fe548e54/Open Design",
|
|
env: { APPIMAGE: installPath },
|
|
},
|
|
installPath,
|
|
);
|
|
expect(ok).toBe(true);
|
|
});
|
|
|
|
it("rejects extract-and-run mode with mismatched APPIMAGE env", () => {
|
|
const ok = matchesAppImageProcess(
|
|
{
|
|
pid: 1234,
|
|
executable: "/tmp/appimage_extracted_fe548e54/Open Design",
|
|
env: { APPIMAGE: "/elsewhere/Other.AppImage" },
|
|
},
|
|
installPath,
|
|
);
|
|
expect(ok).toBe(false);
|
|
});
|
|
});
|