Skip to content
Merged
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
146 changes: 145 additions & 1 deletion tests/commands/adr/create.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { Command } from "@commander-js/extra-typings";

Expand Down Expand Up @@ -35,3 +38,144 @@ describe("registerAdrCreateCommand", () => {
expect(domainOpt).toBeDefined();
});
});

describe("adr create action handler", () => {
let tempDir: string;
let originalCwd: string;
let logSpy: ReturnType<typeof spyOn>;
let exitSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "archgate-create-test-"));
originalCwd = process.cwd();
logSpy = spyOn(console, "log").mockImplementation(() => {});
exitSpy = spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit");
});
});

afterEach(() => {
process.chdir(originalCwd);
rmSync(tempDir, { recursive: true, force: true });
logSpy.mockRestore();
exitSpy.mockRestore();
});

function makeProgram(): Command {
const parent = new Command("adr").exitOverride();
registerAdrCreateCommand(parent);
return parent;
}

test("creates ADR non-interactively with --title and --domain", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync([
"node",
"adr",
"create",
"--title",
"Use PostgreSQL",
"--domain",
"backend",
]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
expect(allOutput).toContain("Created ADR:");
expect(allOutput).toContain("BE-001");
});

test("creates ADR and outputs file on disk", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync([
"node",
"adr",
"create",
"--title",
"Use Redis",
"--domain",
"backend",
]);

const createdFile = join(adrsDir, "BE-001-use-redis.md");
expect(existsSync(createdFile)).toBe(true);
});

test("outputs JSON when --json flag is passed", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync([
"node",
"adr",
"create",
"--title",
"Use Kafka",
"--domain",
"data",
"--json",
]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
const parsed = JSON.parse(allOutput);
expect(parsed.id).toBe("DATA-001");
expect(parsed.fileName).toContain("DATA-001");
expect(parsed.filePath).toBeTruthy();
});

test("creates ADR with custom body via --body option", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync([
"node",
"adr",
"create",
"--title",
"Use GraphQL",
"--domain",
"frontend",
"--body",
"## Context\nWe need a flexible API.",
]);

const createdFile = join(adrsDir, "FE-001-use-graphql.md");
expect(existsSync(createdFile)).toBe(true);
const content = await Bun.file(createdFile).text();
expect(content).toContain("We need a flexible API.");
});

test("exits with error when .archgate/ directory is missing", async () => {
process.chdir(tempDir);
const parent = makeProgram();

await expect(
parent.parseAsync([
"node",
"adr",
"create",
"--title",
"Test",
"--domain",
"general",
])
).rejects.toThrow("process.exit");

expect(exitSpy).toHaveBeenCalledWith(1);
});
});
186 changes: 185 additions & 1 deletion tests/commands/adr/list.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { Command } from "@commander-js/extra-typings";

import { registerAdrListCommand } from "../../../src/commands/adr/list";

const ADR_CONTENT_1 = `---
id: ARCH-001
title: Use TypeScript
domain: architecture
rules: false
---

## Context
We need a type-safe language.
`;

const ADR_CONTENT_2 = `---
id: GEN-001
title: Use Conventional Commits
domain: general
rules: true
---

## Context
We need consistent commit messages.
`;

describe("registerAdrListCommand", () => {
test("registers 'list' as a subcommand", () => {
const parent = new Command("adr");
Expand Down Expand Up @@ -35,3 +60,162 @@ describe("registerAdrListCommand", () => {
expect(domainOpt).toBeDefined();
});
});

describe("adr list action handler", () => {
let tempDir: string;
let originalCwd: string;
let logSpy: ReturnType<typeof spyOn>;
let exitSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "archgate-list-test-"));
originalCwd = process.cwd();
logSpy = spyOn(console, "log").mockImplementation(() => {});
exitSpy = spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit");
});
});

afterEach(() => {
process.chdir(originalCwd);
rmSync(tempDir, { recursive: true, force: true });
logSpy.mockRestore();
exitSpy.mockRestore();
});

function makeProgram(): Command {
const parent = new Command("adr").exitOverride();
registerAdrListCommand(parent);
return parent;
}

test("lists ADRs from .archgate/adrs/ directory in table format", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });
writeFileSync(join(adrsDir, "ARCH-001-use-typescript.md"), ADR_CONTENT_1);
writeFileSync(
join(adrsDir, "GEN-001-use-conventional-commits.md"),
ADR_CONTENT_2
);

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync(["node", "adr", "list"]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
expect(allOutput).toContain("ARCH-001");
expect(allOutput).toContain("GEN-001");
});

test("outputs JSON when --json flag is passed", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });
writeFileSync(join(adrsDir, "ARCH-001-use-typescript.md"), ADR_CONTENT_1);

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync(["node", "adr", "list", "--json"]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
const parsed = JSON.parse(allOutput);
expect(Array.isArray(parsed)).toBe(true);
expect(parsed[0].id).toBe("ARCH-001");
expect(parsed[0].domain).toBe("architecture");
});

test("filters by domain with --domain flag", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });
writeFileSync(join(adrsDir, "ARCH-001-use-typescript.md"), ADR_CONTENT_1);
writeFileSync(
join(adrsDir, "GEN-001-use-conventional-commits.md"),
ADR_CONTENT_2
);

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync([
"node",
"adr",
"list",
"--domain",
"architecture",
]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
expect(allOutput).toContain("ARCH-001");
expect(allOutput).not.toContain("GEN-001");
});

test("combines --domain and --json filters", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });
writeFileSync(join(adrsDir, "ARCH-001-use-typescript.md"), ADR_CONTENT_1);
writeFileSync(
join(adrsDir, "GEN-001-use-conventional-commits.md"),
ADR_CONTENT_2
);

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync([
"node",
"adr",
"list",
"--domain",
"general",
"--json",
]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
const parsed = JSON.parse(allOutput);
expect(parsed).toHaveLength(1);
expect(parsed[0].id).toBe("GEN-001");
});

test("prints 'No ADRs found.' when adrs directory is empty", async () => {
const adrsDir = join(tempDir, ".archgate", "adrs");
mkdirSync(adrsDir, { recursive: true });

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync(["node", "adr", "list"]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
expect(allOutput).toContain("No ADRs found.");
});

test("prints 'No ADRs found.' when adrs directory does not exist", async () => {
mkdirSync(join(tempDir, ".archgate"), { recursive: true });

process.chdir(tempDir);
const parent = makeProgram();
await parent.parseAsync(["node", "adr", "list"]);

const allOutput = logSpy.mock.calls
.map((c: unknown[]) => String(c[0]))
.join("\n");
expect(allOutput).toContain("No ADRs found.");
});

test("exits with error when .archgate/ directory is missing", async () => {
process.chdir(tempDir);
const parent = makeProgram();

await expect(parent.parseAsync(["node", "adr", "list"])).rejects.toThrow(
"process.exit"
);

expect(exitSpy).toHaveBeenCalledWith(1);
});
});
Loading
Loading