diff --git a/packages/opencode/test/util/git.test.ts b/packages/opencode/test/util/git.test.ts new file mode 100644 index 0000000000..6adb92d060 --- /dev/null +++ b/packages/opencode/test/util/git.test.ts @@ -0,0 +1,57 @@ +import { describe, test, expect } from "bun:test" +import { tmpdir } from "../fixture/fixture" +import { git } from "../../src/util/git" + +describe("git() utility", () => { + test("runs a simple git command and returns stdout", async () => { + // Use tmpdir without git:true to avoid commit signing issues; just git init manually + await using tmp = await tmpdir() + await Bun.spawn(["git", "init"], { cwd: tmp.path, stdout: "ignore", stderr: "ignore" }).exited + await Bun.spawn(["git", "config", "core.fsmonitor", "false"], { cwd: tmp.path, stdout: "ignore", stderr: "ignore" }).exited + + const result = await git(["rev-parse", "--is-inside-work-tree"], { cwd: tmp.path }) + expect(result.exitCode).toBe(0) + expect(result.text().trim()).toBe("true") + }) + + test("returns non-zero exit code for unknown git subcommand", async () => { + await using tmp = await tmpdir() + await Bun.spawn(["git", "init"], { cwd: tmp.path, stdout: "ignore", stderr: "ignore" }).exited + + const result = await git(["not-a-real-subcommand"], { cwd: tmp.path }) + expect(result.exitCode).not.toBe(0) + }) + + test("stderr is populated on error", async () => { + await using tmp = await tmpdir() + await Bun.spawn(["git", "init"], { cwd: tmp.path, stdout: "ignore", stderr: "ignore" }).exited + + const result = await git(["checkout", "nonexistent-branch-xyz"], { cwd: tmp.path }) + expect(result.exitCode).not.toBe(0) + expect(result.stderr.length).toBeGreaterThan(0) + }) + + test("passes custom env vars through to git process", async () => { + await using tmp = await tmpdir() + await Bun.spawn(["git", "init"], { cwd: tmp.path, stdout: "ignore", stderr: "ignore" }).exited + + // Use GIT_CONFIG_COUNT to inject a config value that only exists via env + const result = await git(["config", "--get", "test.injected"], { + cwd: tmp.path, + env: { + ...process.env, + GIT_CONFIG_COUNT: "1", + GIT_CONFIG_KEY_0: "test.injected", + GIT_CONFIG_VALUE_0: "from-env", + }, + }) + expect(result.exitCode).toBe(0) + expect(result.text().trim()).toBe("from-env") + }) + + test("returns exitCode 1 and empty stdout when cwd does not exist", async () => { + const result = await git(["status"], { cwd: "/tmp/nonexistent-dir-" + Math.random().toString(36) }) + expect(result.exitCode).not.toBe(0) + expect(result.stdout.length).toBe(0) + }) +}) diff --git a/packages/opencode/test/util/keybind.test.ts b/packages/opencode/test/util/keybind.test.ts new file mode 100644 index 0000000000..708131f1ed --- /dev/null +++ b/packages/opencode/test/util/keybind.test.ts @@ -0,0 +1,154 @@ +import { describe, test, expect } from "bun:test" +import { Keybind } from "../../src/util/keybind" + +describe("Keybind.parse", () => { + test("returns empty array for 'none'", () => { + expect(Keybind.parse("none")).toEqual([]) + }) + + test("parses simple key", () => { + const [info] = Keybind.parse("a") + expect(info.name).toBe("a") + expect(info.ctrl).toBe(false) + expect(info.meta).toBe(false) + expect(info.shift).toBe(false) + expect(info.leader).toBe(false) + }) + + test("parses ctrl+key combo", () => { + const [info] = Keybind.parse("ctrl+s") + expect(info.ctrl).toBe(true) + expect(info.name).toBe("s") + }) + + test("parses multi-modifier combo ctrl+shift+a", () => { + const [info] = Keybind.parse("ctrl+shift+a") + expect(info.ctrl).toBe(true) + expect(info.shift).toBe(true) + expect(info.name).toBe("a") + }) + + test("recognizes 'alt', 'meta', and 'option' as meta modifier", () => { + for (const alias of ["alt", "meta", "option"]) { + const [info] = Keybind.parse(`${alias}+x`) + expect(info.meta).toBe(true) + expect(info.name).toBe("x") + } + }) + + test("parses super modifier", () => { + const [info] = Keybind.parse("super+s") + expect(info.super).toBe(true) + expect(info.name).toBe("s") + }) + + test("parses prefix", () => { + const [info] = Keybind.parse("a") + expect(info.leader).toBe(true) + expect(info.name).toBe("a") + }) + + test("normalizes 'esc' to 'escape'", () => { + const [info] = Keybind.parse("esc") + expect(info.name).toBe("escape") + }) + + test("parses comma-separated multi-binding", () => { + const bindings = Keybind.parse("ctrl+a,ctrl+b") + expect(bindings).toHaveLength(2) + expect(bindings[0].name).toBe("a") + expect(bindings[0].ctrl).toBe(true) + expect(bindings[1].name).toBe("b") + expect(bindings[1].ctrl).toBe(true) + }) +}) + +describe("Keybind.toString", () => { + test("returns empty string for undefined", () => { + expect(Keybind.toString(undefined)).toBe("") + }) + + test("formats ctrl+key", () => { + const result = Keybind.toString({ + ctrl: true, meta: false, shift: false, super: false, leader: false, name: "s", + }) + expect(result).toBe("ctrl+s") + }) + + test("formats meta as 'alt'", () => { + const result = Keybind.toString({ + ctrl: false, meta: true, shift: false, super: false, leader: false, name: "x", + }) + expect(result).toBe("alt+x") + }) + + test("formats super modifier", () => { + const result = Keybind.toString({ + ctrl: false, meta: false, shift: false, super: true, leader: false, name: "s", + }) + expect(result).toBe("super+s") + }) + + test("formats leader prefix with key", () => { + const result = Keybind.toString({ + ctrl: false, meta: false, shift: false, super: false, leader: true, name: "a", + }) + expect(result).toBe(" a") + }) + + test("formats leader-only (no key)", () => { + const result = Keybind.toString({ + ctrl: false, meta: false, shift: false, super: false, leader: true, name: "", + }) + expect(result).toBe("") + }) + + test("maps 'delete' to 'del'", () => { + const result = Keybind.toString({ + ctrl: false, meta: false, shift: false, super: false, leader: false, name: "delete", + }) + expect(result).toBe("del") + }) +}) + +describe("Keybind.match", () => { + test("returns false for undefined first argument", () => { + const b: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "s" } + expect(Keybind.match(undefined, b)).toBe(false) + }) + + test("matches identical bindings", () => { + const a: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "s" } + const b: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "s" } + expect(Keybind.match(a, b)).toBe(true) + }) + + test("treats missing super as false (normalization)", () => { + // Simulate an Info object where super is undefined (e.g. from older code) + const a = { ctrl: false, meta: false, shift: false, leader: false, name: "a" } as Keybind.Info + const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: false, leader: false, name: "a" } + expect(Keybind.match(a, b)).toBe(true) + }) + + test("does not match different keys", () => { + const a: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "s" } + const b: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "x" } + expect(Keybind.match(a, b)).toBe(false) + }) +}) + +describe("Keybind.parse → Keybind.toString roundtrip", () => { + test("roundtrips ctrl+shift+a", () => { + const [parsed] = Keybind.parse("ctrl+shift+a") + expect(Keybind.toString(parsed)).toBe("ctrl+shift+a") + }) + + test("meta aliases normalize to 'alt' on roundtrip", () => { + // parse("meta+x") sets meta:true, toString emits "alt" — lossy but correct + const [parsed] = Keybind.parse("meta+x") + expect(Keybind.toString(parsed)).toBe("alt+x") + + const [parsed2] = Keybind.parse("option+x") + expect(Keybind.toString(parsed2)).toBe("alt+x") + }) +})