|
| 1 | +import { afterEach, describe, expect, test } from "bun:test" |
| 2 | +import { readFileSync } from "fs" |
| 3 | +import { join, resolve } from "path" |
| 4 | +import { Glob } from "bun" |
| 5 | +import { Installation } from "../../src/installation" |
| 6 | + |
| 7 | +const srcDir = resolve(import.meta.dir, "..", "..", "src") |
| 8 | +const fetch0 = globalThis.fetch |
| 9 | + |
| 10 | +afterEach(() => { |
| 11 | + globalThis.fetch = fetch0 |
| 12 | +}) |
| 13 | + |
| 14 | +// --------------------------------------------------------------------------- |
| 15 | +// 1. VERSION normalization |
| 16 | +// --------------------------------------------------------------------------- |
| 17 | +describe("VERSION normalization", () => { |
| 18 | + test("VERSION never starts with 'v' prefix", () => { |
| 19 | + // In test env it's "local", but the logic that produces VERSION strips "v" |
| 20 | + if (Installation.VERSION !== "local") { |
| 21 | + expect(Installation.VERSION.startsWith("v")).toBe(false) |
| 22 | + } |
| 23 | + }) |
| 24 | + |
| 25 | + test("installation/index.ts trims and strips 'v' prefix from OPENCODE_VERSION", () => { |
| 26 | + const content = readFileSync(join(srcDir, "installation", "index.ts"), "utf-8") |
| 27 | + // Verify the VERSION definition includes .trim().replace(/^v/, "") |
| 28 | + expect(content).toContain('.trim().replace(/^v/, "")') |
| 29 | + }) |
| 30 | +}) |
| 31 | + |
| 32 | +// --------------------------------------------------------------------------- |
| 33 | +// 2. Upgrade skip logic — version comparison |
| 34 | +// --------------------------------------------------------------------------- |
| 35 | +describe("upgrade version comparison", () => { |
| 36 | + test("same version from GitHub API matches (no false upgrade)", async () => { |
| 37 | + // Simulate GitHub API returning v0.4.1 → latest() returns "0.4.1" |
| 38 | + globalThis.fetch = (async () => |
| 39 | + new Response(JSON.stringify({ tag_name: "v0.4.1" }), { |
| 40 | + status: 200, |
| 41 | + headers: { "content-type": "application/json" }, |
| 42 | + })) as unknown as typeof fetch |
| 43 | + |
| 44 | + const latest = await Installation.latest("unknown") |
| 45 | + // If VERSION were "0.4.1" (normalized), they'd match → upgrade skipped |
| 46 | + expect(latest).toBe("0.4.1") |
| 47 | + // Verify no "v" prefix that would cause mismatch |
| 48 | + expect(latest.startsWith("v")).toBe(false) |
| 49 | + }) |
| 50 | + |
| 51 | + test("same version from npm API matches (no false upgrade)", async () => { |
| 52 | + globalThis.fetch = (async () => |
| 53 | + new Response(JSON.stringify({ version: "0.4.1" }), { |
| 54 | + status: 200, |
| 55 | + headers: { "content-type": "application/json" }, |
| 56 | + })) as unknown as typeof fetch |
| 57 | + |
| 58 | + const latest = await Installation.latest("npm") |
| 59 | + expect(latest).toBe("0.4.1") |
| 60 | + expect(latest.startsWith("v")).toBe(false) |
| 61 | + }) |
| 62 | + |
| 63 | + test("different version correctly triggers upgrade", async () => { |
| 64 | + globalThis.fetch = (async () => |
| 65 | + new Response(JSON.stringify({ tag_name: "v0.5.0" }), { |
| 66 | + status: 200, |
| 67 | + headers: { "content-type": "application/json" }, |
| 68 | + })) as unknown as typeof fetch |
| 69 | + |
| 70 | + const latest = await Installation.latest("unknown") |
| 71 | + expect(latest).toBe("0.5.0") |
| 72 | + // "0.4.1" !== "0.5.0" → upgrade proceeds |
| 73 | + expect("0.4.1").not.toBe(latest) |
| 74 | + }) |
| 75 | + |
| 76 | + test("auto-upgrade also uses normalized comparison", () => { |
| 77 | + // The auto-upgrade in cli/upgrade.ts uses the same Installation.VERSION |
| 78 | + const content = readFileSync(join(srcDir, "cli", "upgrade.ts"), "utf-8") |
| 79 | + expect(content).toContain("Installation.VERSION === latest") |
| 80 | + }) |
| 81 | +}) |
| 82 | + |
| 83 | +// --------------------------------------------------------------------------- |
| 84 | +// 3. User-facing strings: no stale "opencode" references |
| 85 | +// --------------------------------------------------------------------------- |
| 86 | +describe("user-facing strings use 'altimate' not 'opencode'", () => { |
| 87 | + // Patterns that indicate user-facing strings containing "opencode" where it should be "altimate" |
| 88 | + const userFacingOpencode = /(?:run|Run)\s+[`'"]opencode\s|opencode\s+upgrade|opencode\s+auth/ |
| 89 | + |
| 90 | + test("app.tsx toast uses 'altimate upgrade' not 'opencode upgrade'", () => { |
| 91 | + const content = readFileSync(join(srcDir, "cli", "cmd", "tui", "app.tsx"), "utf-8") |
| 92 | + expect(content).toContain("altimate upgrade") |
| 93 | + expect(content).not.toMatch(/opencode upgrade/) |
| 94 | + }) |
| 95 | + |
| 96 | + test("provider error messages use 'altimate' not 'opencode'", () => { |
| 97 | + const errorTs = readFileSync(join(srcDir, "provider", "error.ts"), "utf-8") |
| 98 | + expect(errorTs).not.toMatch(/`opencode auth/) |
| 99 | + expect(errorTs).toContain("`altimate auth") |
| 100 | + }) |
| 101 | + |
| 102 | + test("provider.ts uses 'altimate auth' not 'opencode auth'", () => { |
| 103 | + const content = readFileSync(join(srcDir, "provider", "provider.ts"), "utf-8") |
| 104 | + expect(content).not.toMatch(/`opencode auth/) |
| 105 | + }) |
| 106 | + |
| 107 | + test("acp/agent.ts uses 'altimate auth' not 'opencode auth'", () => { |
| 108 | + const content = readFileSync(join(srcDir, "acp", "agent.ts"), "utf-8") |
| 109 | + expect(content).not.toMatch(/`opencode auth/) |
| 110 | + expect(content).toContain("`altimate auth") |
| 111 | + }) |
| 112 | + |
| 113 | + test("acp/agent.ts terminal-auth command uses 'altimate' binary", () => { |
| 114 | + const content = readFileSync(join(srcDir, "acp", "agent.ts"), "utf-8") |
| 115 | + // The terminal-auth capability tells IDEs which command to run for auth |
| 116 | + expect(content).toMatch(/command:\s*"altimate"/) |
| 117 | + expect(content).not.toMatch(/command:\s*"opencode"/) |
| 118 | + }) |
| 119 | + |
| 120 | + test("no user-facing 'opencode' command references in src/ (broad scan)", async () => { |
| 121 | + const violations: string[] = [] |
| 122 | + const glob = new Glob("**/*.{ts,tsx}") |
| 123 | + |
| 124 | + for await (const file of glob.scan({ cwd: srcDir })) { |
| 125 | + const filePath = join(srcDir, file) |
| 126 | + const content = readFileSync(filePath, "utf-8") |
| 127 | + const lines = content.split("\n") |
| 128 | + |
| 129 | + for (let i = 0; i < lines.length; i++) { |
| 130 | + const line = lines[i] |
| 131 | + // Skip comments, imports, internal identifiers |
| 132 | + const trimmed = line.trim() |
| 133 | + if (trimmed.startsWith("//") || trimmed.startsWith("import ")) continue |
| 134 | + if (trimmed.startsWith("*")) continue // JSDoc |
| 135 | + |
| 136 | + // Check for user-facing strings like "run `opencode ...", "Run 'opencode ..." |
| 137 | + if (userFacingOpencode.test(line)) { |
| 138 | + violations.push(`${file}:${i + 1}: ${trimmed}`) |
| 139 | + } |
| 140 | + } |
| 141 | + } |
| 142 | + expect(violations).toEqual([]) |
| 143 | + }) |
| 144 | +}) |
0 commit comments