alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
570 lines (472 loc) • 18.4 kB
text/typescript
import { Alepha } from "alepha";
import {
FileSystemProvider,
MemoryFileSystemProvider,
MemoryShellProvider,
ShellProvider,
} from "alepha/system";
import { describe, it } from "vitest";
import { VendorService } from "../services/VendorService.ts";
describe("VendorService", () => {
const createTestEnv = () => {
const alepha = Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.with({ provide: FileSystemProvider, use: MemoryFileSystemProvider });
const service = alepha.inject(VendorService);
const shell = alepha.inject(MemoryShellProvider);
const fs = alepha.inject(MemoryFileSystemProvider);
return { service, shell, fs };
};
/**
* Helper to create a test service with a stubbed clone that returns
* a known directory, plus stubs getCommitHash to return a fixed hash.
*/
const createTestService = (shell: MemoryShellProvider) => {
class TestVendorService extends VendorService {
protected override async cloneRemote(): Promise<string> {
await shell.run(
"git clone --depth 1 --branch main --filter=blob:none remote /tmp/test-clone",
);
return "/tmp/test-clone";
}
protected override async cloneAtCommit(): Promise<string> {
return "/tmp/test-baseline";
}
protected override async getCommitHash(): Promise<string> {
return "abc123";
}
}
const alepha = Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.with({ provide: FileSystemProvider, use: MemoryFileSystemProvider });
return {
service: alepha.inject(TestVendorService),
fs: alepha.inject(MemoryFileSystemProvider),
};
};
describe("sync", () => {
it("should clone the remote repository", async ({ expect }) => {
const { service, shell, fs } = createTestEnv();
await fs.mkdir("/tmp/.alepha-vendor-fake/packages/my-pkg", {
recursive: true,
});
await fs.writeFile(
"/tmp/.alepha-vendor-fake/packages/my-pkg/index.ts",
"export {}",
);
await service.sync({
root: "/project",
remote: "git@github.com:user/repo.git",
branch: "main",
dir: "packages",
packages: ["my-pkg"],
});
expect(
shell.wasCalledMatching(
/git clone --depth 1 --branch main --filter=blob:none git@github.com:user\/repo\.git/,
),
).toBe(true);
});
it("should remove local package dir before copying", async ({ expect }) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
await fs.mkdir("/tmp/test-clone/packages/my-pkg", {
recursive: true,
});
await fs.writeFile(
"/tmp/test-clone/packages/my-pkg/index.ts",
"export {}",
);
await fs.mkdir("/project/packages/my-pkg", { recursive: true });
await fs.writeFile("/project/packages/my-pkg/old-file.ts", "old");
const result = await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["my-pkg"],
force: true,
});
expect(result.synced).toEqual(["my-pkg"]);
expect(result.errors).toEqual([]);
expect(fs.wasDeleted("/project/packages/my-pkg")).toBe(true);
});
it("should report errors for missing remote packages", async ({
expect,
}) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
await fs.mkdir("/tmp/test-clone/packages", { recursive: true });
const result = await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["missing-pkg"],
});
expect(result.synced).toEqual([]);
expect(result.errors).toEqual([
'Package "missing-pkg" not found in remote',
]);
});
it("should sync multiple packages", async ({ expect }) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
await fs.mkdir("/tmp/test-clone/packages/pkg-a", { recursive: true });
await fs.writeFile("/tmp/test-clone/packages/pkg-a/index.ts", "a");
await fs.mkdir("/tmp/test-clone/packages/pkg-b", { recursive: true });
await fs.writeFile("/tmp/test-clone/packages/pkg-b/index.ts", "b");
const result = await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg-a", "pkg-b"],
force: true,
});
expect(result.synced).toEqual(["pkg-a", "pkg-b"]);
expect(result.errors).toEqual([]);
});
it("should clean up temp directory even on error", async ({ expect }) => {
class TestVendorService extends VendorService {
protected override async cloneRemote(): Promise<string> {
return "/tmp/test-clone";
}
protected override async getCommitHash(): Promise<string> {
return "abc123";
}
}
const alepha = Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.with({ provide: FileSystemProvider, use: MemoryFileSystemProvider });
const testService = alepha.inject(TestVendorService);
const testFs = alepha.inject(MemoryFileSystemProvider);
await testFs.mkdir("/tmp/test-clone/packages/pkg-a", { recursive: true });
await testFs.writeFile("/tmp/test-clone/packages/pkg-a/index.ts", "a");
/**
* Make cp throw to simulate a failure mid-sync.
*/
const originalCp = testFs.cp.bind(testFs);
testFs.cp = async () => {
throw new Error("disk full");
};
await expect(
testService.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg-a"],
force: true,
}),
).rejects.toThrow("disk full");
expect(testFs.wasDeleted("/tmp/test-clone")).toBe(true);
testFs.cp = originalCp;
});
it("should write vendor.json after successful sync", async ({ expect }) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
await fs.mkdir("/tmp/test-clone/packages/my-pkg", { recursive: true });
await fs.writeFile("/tmp/test-clone/packages/my-pkg/index.ts", "code");
await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["my-pkg"],
});
expect(fs.wasWritten("/project/packages/vendor.json")).toBe(true);
const content = await fs.readFile("/project/packages/vendor.json");
const lock = JSON.parse(content.toString());
expect(lock.commit).toBe("abc123");
expect(lock.remote).toBe("remote");
});
it("should skip modification check on first sync (no vendor.json)", async ({
expect,
}) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
await fs.mkdir("/tmp/test-clone/packages/my-pkg", { recursive: true });
await fs.writeFile("/tmp/test-clone/packages/my-pkg/index.ts", "code");
const result = await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["my-pkg"],
});
expect(result.synced).toEqual(["my-pkg"]);
expect(result.aborted).toBeUndefined();
});
it("should abort when local modifications detected against baseline", async ({
expect,
}) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
// Baseline (last synced state)
await fs.mkdir("/tmp/test-baseline/packages/my-pkg", { recursive: true });
await fs.writeFile(
"/tmp/test-baseline/packages/my-pkg/index.ts",
"original",
);
// Local (user modified)
await fs.mkdir("/project/packages/my-pkg", { recursive: true });
await fs.writeFile("/project/packages/my-pkg/index.ts", "modified");
// Remote (latest)
await fs.mkdir("/tmp/test-clone/packages/my-pkg", { recursive: true });
await fs.writeFile("/tmp/test-clone/packages/my-pkg/index.ts", "latest");
// Vendor lock exists from a previous sync
await fs.mkdir("/project/packages", { recursive: true });
await fs.writeFile(
"/project/packages/vendor.json",
JSON.stringify({ remote: "remote", commit: "old-hash" }),
);
const result = await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["my-pkg"],
});
expect(result.aborted).toBeDefined();
expect(result.synced).toEqual([]);
});
it("should sync when local matches baseline", async ({ expect }) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
// Baseline and local are identical
await fs.mkdir("/tmp/test-baseline/packages/my-pkg", { recursive: true });
await fs.writeFile("/tmp/test-baseline/packages/my-pkg/index.ts", "same");
await fs.mkdir("/project/packages/my-pkg", { recursive: true });
await fs.writeFile("/project/packages/my-pkg/index.ts", "same");
// Remote has updates
await fs.mkdir("/tmp/test-clone/packages/my-pkg", { recursive: true });
await fs.writeFile("/tmp/test-clone/packages/my-pkg/index.ts", "updated");
await fs.mkdir("/project/packages", { recursive: true });
await fs.writeFile(
"/project/packages/vendor.json",
JSON.stringify({ remote: "remote", commit: "old-hash" }),
);
const result = await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["my-pkg"],
});
expect(result.synced).toEqual(["my-pkg"]);
expect(result.aborted).toBeUndefined();
});
it("should skip modification check with --force even when vendor.json exists", async ({
expect,
}) => {
const { service, fs } = createTestService(
Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.inject(MemoryShellProvider),
);
// Local has modifications
await fs.mkdir("/project/packages/my-pkg", { recursive: true });
await fs.writeFile("/project/packages/my-pkg/index.ts", "modified");
// Remote
await fs.mkdir("/tmp/test-clone/packages/my-pkg", { recursive: true });
await fs.writeFile("/tmp/test-clone/packages/my-pkg/index.ts", "latest");
await fs.mkdir("/project/packages", { recursive: true });
await fs.writeFile(
"/project/packages/vendor.json",
JSON.stringify({ remote: "remote", commit: "old-hash" }),
);
const result = await service.sync({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["my-pkg"],
force: true,
});
expect(result.synced).toEqual(["my-pkg"]);
expect(result.aborted).toBeUndefined();
});
});
describe("diff", () => {
const createDiffTestService = () => {
class TestVendorService extends VendorService {
protected override async cloneAtCommit(): Promise<string> {
return "/tmp/test-baseline";
}
}
const alepha = Alepha.create()
.with({ provide: ShellProvider, use: MemoryShellProvider })
.with({ provide: FileSystemProvider, use: MemoryFileSystemProvider });
return {
service: alepha.inject(TestVendorService),
fs: alepha.inject(MemoryFileSystemProvider),
};
};
const writeVendorLock = async (fs: MemoryFileSystemProvider) => {
await fs.mkdir("/project/packages", { recursive: true });
await fs.writeFile(
"/project/packages/vendor.json",
JSON.stringify({ remote: "remote", commit: "abc123" }),
);
};
it("should return no changes when no vendor.json exists", async ({
expect,
}) => {
const { service } = createDiffTestService();
const result = await service.diff({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg"],
});
expect(result.packages).toEqual([]);
expect(result.totalChanges).toBe(0);
});
it("should detect locally added files", async ({ expect }) => {
const { service, fs } = createDiffTestService();
await writeVendorLock(fs);
// Baseline (last synced state)
await fs.mkdir("/tmp/test-baseline/packages/pkg", { recursive: true });
await fs.writeFile("/tmp/test-baseline/packages/pkg/shared.ts", "same");
// Local (user added a file)
await fs.mkdir("/project/packages/pkg", { recursive: true });
await fs.writeFile("/project/packages/pkg/shared.ts", "same");
await fs.writeFile("/project/packages/pkg/local-only.ts", "local");
const result = await service.diff({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg"],
});
expect(result.packages[0].added).toEqual(["local-only.ts"]);
expect(result.totalChanges).toBe(1);
});
it("should detect locally modified files", async ({ expect }) => {
const { service, fs } = createDiffTestService();
await writeVendorLock(fs);
await fs.mkdir("/tmp/test-baseline/packages/pkg", { recursive: true });
await fs.writeFile("/tmp/test-baseline/packages/pkg/file.ts", "original");
await fs.mkdir("/project/packages/pkg", { recursive: true });
await fs.writeFile("/project/packages/pkg/file.ts", "modified by user");
const result = await service.diff({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg"],
});
expect(result.packages[0].modified).toHaveLength(1);
expect(result.packages[0].modified[0].file).toBe("file.ts");
expect(result.packages[0].modified[0].changes).toEqual([
{ line: 1, type: "removed", text: "original" },
{ line: 1, type: "added", text: "modified by user" },
]);
expect(result.totalChanges).toBe(1);
});
it("should detect locally removed files", async ({ expect }) => {
const { service, fs } = createDiffTestService();
await writeVendorLock(fs);
// Baseline had two files
await fs.mkdir("/tmp/test-baseline/packages/pkg", { recursive: true });
await fs.writeFile("/tmp/test-baseline/packages/pkg/kept.ts", "same");
await fs.writeFile("/tmp/test-baseline/packages/pkg/deleted.ts", "gone");
// Local only has one
await fs.mkdir("/project/packages/pkg", { recursive: true });
await fs.writeFile("/project/packages/pkg/kept.ts", "same");
const result = await service.diff({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg"],
});
expect(result.packages[0].removed).toEqual(["deleted.ts"]);
expect(result.totalChanges).toBe(1);
});
it("should report no changes when local matches baseline", async ({
expect,
}) => {
const { service, fs } = createDiffTestService();
await writeVendorLock(fs);
await fs.mkdir("/tmp/test-baseline/packages/pkg", { recursive: true });
await fs.writeFile("/tmp/test-baseline/packages/pkg/file.ts", "same");
await fs.mkdir("/project/packages/pkg", { recursive: true });
await fs.writeFile("/project/packages/pkg/file.ts", "same");
const result = await service.diff({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg"],
});
expect(result.totalChanges).toBe(0);
});
it("should include line-level changes for multi-line modifications", async ({
expect,
}) => {
const { service, fs } = createDiffTestService();
await writeVendorLock(fs);
await fs.mkdir("/tmp/test-baseline/packages/pkg", { recursive: true });
await fs.writeFile(
"/tmp/test-baseline/packages/pkg/config.ts",
"const a = 1;\nconst b = 2;\nconst c = 3;",
);
await fs.mkdir("/project/packages/pkg", { recursive: true });
await fs.writeFile(
"/project/packages/pkg/config.ts",
"const a = 1;\nconst b = 99;\nconst c = 3;\nconst d = 4;",
);
const result = await service.diff({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: ["pkg"],
});
const fileDiff = result.packages[0].modified[0];
expect(fileDiff.file).toBe("config.ts");
expect(fileDiff.changes).toEqual([
{ line: 2, type: "removed", text: "const b = 2;" },
{ line: 2, type: "added", text: "const b = 99;" },
{ line: 4, type: "added", text: "const d = 4;" },
]);
});
it("should clean up temp directory after diff", async ({ expect }) => {
const { service, fs } = createDiffTestService();
await writeVendorLock(fs);
await fs.mkdir("/tmp/test-baseline/packages", { recursive: true });
await service.diff({
root: "/project",
remote: "remote",
branch: "main",
dir: "packages",
packages: [],
});
expect(fs.wasDeleted("/tmp/test-baseline")).toBe(true);
});
});
});