alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
316 lines (276 loc) • 8.98 kB
text/typescript
import { Alepha } from "alepha";
import { cliOptions } from "alepha/command";
import {
LogDestinationProvider,
MemoryDestinationProvider,
} from "alepha/logger";
import { describe, expect, test } from "vitest";
import {
ChangelogCommand,
changelogOptions,
GitMessageParser,
GitProvider,
} from "../commands/gen/changelog.ts";
// =============================================================================
// MOCK GIT PROVIDER
// =============================================================================
let currentMockResponses: Map<string, string> | null = null;
class MockGitProvider extends GitProvider {
override async exec(cmd: string, _cwd: string): Promise<string> {
if (!currentMockResponses) {
throw new Error("No git mocks configured");
}
const response = currentMockResponses.get(cmd);
if (response === undefined) {
throw new Error(`Unmocked git command: ${cmd}`);
}
return response;
}
}
function setMockGitResponses(
mocks: Array<{ cmd: string; response: string }>,
): void {
currentMockResponses = new Map<string, string>();
for (const { cmd, response } of mocks) {
currentMockResponses.set(cmd, response);
}
}
// =============================================================================
// TESTS
// =============================================================================
describe("changelog", () => {
describe("GitMessageParser.parseCommit", () => {
const defaultConfig = { ignore: ["chore", "ci", "build", "test"] };
const getParser = () => {
const alepha = Alepha.create();
return alepha.inject(GitMessageParser);
};
test("should parse conventional commit with scope", () => {
const parser = getParser();
const result = parser.parseCommit(
"abc12345 feat(ui): add button component",
defaultConfig,
);
expect(result).toEqual({
hash: "abc12345",
type: "feat",
scope: "ui",
description: "add button component",
breaking: false,
});
});
test("should ignore conventional commit without scope", () => {
const parser = getParser();
const result = parser.parseCommit(
"def45678 fix: resolve memory leak",
defaultConfig,
);
expect(result).toBeNull();
});
test("should detect breaking change with ! marker", () => {
const parser = getParser();
const result = parser.parseCommit(
"aaa78901 feat(api)!: remove deprecated endpoint",
defaultConfig,
);
expect(result).toEqual({
hash: "aaa78901",
type: "feat",
scope: "api",
description: "remove deprecated endpoint",
breaking: true,
});
});
test("should detect breaking change in description", () => {
const parser = getParser();
const result = parser.parseCommit(
"bbb01234 feat(auth): breaking change to token format",
defaultConfig,
);
expect(result).toEqual({
hash: "bbb01234",
type: "feat",
scope: "auth",
description: "breaking change to token format",
breaking: true,
});
});
test("should ignore commits with ignored scope", () => {
const parser = getParser();
const result = parser.parseCommit(
"ccc34567 feat(chore): update deps",
defaultConfig,
);
expect(result).toBeNull();
});
test("should ignore commits with nested ignored scope", () => {
const parser = getParser();
const result = parser.parseCommit(
"ddd67890 feat(test/unit): add tests",
defaultConfig,
);
expect(result).toBeNull();
});
test("should ignore module-style commits (no type)", () => {
const parser = getParser();
const result = parser.parseCommit("eee90123 cli: add new command", {});
expect(result).toBeNull();
});
test("should return null for non-matching commits", () => {
const parser = getParser();
const result = parser.parseCommit(
"fff78901 random commit message",
defaultConfig,
);
expect(result).toBeNull();
});
test("should truncate hash to 8 characters", () => {
const parser = getParser();
const result = parser.parseCommit(
"abcdef1234567890 feat(core): something",
defaultConfig,
);
expect(result?.hash).toBe("abcdef12");
});
test("should parse all supported types", () => {
const parser = getParser();
const types = ["feat", "fix", "docs", "refactor", "perf", "revert"];
for (const type of types) {
const result = parser.parseCommit(
`abc12345 ${type}(core): test`,
defaultConfig,
);
expect(result?.type).toBe(type);
}
});
test("should handle nested scopes", () => {
const parser = getParser();
const result = parser.parseCommit(
"abc12345 feat(api/users): add endpoint",
defaultConfig,
);
expect(result).toEqual({
hash: "abc12345",
type: "feat",
scope: "api/users",
description: "add endpoint",
breaking: false,
});
});
});
describe("changelogOptions atom", () => {
test("should have default ignore list", async () => {
const alepha = Alepha.create();
await alepha.start();
const config = alepha.store.get(changelogOptions);
expect(config.ignore).toContain("chore");
expect(config.ignore).toContain("ci");
expect(config.ignore).toContain("build");
expect(config.ignore).toContain("test");
});
test("should allow custom configuration", async () => {
const alepha = Alepha.create();
alepha.set(changelogOptions, {
ignore: ["custom", "internal"],
});
await alepha.start();
const config = alepha.store.get(changelogOptions);
expect(config.ignore).toEqual(["custom", "internal"]);
});
});
describe("changelog command", () => {
const setupCommand = async (
gitMocks: { cmd: string; response: string }[],
argv: string[] = ["changelog"],
options?: { ignore?: string[] },
) => {
setMockGitResponses(gitMocks);
const alepha = Alepha.create()
.with({
provide: LogDestinationProvider,
use: MemoryDestinationProvider,
})
.with({ provide: GitProvider, use: MockGitProvider })
.with(ChangelogCommand);
alepha.store.mut(cliOptions, (old) => ({
...old,
argv,
}));
if (options) {
alepha.set(changelogOptions, options);
}
await alepha.start();
return {
alepha,
logger: alepha.inject(MemoryDestinationProvider),
};
};
test("should show changes since latest tag", async () => {
await setupCommand([
{ cmd: "tag --sort=-version:refname", response: "1.0.0\n0.9.0\n" },
{
cmd: "log 1.0.0..HEAD --oneline",
response: [
"abc12345 feat(ui): add dashboard",
"def56789 fix(core): resolve crash",
].join("\n"),
},
]);
// Outputs to stdout - test verifies no error
});
test("should show changes from specified version", async () => {
await setupCommand(
[
{
cmd: "log 0.5.0..HEAD --oneline",
response: "abc12345 feat(api): new feature",
},
],
["changelog", "--from=0.5.0"],
);
// Outputs to stdout - test verifies no error
});
test("should handle no tags found", async () => {
await setupCommand([
{ cmd: "tag --sort=-version:refname", response: "" },
]);
// Should output "No version tags found" - no error
});
test("should handle no changes since tag", async () => {
await setupCommand([
{ cmd: "tag --sort=-version:refname", response: "1.0.0\n" },
{ cmd: "log 1.0.0..HEAD --oneline", response: "" },
]);
// Should output "No changes since 1.0.0" - no error
});
test("should filter commits by ignore list", async () => {
await setupCommand(
[
{ cmd: "tag --sort=-version:refname", response: "1.0.0\n" },
{
cmd: "log 1.0.0..HEAD --oneline",
response: [
"abc12345 feat(ui): add dashboard",
"def56789 feat(internal): should be filtered",
].join("\n"),
},
],
["changelog"],
{ ignore: ["internal"] },
);
// "feat(internal)" is filtered out
});
test("should use -f alias for --from", async () => {
await setupCommand(
[
{
cmd: "log 0.8.0..HEAD --oneline",
response: "abc12345 feat(cli): new command",
},
],
["changelog", "-f=0.8.0"],
);
// Outputs to stdout - test verifies no error
});
});
});