Skip to content

Commit 02ca734

Browse files
authored
Add cluster integration tests with testcontainers (#44)
* Add cluster integration tests with testcontainers * Removed beforeAll dep * Ignore integration tests for unit tests
1 parent c68b404 commit 02ca734

7 files changed

Lines changed: 1367 additions & 21 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"typecheck": "pnpm -r typecheck",
77
"test": "pnpm -r test",
8+
"test:integration": "pnpm -r test:integration",
89
"build": "pnpm -r build"
910
},
1011
"engines": {

packages/postgres/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@
2828
"require": "./dist/index.cjs"
2929
}
3030
},
31-
"files": ["dist", "src/migrations"],
31+
"files": [
32+
"dist",
33+
"src/migrations"
34+
],
3235
"scripts": {
3336
"build": "tsup && tsc --emitDeclarationOnly --outDir dist",
3437
"typecheck": "tsc --noEmit",
3538
"test": "vitest run",
39+
"test:integration": "vitest run -c vitest.integration.config.ts",
3640
"test:watch": "vitest"
3741
},
3842
"dependencies": {
@@ -41,6 +45,8 @@
4145
"zod": "^4.4.3"
4246
},
4347
"devDependencies": {
48+
"@testcontainers/postgresql": "^12.0.1",
49+
"testcontainers": "^12.0.1",
4450
"tsup": "^8.5.1",
4551
"typescript": "6.0.3",
4652
"vitest": "4.1.5"
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { describe, it, expect } from "vitest";
2+
import { NotFoundError } from "@lithium-ai/core";
3+
import { PostgresClusterAdapter } from "./cluster.port";
4+
import { createClusterService } from "@lithium-ai/core";
5+
import { setupTestDb } from "../test/setup";
6+
7+
describe("Cluster Integration", async () => {
8+
const clusterService = createClusterService(
9+
new PostgresClusterAdapter(await setupTestDb())
10+
);
11+
12+
it("should create a root cluster with correct shape", async () => {
13+
// Arrange
14+
const name = crypto.randomUUID();
15+
16+
// Act
17+
const result = await clusterService.create({ name });
18+
19+
// Assert
20+
expect(result).toEqual({
21+
success: true,
22+
value: {
23+
id: expect.any(String),
24+
parentId: null,
25+
path: name,
26+
name,
27+
description: null,
28+
createdAt: expect.any(Date),
29+
},
30+
});
31+
});
32+
33+
it("should retrieve a created cluster by path", async () => {
34+
// Arrange
35+
const name = crypto.randomUUID();
36+
const created = await clusterService.create({ name });
37+
38+
// Act
39+
const found = await clusterService.findByPath({ path: name });
40+
41+
// Assert
42+
expect(found).toEqual(created);
43+
});
44+
45+
it("should build child path as parentPath.name", async () => {
46+
// Arrange
47+
const parentName = crypto.randomUUID();
48+
const childName = crypto.randomUUID();
49+
await clusterService.create({ name: parentName });
50+
51+
// Act
52+
const child = await clusterService.create({
53+
name: childName,
54+
parentPath: parentName,
55+
});
56+
57+
// Assert
58+
expect(child).toEqual({
59+
success: true,
60+
value: expect.objectContaining({
61+
path: `${parentName}.${childName}`,
62+
name: childName,
63+
}),
64+
});
65+
});
66+
67+
it("should set parentId to the parent cluster ID", async () => {
68+
// Arrange
69+
const parentName = crypto.randomUUID();
70+
const parent = await clusterService.create({ name: parentName });
71+
if (!parent.success) throw new Error("Parent creation failed");
72+
73+
// Act
74+
const child = await clusterService.create({
75+
name: crypto.randomUUID(),
76+
parentPath: parentName,
77+
});
78+
79+
// Assert
80+
expect(child).toEqual({
81+
success: true,
82+
value: expect.objectContaining({
83+
parentId: parent.value.id,
84+
}),
85+
});
86+
});
87+
88+
it("should include created cluster in list results with correct shape", async () => {
89+
// Arrange
90+
const name = crypto.randomUUID();
91+
const created = await clusterService.create({ name });
92+
if (!created.success) throw new Error("Create failed");
93+
94+
// Act
95+
const result = await clusterService.list();
96+
if (!result.success) throw new Error("List failed");
97+
98+
// Assert
99+
expect(result.value).toContainEqual(created.value);
100+
});
101+
102+
it("should return exact descendant IDs for parent and child", async () => {
103+
// Arrange
104+
const parentName = crypto.randomUUID();
105+
const childName = crypto.randomUUID();
106+
const parent = await clusterService.create({ name: parentName });
107+
if (!parent.success) throw new Error("Parent creation failed");
108+
const child = await clusterService.create({
109+
name: childName,
110+
parentPath: parentName,
111+
});
112+
if (!child.success) throw new Error("Child creation failed");
113+
114+
// Act
115+
const result = await clusterService.listDescendantIds({ path: parentName });
116+
117+
// Assert
118+
expect(result).toEqual({
119+
success: true,
120+
value: expect.arrayContaining([parent.value.id, child.value.id]),
121+
});
122+
});
123+
124+
it("should not include unrelated clusters in descendant IDs", async () => {
125+
// Arrange
126+
const parentName = crypto.randomUUID();
127+
const unrelatedName = crypto.randomUUID();
128+
const parent = await clusterService.create({ name: parentName });
129+
if (!parent.success) throw new Error("Parent creation failed");
130+
const unrelated = await clusterService.create({ name: unrelatedName });
131+
if (!unrelated.success) throw new Error("Unrelated creation failed");
132+
133+
// Act
134+
const result = await clusterService.listDescendantIds({ path: parentName });
135+
if (!result.success) throw new Error("listDescendantIds failed");
136+
137+
// Assert
138+
expect(result.value).toContain(parent.value.id);
139+
expect(result.value).not.toContain(unrelated.value.id);
140+
});
141+
142+
it("should return NotFoundError for missing parent path", async () => {
143+
// Act
144+
const result = await clusterService.create({
145+
name: crypto.randomUUID(),
146+
parentPath: "nonexistent",
147+
});
148+
149+
// Assert
150+
expect(result).toEqual({
151+
success: false,
152+
error: expect.any(NotFoundError),
153+
});
154+
});
155+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { PostgreSqlContainer } from "@testcontainers/postgresql";
2+
import postgres, { type Sql } from "postgres";
3+
import fs from "fs";
4+
import path from "path";
5+
6+
const MIGRATIONS_DIR = path.join(__dirname, "../migrations");
7+
8+
export async function setupTestDb(): Promise<Sql> {
9+
const container = await new PostgreSqlContainer("postgres:16").start();
10+
const sql = postgres(container.getConnectionUri());
11+
12+
const migrations = fs
13+
.readdirSync(MIGRATIONS_DIR)
14+
.filter((f) => f.endsWith(".sql"))
15+
.sort();
16+
17+
for (const file of migrations) {
18+
const content = fs.readFileSync(path.join(MIGRATIONS_DIR, file), "utf-8");
19+
await sql.unsafe(content);
20+
}
21+
22+
return sql;
23+
}

packages/postgres/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config";
33
export default defineConfig({
44
test: {
55
include: ["src/**/*.test.ts"],
6+
exclude: ["src/**/*.integration.test.ts", "**/node_modules/**"],
67
passWithNoTests: true,
78
},
89
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
include: ["src/**/*.integration.test.ts"],
6+
},
7+
});

0 commit comments

Comments
 (0)