mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-02 06:24:38 +00:00
194 lines
8.2 KiB
TypeScript
194 lines
8.2 KiB
TypeScript
import { expect, test } from "bun:test"
|
|
import { statSync } from "node:fs"
|
|
import { cp, mkdtemp, rm } from "node:fs/promises"
|
|
import { createRequire } from "node:module"
|
|
import os from "node:os"
|
|
import path from "node:path"
|
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
import type { Configuration } from "electron-builder"
|
|
|
|
const legacyDesktopEntry = "resources/linux/opencode-desktop.desktop"
|
|
// Use electron-builder's matcher so the tests also cover its glob and directory traversal semantics.
|
|
const { FileMatcher } = createRequire(import.meta.resolve("electron-builder"))("app-builder-lib/out/fileMatcher")
|
|
|
|
const channels = [
|
|
{ channel: "dev", appId: "ai.opencode.desktop.dev" },
|
|
{ channel: "beta", appId: "ai.opencode.desktop.beta" },
|
|
{ channel: "prod", appId: "ai.opencode.desktop" },
|
|
] as const
|
|
|
|
for (const channel of channels) {
|
|
test(`uses one Linux desktop identity for ${channel.channel}`, async () => {
|
|
const previous = process.env.OPENCODE_CHANNEL
|
|
process.env.OPENCODE_CHANNEL = channel.channel
|
|
|
|
const module = await import(`./electron-builder.config.ts?channel=${channel.channel}`)
|
|
const config = module.default as Configuration
|
|
|
|
if (previous === undefined) delete process.env.OPENCODE_CHANNEL
|
|
else process.env.OPENCODE_CHANNEL = previous
|
|
|
|
expect(config.appId).toBe(channel.appId)
|
|
expect(config.extraMetadata?.desktopName).toBe(`${channel.appId}.desktop`)
|
|
expect(config.linux?.executableName).toBe(channel.appId)
|
|
expect(config.linux?.desktop?.entry?.StartupWMClass).toBe(channel.appId)
|
|
expect(config.deb?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${channel.appId}.metainfo.xml`))
|
|
expect(config.rpm?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${channel.appId}.metainfo.xml`))
|
|
})
|
|
|
|
test(`trims external dependencies without excluding runtime files for ${channel.channel}`, async () => {
|
|
const config = (await import(`./electron-builder.config.ts?channel=${channel.channel}`)).default as Configuration
|
|
const filter = new FileMatcher(import.meta.dirname, "", (value: string) => value, [
|
|
"**/*",
|
|
...(Array.isArray(config.files) ? config.files : []).filter(
|
|
(value): value is string => typeof value === "string" && value.startsWith("!"),
|
|
),
|
|
]).createFilter()
|
|
for (const prefix of ["node_modules/", "node_modules/parent/node_modules/"]) {
|
|
for (const file of [
|
|
"@zip.js/zip.js/dist/zip.js",
|
|
"@zip.js/zip.js/dist/z-worker.js",
|
|
"@zip.js/zip.js/index.cjs",
|
|
"@zip.js/zip.js/index.min.js",
|
|
"@zip.js/zip.js/index-fflate.js",
|
|
"@zip.js/zip.js/deno.json",
|
|
"@zip.js/zip.js/eslint.config.mjs",
|
|
"electron-updater/out/main.js.map",
|
|
"electron-updater/out/providers/GitHubProvider.js.map",
|
|
"builder-util-runtime/out/httpExecutor.js.map",
|
|
"lazy-val/out/main.js.map",
|
|
"ajv/lib/core.ts",
|
|
"ajv/dist/compile/index.js.map",
|
|
"ajv-formats/src/formats.ts",
|
|
"ajv-formats/dist/formats.js.map",
|
|
"js-yaml/dist/js-yaml.js",
|
|
"js-yaml/dist/js-yaml.min.js",
|
|
"js-yaml/dist/js-yaml.mjs.map",
|
|
"js-yaml/bin/js-yaml.js",
|
|
]) {
|
|
expect(filter(path.join(import.meta.dirname, prefix, file), statSync(import.meta.filename))).toBe(false)
|
|
}
|
|
for (const file of [
|
|
"@zip.js/zip.js/index.js",
|
|
"@zip.js/zip.js/lib/zip-fs.js",
|
|
"@zip.js/zip.js/lib/z-worker-inline.js",
|
|
"@zip.js/zip.js/lib/core/streams/codecs/deflate.js",
|
|
"electron-updater/out/main.js",
|
|
"electron-updater/out/MacUpdater.js",
|
|
"electron-updater/out/NsisUpdater.js",
|
|
"electron-updater/out/providers/GitHubProvider.js",
|
|
"builder-util-runtime/out/httpExecutor.js",
|
|
"lazy-val/out/main.js",
|
|
"ajv/dist/ajv.js",
|
|
"ajv/dist/refs/json-schema-draft-07.json",
|
|
"ajv-formats/dist/formats.js",
|
|
"js-yaml/index.js",
|
|
"js-yaml/lib/loader.js",
|
|
"js-yaml/dist/js-yaml.mjs",
|
|
"debug/src/index.js",
|
|
"unrelated/dist/index.js.map",
|
|
...["@zip.js/zip.js", "electron-updater", "builder-util-runtime", "ajv", "ajv-formats", "js-yaml"].flatMap(
|
|
(name) => [`${name}/package.json`, `${name}/LICENSE`],
|
|
),
|
|
]) {
|
|
expect(filter(path.join(import.meta.dirname, prefix, file), statSync(import.meta.filename))).toBe(true)
|
|
}
|
|
expect(filter(path.join(import.meta.dirname, prefix, "@zip.js/zip.js/dist"), statSync(import.meta.dirname))).toBe(
|
|
false,
|
|
)
|
|
expect(filter(path.join(import.meta.dirname, prefix, "@zip.js/zip.js/lib"), statSync(import.meta.dirname))).toBe(
|
|
true,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
test("the trimmed Zip.js package can still export compressed logs", async () => {
|
|
const config = (await import("./electron-builder.config.ts")).default
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "opencode-zip-package-"))
|
|
const source = path.dirname(fileURLToPath(import.meta.resolve("@zip.js/zip.js/package.json")))
|
|
const filter = new FileMatcher(dir, "", (value: string) => value, [
|
|
"**/*",
|
|
...(Array.isArray(config.files) ? config.files : []).filter(
|
|
(value): value is string => typeof value === "string" && value.startsWith("!"),
|
|
),
|
|
]).createFilter()
|
|
try {
|
|
await cp(source, dir, {
|
|
recursive: true,
|
|
filter: (file) =>
|
|
filter(path.join(dir, "node_modules/@zip.js/zip.js", path.relative(source, file)), statSync(file)),
|
|
})
|
|
const zip = await import(pathToFileURL(path.join(dir, "index.js")).href)
|
|
const writer = new zip.ZipWriter(new zip.BlobWriter("application/zip"))
|
|
await writer.add("desktop.log", new zip.BlobReader(new Blob(["diagnostic log\n".repeat(100)])))
|
|
const reader = new zip.ZipReader(new zip.BlobReader(await writer.close()))
|
|
const entries = await reader.getEntries()
|
|
expect(entries.map((entry: { filename: string }) => entry.filename)).toEqual(["desktop.log"])
|
|
expect(entries[0].compressionMethod).toBe(8)
|
|
expect(await entries[0].getData(new zip.TextWriter())).toBe("diagnostic log\n".repeat(100))
|
|
await reader.close()
|
|
await zip.terminateWorkers()
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test("keeps a hidden prod launcher for old Linux pins", async () => {
|
|
const previous = process.env.OPENCODE_CHANNEL
|
|
process.env.OPENCODE_CHANNEL = "prod"
|
|
|
|
const module = await import("./electron-builder.config.ts?compat=prod")
|
|
const config = module.default as Configuration
|
|
|
|
if (previous === undefined) delete process.env.OPENCODE_CHANNEL
|
|
else process.env.OPENCODE_CHANNEL = previous
|
|
|
|
expect(
|
|
config.deb?.fpm?.some((entry) =>
|
|
entry.endsWith("opencode-desktop.desktop=/usr/share/applications/opencode-desktop.desktop"),
|
|
),
|
|
).toBe(true)
|
|
expect(
|
|
config.rpm?.fpm?.some((entry) =>
|
|
entry.endsWith("opencode-desktop.desktop=/usr/share/applications/opencode-desktop.desktop"),
|
|
),
|
|
).toBe(true)
|
|
|
|
const desktop = await Bun.file(legacyDesktopEntry).text()
|
|
expect(desktop).toContain("Exec=/opt/OpenCode/ai.opencode.desktop %U")
|
|
expect(desktop).toContain("Icon=ai.opencode.desktop")
|
|
expect(desktop).toContain("StartupWMClass=ai.opencode.desktop")
|
|
expect(desktop).toContain("NoDisplay=true")
|
|
})
|
|
|
|
for (const channel of ["dev", "beta"] as const) {
|
|
test(`bundles the CLI outside the ${channel} app archive`, async () => {
|
|
const previous = process.env.OPENCODE_CHANNEL
|
|
process.env.OPENCODE_CHANNEL = channel
|
|
const module = await import(`./electron-builder.config.ts?cli-resource=${channel}`)
|
|
const config = module.default as Configuration
|
|
if (previous === undefined) delete process.env.OPENCODE_CHANNEL
|
|
else process.env.OPENCODE_CHANNEL = previous
|
|
|
|
expect(config.files).toContain("!resources/opencode-cli*")
|
|
expect(config.extraResources).toEqual([
|
|
{
|
|
from: "resources/",
|
|
to: "",
|
|
filter: ["opencode-cli*"],
|
|
},
|
|
])
|
|
})
|
|
}
|
|
|
|
test("does not bundle the CLI in prod builds", async () => {
|
|
const previous = process.env.OPENCODE_CHANNEL
|
|
process.env.OPENCODE_CHANNEL = "prod"
|
|
const module = await import("./electron-builder.config.ts?no-cli-resource=prod")
|
|
const config = module.default as Configuration
|
|
if (previous === undefined) delete process.env.OPENCODE_CHANNEL
|
|
else process.env.OPENCODE_CHANNEL = previous
|
|
|
|
expect(config.extraResources).toEqual([])
|
|
})
|