Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions tests/args.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import test from "node:test";
import assert from "node:assert/strict";

import { parseArgs, splitRawArgumentString } from "../plugins/codex/scripts/lib/args.mjs";

// ---------------------------------------------------------------------------
// parseArgs
// ---------------------------------------------------------------------------

test("parseArgs collects bare positional tokens", () => {
const { options, positionals } = parseArgs(["foo", "bar", "baz"]);
assert.deepEqual(options, {});
assert.deepEqual(positionals, ["foo", "bar", "baz"]);
});

test("parseArgs recognises a boolean flag", () => {
const { options, positionals } = parseArgs(["--wait"], {
booleanOptions: ["wait"]
});
assert.equal(options.wait, true);
assert.deepEqual(positionals, []);
});

test("parseArgs recognises an inline-value boolean flag set to false", () => {
const { options } = parseArgs(["--wait=false"], {
booleanOptions: ["wait"]
});
assert.equal(options.wait, false);
});

test("parseArgs recognises an inline-value boolean flag set to a non-false string", () => {
const { options } = parseArgs(["--wait=yes"], {
booleanOptions: ["wait"]
});
assert.equal(options.wait, true);
});

test("parseArgs reads a value option from the next token", () => {
const { options, positionals } = parseArgs(["--base", "main", "extra"], {
valueOptions: ["base"]
});
assert.equal(options.base, "main");
assert.deepEqual(positionals, ["extra"]);
});

test("parseArgs reads a value option supplied as --key=value", () => {
const { options } = parseArgs(["--base=main"], {
valueOptions: ["base"]
});
assert.equal(options.base, "main");
});

test("parseArgs throws when a value option is missing its argument", () => {
assert.throws(
() => parseArgs(["--base"], { valueOptions: ["base"] }),
/Missing value for --base/
);
});

test("parseArgs resolves long-option aliases", () => {
const { options } = parseArgs(["--bg"], {
booleanOptions: ["background"],
aliasMap: { bg: "background" }
});
assert.equal(options.background, true);
});

test("parseArgs handles a short boolean flag via aliasMap", () => {
const { options } = parseArgs(["-w"], {
booleanOptions: ["wait"],
aliasMap: { w: "wait" }
});
assert.equal(options.wait, true);
});

test("parseArgs handles a short value flag via aliasMap", () => {
const { options } = parseArgs(["-b", "main"], {
valueOptions: ["base"],
aliasMap: { b: "base" }
});
assert.equal(options.base, "main");
});

test("parseArgs throws when a short value flag is missing its argument", () => {
assert.throws(
() => parseArgs(["-b"], { valueOptions: ["base"], aliasMap: { b: "base" } }),
/Missing value for -b/
);
});

test("parseArgs treats everything after -- as positional", () => {
const { options, positionals } = parseArgs(["--wait", "--", "--not-a-flag", "pos"], {
booleanOptions: ["wait"]
});
assert.equal(options.wait, true);
assert.deepEqual(positionals, ["--not-a-flag", "pos"]);
});

test("parseArgs treats unknown long flags as positionals", () => {
const { positionals } = parseArgs(["--unknown-flag"]);
assert.deepEqual(positionals, ["--unknown-flag"]);
});

test("parseArgs treats a bare - as a positional (stdin convention)", () => {
const { positionals } = parseArgs(["-"]);
assert.deepEqual(positionals, ["-"]);
});

test("parseArgs handles multiple flags and positionals together", () => {
const { options, positionals } = parseArgs(
["--scope", "working-tree", "--wait", "src/app.js"],
{ valueOptions: ["scope"], booleanOptions: ["wait"] }
);
assert.equal(options.scope, "working-tree");
assert.equal(options.wait, true);
assert.deepEqual(positionals, ["src/app.js"]);
});

// ---------------------------------------------------------------------------
// splitRawArgumentString
// ---------------------------------------------------------------------------

test("splitRawArgumentString splits on whitespace", () => {
assert.deepEqual(splitRawArgumentString("foo bar baz"), ["foo", "bar", "baz"]);
});

test("splitRawArgumentString ignores leading and trailing whitespace", () => {
assert.deepEqual(splitRawArgumentString(" foo bar "), ["foo", "bar"]);
});

test("splitRawArgumentString handles double-quoted tokens with spaces", () => {
assert.deepEqual(splitRawArgumentString('"hello world" next'), ["hello world", "next"]);
});

test("splitRawArgumentString handles single-quoted tokens with spaces", () => {
assert.deepEqual(splitRawArgumentString("'hello world' next"), ["hello world", "next"]);
});

test("splitRawArgumentString handles backslash-escaped spaces", () => {
assert.deepEqual(splitRawArgumentString("hello\\ world next"), ["hello world", "next"]);
});

test("splitRawArgumentString handles trailing backslash as literal backslash", () => {
assert.deepEqual(splitRawArgumentString("foo\\"), ["foo\\"]);
});

test("splitRawArgumentString returns empty array for empty string", () => {
assert.deepEqual(splitRawArgumentString(""), []);
});

test("splitRawArgumentString returns empty array for whitespace-only string", () => {
assert.deepEqual(splitRawArgumentString(" "), []);
});

test("splitRawArgumentString handles adjacent quoted tokens", () => {
assert.deepEqual(splitRawArgumentString('"a b""c d"'), ["a bc d"]);
});

test("splitRawArgumentString handles backslash-escaped quote inside unquoted token", () => {
assert.deepEqual(splitRawArgumentString('say\\"hi'), ['say"hi']);
});
47 changes: 47 additions & 0 deletions tests/broker-endpoint.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,50 @@ test("createBrokerEndpoint uses named pipes on Windows", () => {
path: "\\\\.\\pipe\\cxc-12345-codex-app-server"
});
});

// ---------------------------------------------------------------------------
// parseBrokerEndpoint — error cases
// ---------------------------------------------------------------------------

test("parseBrokerEndpoint throws for an empty string", () => {
assert.throws(() => parseBrokerEndpoint(""), /Missing broker endpoint/);
});

test("parseBrokerEndpoint throws for a null/undefined value", () => {
assert.throws(() => parseBrokerEndpoint(null), /Missing broker endpoint/);
assert.throws(() => parseBrokerEndpoint(undefined), /Missing broker endpoint/);
});

test("parseBrokerEndpoint throws for an unsupported scheme", () => {
assert.throws(
() => parseBrokerEndpoint("tcp://localhost:1234"),
/Unsupported broker endpoint/
);
});

test("parseBrokerEndpoint throws when a pipe: endpoint has no path", () => {
assert.throws(
() => parseBrokerEndpoint("pipe:"),
/Broker pipe endpoint is missing its path/
);
});

test("parseBrokerEndpoint throws when a unix: endpoint has no path", () => {
assert.throws(
() => parseBrokerEndpoint("unix:"),
/Broker Unix socket endpoint is missing its path/
);
});

// ---------------------------------------------------------------------------
// createBrokerEndpoint — Linux platform
// ---------------------------------------------------------------------------

test("createBrokerEndpoint uses Unix socket on Linux", () => {
const endpoint = createBrokerEndpoint("/tmp/cxc-linux", "linux");
assert.equal(endpoint, "unix:/tmp/cxc-linux/broker.sock");
assert.deepEqual(parseBrokerEndpoint(endpoint), {
kind: "unix",
path: "/tmp/cxc-linux/broker.sock"
});
});
127 changes: 127 additions & 0 deletions tests/fs.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import test from "node:test";
import assert from "node:assert/strict";

import {
ensureAbsolutePath,
createTempDir,
readJsonFile,
writeJsonFile,
safeReadFile,
isProbablyText
} from "../plugins/codex/scripts/lib/fs.mjs";

import { makeTempDir } from "./helpers.mjs";

// ---------------------------------------------------------------------------
// ensureAbsolutePath
// ---------------------------------------------------------------------------

test("ensureAbsolutePath returns an absolute path unchanged", () => {
const absolute = "/usr/local/bin/codex";
assert.equal(ensureAbsolutePath("/any/cwd", absolute), absolute);
});

test("ensureAbsolutePath resolves a relative path against cwd", () => {
const cwd = makeTempDir();
const result = ensureAbsolutePath(cwd, "subdir/file.txt");
assert.equal(result, path.join(cwd, "subdir", "file.txt"));
assert.equal(path.isAbsolute(result), true);
});

test("ensureAbsolutePath resolves a dot-relative path", () => {
const cwd = makeTempDir();
const result = ensureAbsolutePath(cwd, "./file.txt");
assert.equal(result, path.resolve(cwd, "file.txt"));
});

// ---------------------------------------------------------------------------
// createTempDir
// ---------------------------------------------------------------------------

test("createTempDir creates an existing directory under os.tmpdir()", () => {
const dir = createTempDir();
assert.equal(fs.existsSync(dir), true);
assert.equal(fs.statSync(dir).isDirectory(), true);
assert.ok(dir.startsWith(os.tmpdir()));
fs.rmdirSync(dir);
});

test("createTempDir respects a custom prefix", () => {
const dir = createTempDir("my-prefix-");
assert.ok(path.basename(dir).startsWith("my-prefix-"));
fs.rmdirSync(dir);
});

// ---------------------------------------------------------------------------
// readJsonFile / writeJsonFile
// ---------------------------------------------------------------------------

test("writeJsonFile writes valid JSON and readJsonFile round-trips it", () => {
const dir = makeTempDir();
const filePath = path.join(dir, "data.json");
const payload = { hello: "world", count: 42, nested: { ok: true } };

writeJsonFile(filePath, payload);
const content = fs.readFileSync(filePath, "utf8");
assert.ok(content.endsWith("\n"), "file should end with a newline");

const parsed = readJsonFile(filePath);
assert.deepEqual(parsed, payload);
});

test("readJsonFile throws on malformed JSON", () => {
const dir = makeTempDir();
const filePath = path.join(dir, "bad.json");
fs.writeFileSync(filePath, "{ not valid json }", "utf8");
assert.throws(() => readJsonFile(filePath));
});

// ---------------------------------------------------------------------------
// safeReadFile
// ---------------------------------------------------------------------------

test("safeReadFile returns the file contents when the file exists", () => {
const dir = makeTempDir();
const filePath = path.join(dir, "hello.txt");
fs.writeFileSync(filePath, "hello codex\n", "utf8");
assert.equal(safeReadFile(filePath), "hello codex\n");
});

test("safeReadFile returns empty string when the file does not exist", () => {
const dir = makeTempDir();
const missing = path.join(dir, "missing.txt");
assert.equal(safeReadFile(missing), "");
});

// ---------------------------------------------------------------------------
// isProbablyText
// ---------------------------------------------------------------------------

test("isProbablyText returns true for an ASCII text buffer", () => {
const buffer = Buffer.from("Hello, world!\n", "utf8");
assert.equal(isProbablyText(buffer), true);
});

test("isProbablyText returns true for a UTF-8 text buffer without null bytes", () => {
const buffer = Buffer.from("こんにちは世界\n", "utf8");
assert.equal(isProbablyText(buffer), true);
});

test("isProbablyText returns false for a buffer containing a null byte", () => {
const buffer = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00, 0x00]);
assert.equal(isProbablyText(buffer), false);
});

test("isProbablyText returns true for an empty buffer", () => {
assert.equal(isProbablyText(Buffer.alloc(0)), true);
});

test("isProbablyText only samples the first 4096 bytes", () => {
// Build a buffer that has a null byte only beyond the 4096-byte sample
const safe = Buffer.alloc(4096, 0x61); // 'a' x 4096
const withNull = Buffer.concat([safe, Buffer.from([0x00])]);
assert.equal(isProbablyText(withNull), true);
});
Loading