Skip to content

Commit 34367af

Browse files
committed
feat(tests): add unit tests for ObjectQL engine and InMemoryDriver, enhance AppPlugin and Runtime tests
1 parent f2f9adc commit 34367af

11 files changed

Lines changed: 471 additions & 10 deletions

File tree

packages/client/src/client.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('ObjectStackClient', () => {
4646
});
4747

4848
const result = await client.meta.getTypes();
49-
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta', expect.any(Object));
49+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/metadata', expect.any(Object));
5050
expect(result.types).toEqual(['object', 'plugin', 'view']);
5151
});
5252

@@ -65,7 +65,7 @@ describe('ObjectStackClient', () => {
6565
});
6666

6767
const result = await client.meta.getItems('object');
68-
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object', expect.any(Object));
68+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/metadata/object', expect.any(Object));
6969
expect(result.type).toBe('object');
7070
expect(result.items).toHaveLength(2);
7171
});
@@ -85,7 +85,7 @@ describe('ObjectStackClient', () => {
8585
});
8686

8787
const result = await client.meta.getItem('object', 'customer');
88-
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/meta/object/customer', expect.any(Object));
88+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/metadata/object/customer', expect.any(Object));
8989
expect(result.name).toBe('customer');
9090
});
9191
});

packages/core/src/api-registry-plugin.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ describe('API Registry Plugin', () => {
99
let kernel: ObjectKernel;
1010

1111
beforeEach(() => {
12-
kernel = new ObjectKernel();
12+
kernel = new ObjectKernel({
13+
skipSystemValidation: true
14+
});
1315
});
1416

1517
describe('Plugin Registration', () => {

packages/core/src/kernel.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('ObjectKernel', () => {
1010
kernel = new ObjectKernel({
1111
logger: { level: 'error' }, // Suppress logs in tests
1212
gracefulShutdown: false, // Disable for tests
13+
skipSystemValidation: true,
1314
});
1415
});
1516

@@ -226,6 +227,7 @@ describe('ObjectKernel', () => {
226227
logger: { level: 'error' },
227228
rollbackOnFailure: false,
228229
gracefulShutdown: false,
230+
skipSystemValidation: true,
229231
});
230232

231233
let plugin1Destroyed = false;

packages/core/src/kernel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export interface ObjectKernelConfig {
2121

2222
/** Whether to rollback on startup failure */
2323
rollbackOnFailure?: boolean;
24+
25+
/** Whether to skip strict system requirement validation (Critical for testing) */
26+
skipSystemValidation?: boolean;
2427
}
2528

2629
/**
@@ -190,6 +193,11 @@ export class ObjectKernel {
190193
* Validate Critical System Requirements
191194
*/
192195
private validateSystemRequirements() {
196+
if (this.config.skipSystemValidation) {
197+
this.logger.debug('System requirement validation skipped');
198+
return;
199+
}
200+
193201
this.logger.debug('Validating system service requirements...');
194202
const missingServices: string[] = [];
195203
const missingCoreServices: string[] = [];
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { ObjectQL } from './engine';
3+
import { SchemaRegistry } from './registry';
4+
import { DriverInterface } from '@objectstack/spec/data';
5+
6+
// Mock the SchemaRegistry to avoid side effects between tests
7+
vi.mock('./registry', () => {
8+
const mockObjects = new Map();
9+
return {
10+
SchemaRegistry: {
11+
getObject: vi.fn((name) => mockObjects.get(name)),
12+
registerObject: vi.fn((obj) => mockObjects.set(obj.name, obj)),
13+
registerKind: vi.fn(),
14+
metadata: {
15+
get: vi.fn(() => mockObjects) // Expose for verification if needed
16+
}
17+
}
18+
};
19+
});
20+
21+
describe('ObjectQL Engine', () => {
22+
let engine: ObjectQL;
23+
let mockDriver: DriverInterface;
24+
let mockDriver2: DriverInterface;
25+
26+
beforeEach(() => {
27+
// Clear Registry Mocks
28+
vi.clearAllMocks();
29+
30+
// Setup Drivers
31+
mockDriver = {
32+
name: 'default-driver',
33+
connect: vi.fn().mockResolvedValue(undefined),
34+
disconnect: vi.fn().mockResolvedValue(undefined),
35+
find: vi.fn().mockResolvedValue([{ id: '1', name: 'Test Record' }]),
36+
findOne: vi.fn(),
37+
create: vi.fn().mockResolvedValue({ id: '1', success: true }),
38+
update: vi.fn(),
39+
delete: vi.fn(),
40+
count: vi.fn(),
41+
capabilities: {} as any // Simplified
42+
} as unknown as DriverInterface;
43+
44+
mockDriver2 = {
45+
name: 'mongo',
46+
connect: vi.fn().mockResolvedValue(undefined),
47+
disconnect: vi.fn().mockResolvedValue(undefined),
48+
find: vi.fn().mockResolvedValue([{ id: '2', name: 'Mongo Record' }]),
49+
findOne: vi.fn(),
50+
create: vi.fn().mockResolvedValue({ id: '2', success: true }),
51+
update: vi.fn(),
52+
delete: vi.fn(),
53+
count: vi.fn(),
54+
capabilities: {} as any
55+
} as unknown as DriverInterface;
56+
57+
engine = new ObjectQL();
58+
});
59+
60+
describe('Initialization', () => {
61+
it('should initialize with default logger', () => {
62+
expect(engine).toBeDefined();
63+
expect(engine.getStatus().status).toBe('running');
64+
});
65+
66+
it('should register and connect drivers on init', async () => {
67+
engine.registerDriver(mockDriver, true);
68+
await engine.init();
69+
expect(mockDriver.connect).toHaveBeenCalled();
70+
});
71+
});
72+
73+
describe('Metadata Registration', () => {
74+
it('should register objects from app manifest', () => {
75+
const manifest = {
76+
id: 'com.example.app',
77+
objects: [
78+
{ name: 'task', fields: {} }
79+
]
80+
};
81+
82+
engine.registerApp(manifest);
83+
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(expect.objectContaining({ name: 'task' }));
84+
});
85+
86+
it('should register kinds from app manifest', () => {
87+
const manifest = {
88+
id: 'com.example.app',
89+
contributes: {
90+
kinds: [{ id: 'test.kind', description: 'Test Kind' }]
91+
}
92+
};
93+
94+
engine.registerApp(manifest);
95+
expect(SchemaRegistry.registerKind).toHaveBeenCalledWith(expect.objectContaining({ id: 'test.kind' }));
96+
});
97+
});
98+
99+
describe('Driver Routing', () => {
100+
beforeEach(async () => {
101+
// Setup:
102+
// - Default Driver: mockDriver
103+
// - Specific Driver: mockDriver2 (named 'mongo')
104+
engine.registerDriver(mockDriver, true);
105+
engine.registerDriver(mockDriver2);
106+
await engine.init();
107+
});
108+
109+
it('should route to default driver when no datasource is specified', async () => {
110+
// Mock Schema: Object uses default datasource
111+
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'task', datasource: 'default', fields: {} });
112+
113+
await engine.find('task', { filters: [] });
114+
115+
expect(mockDriver.find).toHaveBeenCalled();
116+
expect(mockDriver2.find).not.toHaveBeenCalled();
117+
});
118+
119+
it('should route to specific driver when datasource is specified', async () => {
120+
// Mock Schema: Object uses 'mongo' datasource
121+
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'log', datasource: 'mongo', fields: {} });
122+
123+
await engine.find('log', { filters: [] });
124+
125+
expect(mockDriver.find).not.toHaveBeenCalled();
126+
expect(mockDriver2.find).toHaveBeenCalled();
127+
});
128+
129+
it('should throw error if datasource is not found', async () => {
130+
// Mock Schema: Object uses unknown datasource
131+
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'old_data', datasource: 'legacy_sql', fields: {} });
132+
133+
await expect(engine.find('old_data', {})).rejects.toThrow("Datasource 'legacy_sql' configured for object 'old_data' is not registered");
134+
});
135+
});
136+
137+
describe('CRUD Operations', () => {
138+
beforeEach(async () => {
139+
engine.registerDriver(mockDriver, true);
140+
await engine.init();
141+
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'task', fields: {} });
142+
});
143+
144+
it('should execute insert operation', async () => {
145+
const result = await engine.insert('task', { title: 'New Task' });
146+
expect(mockDriver.create).toHaveBeenCalledWith('task', { title: 'New Task' }, undefined);
147+
expect(result).toEqual({ id: '1', success: true });
148+
});
149+
150+
it('should execute find operation', async () => {
151+
const result = await engine.find('task', {});
152+
expect(mockDriver.find).toHaveBeenCalled();
153+
expect(result).toHaveLength(1);
154+
});
155+
});
156+
});

packages/plugins/driver-memory/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
"types": "dist/index.d.ts",
88
"scripts": {
99
"build": "tsc",
10-
"dev": "tsc -w"
10+
"dev": "tsc -w",
11+
"test": "vitest run"
1112
},
1213
"dependencies": {
1314
"@objectstack/core": "workspace:*",
1415
"@objectstack/spec": "workspace:*"
1516
},
1617
"devDependencies": {
17-
"typescript": "^5.0.0"
18+
"typescript": "^5.0.0",
19+
"vitest": "^4.0.18",
20+
"@types/node": "^25.1.0"
1821
}
1922
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
2+
import { InMemoryDriver } from './memory-driver.js';
3+
4+
describe('InMemoryDriver', () => {
5+
let driver: InMemoryDriver;
6+
const testTable = 'test_table';
7+
8+
beforeEach(async () => {
9+
driver = new InMemoryDriver();
10+
await driver.connect();
11+
// No explicit clear DB method exposed, but new instance is clean.
12+
});
13+
14+
describe('Lifecycle', () => {
15+
it('should connect successfully', async () => {
16+
expect(driver.checkHealth()).resolves.toBe(true);
17+
});
18+
19+
it('should clear data on disconnect', async () => {
20+
await driver.create(testTable, { id: '1', name: 'test' });
21+
await driver.disconnect();
22+
const results = await driver.find(testTable, { select: ['id'] });
23+
expect(results).toHaveLength(0);
24+
});
25+
});
26+
27+
describe('Plugin Installation', () => {
28+
it('should register driver with engine', () => {
29+
const registerDriverFn = vi.fn();
30+
const mockEngine = {
31+
ql: {
32+
registerDriver: registerDriverFn
33+
}
34+
};
35+
36+
driver.install({ engine: mockEngine });
37+
expect(registerDriverFn).toHaveBeenCalledWith(driver);
38+
});
39+
40+
it('should handle missing engine gracefully', () => {
41+
const mockCtx = {}; // No engine
42+
// Should not throw
43+
driver.install(mockCtx);
44+
});
45+
});
46+
47+
describe('CRUD Operations', () => {
48+
it('should create and find records', async () => {
49+
const data = { id: '1', name: 'Alice', age: 30 };
50+
const created = await driver.create(testTable, data);
51+
expect(created.id).toBe('1');
52+
53+
const results = await driver.find(testTable, {
54+
select: ['id', 'name', 'age']
55+
});
56+
57+
expect(results).toHaveLength(1);
58+
expect(results[0].id).toBe('1');
59+
expect(results[0].name).toBe('Alice');
60+
});
61+
62+
it('should support updating records by ID', async () => {
63+
await driver.create(testTable, { id: '1', name: 'Bob', active: true });
64+
65+
const updateResult = await driver.update(
66+
testTable,
67+
'1',
68+
{ active: false }
69+
);
70+
71+
expect(updateResult.active).toBe(false);
72+
73+
const results = await driver.find(testTable, { select: ['active'] });
74+
expect(results[0].active).toBe(false);
75+
});
76+
77+
it('should support deleting records by ID', async () => {
78+
await driver.create(testTable, { id: '1', name: 'Charlie' });
79+
await driver.create(testTable, { id: '2', name: 'David' });
80+
81+
const deleteResult = await driver.delete(testTable, '1');
82+
expect(deleteResult).toBe(true);
83+
84+
const results = await driver.find(testTable, { select: ['name'] });
85+
expect(results).toHaveLength(1);
86+
expect(results[0].name).toBe('David');
87+
});
88+
});
89+
90+
describe('Query Capability', () => {
91+
it('should filter results', async () => {
92+
await driver.create(testTable, { id: '1', role: 'admin' });
93+
await driver.create(testTable, { id: '2', role: 'user' });
94+
await driver.create(testTable, { id: '3', role: 'user' });
95+
96+
const results = await driver.find(testTable, {
97+
select: ['id'],
98+
where: { role: 'user' }
99+
});
100+
101+
expect(results).toHaveLength(2);
102+
});
103+
104+
it('should limit results', async () => {
105+
await driver.create(testTable, { id: '1' });
106+
await driver.create(testTable, { id: '2' });
107+
await driver.create(testTable, { id: '3' });
108+
109+
const results = await driver.find(testTable, {
110+
select: ['id'],
111+
limit: 2
112+
});
113+
114+
expect(results).toHaveLength(2);
115+
});
116+
});
117+
});

packages/runtime/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
"types": "dist/index.d.ts",
99
"scripts": {
1010
"build": "tsc",
11-
"dev": "tsc -w"
11+
"dev": "tsc -w",
12+
"test": "vitest run"
1213
},
1314
"dependencies": {
1415
"@objectstack/core": "workspace:*",
15-
"@objectstack/types": "workspace:*",
16-
"@objectstack/spec": "workspace:*"
16+
"@objectstack/spec": "workspace:*",
17+
"@objectstack/types": "workspace:*"
1718
},
1819
"devDependencies": {
19-
"typescript": "^5.0.0"
20+
"typescript": "^5.0.0",
21+
"vitest": "^4.0.18"
2022
}
2123
}

0 commit comments

Comments
 (0)