test: await async store/defs calls in all test files
Update all *.test.ts files to properly await the async PulseStore, ScopedStore, and defs functions. Fix gc.test.ts timing issues for async GC trigger, fix bad await assignment targets, and correct .length access on Promise objects. Made-with: Cursor
This commit is contained in:
@@ -40,7 +40,7 @@ describe('Adaptive tick frequency', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
store.close();
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -89,13 +89,13 @@ describe('Adaptive tick frequency', () => {
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
// Check if we have any tick events at all
|
||||
let tickEvents = store.queryByKind('tick');
|
||||
let tickEvents = await store.queryByKind('tick');
|
||||
console.log(`Found ${tickEvents.length} tick events`);
|
||||
|
||||
if (tickEvents.length === 0) {
|
||||
// Wait a bit more
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
tickEvents = store.queryByKind('tick');
|
||||
tickEvents = await store.queryByKind('tick');
|
||||
console.log(`After waiting longer: ${tickEvents.length} tick events`);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ describe('Adaptive tick frequency', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
|
||||
const tickEvents = store.queryByKind('tick');
|
||||
const tickEvents = await store.queryByKind('tick');
|
||||
|
||||
for (const tickEvent of tickEvents) {
|
||||
if (tickEvent.meta) {
|
||||
@@ -182,8 +182,8 @@ describe('Adaptive tick frequency', () => {
|
||||
|
||||
it('should use custom hasActiveWork function', async () => {
|
||||
// Pre-populate with active topics data
|
||||
const activeTopicsHash = store.putObject(['topic1', 'topic2']);
|
||||
store.appendEvent({
|
||||
const activeTopicsHash = await store.putObject(['topic1', 'topic2']);
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'activeTopics',
|
||||
@@ -220,7 +220,7 @@ describe('Adaptive tick frequency', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
const tickEvents = store.queryByKind('tick');
|
||||
const tickEvents = await store.queryByKind('tick');
|
||||
|
||||
for (const tickEvent of tickEvents) {
|
||||
if (tickEvent.meta) {
|
||||
@@ -269,7 +269,7 @@ describe('Adaptive tick frequency', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
|
||||
const tickEvents = store.queryByKind('tick');
|
||||
const tickEvents = await store.queryByKind('tick');
|
||||
|
||||
for (const tickEvent of tickEvents) {
|
||||
if (tickEvent.meta) {
|
||||
@@ -325,8 +325,8 @@ describe('Adaptive tick frequency', () => {
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
// Manually write a vital event to trigger wake
|
||||
const wakeData = store.putObject({ wake: true });
|
||||
store.appendEvent({
|
||||
const wakeData = await store.putObject({ wake: true });
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'vital',
|
||||
key: 'wake_signal',
|
||||
@@ -335,7 +335,7 @@ describe('Adaptive tick frequency', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const tickEvents = store.queryByKind('tick');
|
||||
const tickEvents = await store.queryByKind('tick');
|
||||
|
||||
for (const tickEvent of tickEvents) {
|
||||
if (tickEvent.meta) {
|
||||
|
||||
@@ -26,16 +26,16 @@ describe('Definition Layer', () => {
|
||||
let tempDir: string;
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'pulse-defs-'));
|
||||
const systemDbPath = join(tempDir, '_system.db');
|
||||
db = new Database(systemDbPath, { create: true });
|
||||
db.exec('PRAGMA journal_mode = WAL');
|
||||
initDefsSchema(db);
|
||||
await initDefsSchema(db);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db?.close();
|
||||
afterEach(async () => {
|
||||
await db?.close();
|
||||
if (existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -43,8 +43,8 @@ describe('Definition Layer', () => {
|
||||
|
||||
// ── Test 1: object_defs registration ──────────────────────────
|
||||
|
||||
it('1. registers object definitions', () => {
|
||||
const objDef = registerObjectDef(db, {
|
||||
it('1. registers object definitions', async () => {
|
||||
const objDef = await registerObjectDef(db, {
|
||||
name: 'User',
|
||||
codeRev: 'v1.0.0',
|
||||
});
|
||||
@@ -55,16 +55,16 @@ describe('Definition Layer', () => {
|
||||
expect(objDef.createdAt).toBeGreaterThan(0);
|
||||
|
||||
// Verify it's stored
|
||||
const retrieved = getObjectDef(db, 'User', 'v1.0.0');
|
||||
const retrieved = await getObjectDef(db, 'User', 'v1.0.0');
|
||||
expect(retrieved).toEqual(objDef);
|
||||
});
|
||||
|
||||
// ── Test 2: event_defs registration + content hash ───────────
|
||||
|
||||
it('2. registers event definitions with content-based hash', () => {
|
||||
it('2. registers event definitions with content-based hash', async () => {
|
||||
const schema = { type: 'object', properties: { name: { type: 'string' } } };
|
||||
|
||||
const eventDef = registerEventDef(db, {
|
||||
const eventDef = await registerEventDef(db, {
|
||||
name: 'UserCreated',
|
||||
schema,
|
||||
codeRev: 'v1.0.0',
|
||||
@@ -77,7 +77,7 @@ describe('Definition Layer', () => {
|
||||
expect(eventDef.hash).toHaveLength(64); // SHA-256 hex
|
||||
|
||||
// Same content should produce same hash
|
||||
const eventDef2 = registerEventDef(db, {
|
||||
const eventDef2 = await registerEventDef(db, {
|
||||
name: 'UserCreated',
|
||||
schema,
|
||||
codeRev: 'v1.1.0', // Different code_rev, same content
|
||||
@@ -88,14 +88,14 @@ describe('Definition Layer', () => {
|
||||
|
||||
// ── Test 3: event_def version chain ───────────────────────────
|
||||
|
||||
it('3. supports event_def version chains via parent_hash', () => {
|
||||
const v1 = registerEventDef(db, {
|
||||
it('3. supports event_def version chains via parent_hash', async () => {
|
||||
const v1 = await registerEventDef(db, {
|
||||
name: 'UserUpdated',
|
||||
schema: { type: 'object', properties: { name: { type: 'string' } } },
|
||||
codeRev: 'v1.0.0',
|
||||
});
|
||||
|
||||
const v2 = registerEventDef(db, {
|
||||
const v2 = await registerEventDef(db, {
|
||||
name: 'UserUpdated',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -112,8 +112,8 @@ describe('Definition Layer', () => {
|
||||
expect(v2.hash).not.toBe(v1.hash); // Different content = different hash
|
||||
|
||||
// Verify retrieval
|
||||
const retrievedV1 = getEventDef(db, 'UserUpdated', 'v1.0.0');
|
||||
const retrievedV2 = getEventDef(db, 'UserUpdated', 'v2.0.0');
|
||||
const retrievedV1 = await getEventDef(db, 'UserUpdated', 'v1.0.0');
|
||||
const retrievedV2 = await getEventDef(db, 'UserUpdated', 'v2.0.0');
|
||||
|
||||
expect(retrievedV1?.hash).toBe(v1.hash);
|
||||
expect(retrievedV2?.hash).toBe(v2.hash);
|
||||
@@ -147,7 +147,7 @@ describe('Definition Layer', () => {
|
||||
expect(typeof projDef.hash).toBe('string');
|
||||
|
||||
// Verify retrieval includes sources
|
||||
const retrieved = getProjectionDef(db, 'UserCount', 'v1.0.0');
|
||||
const retrieved = await getProjectionDef(db, 'UserCount', 'v1.0.0');
|
||||
expect(retrieved).toEqual(projDef);
|
||||
});
|
||||
|
||||
@@ -155,15 +155,15 @@ describe('Definition Layer', () => {
|
||||
|
||||
it('5. enforces (name, code_rev) UNIQUE constraints', async () => {
|
||||
// Object defs
|
||||
registerObjectDef(db, { name: 'Product', codeRev: 'v1.0.0' });
|
||||
expect(() => {
|
||||
registerObjectDef(db, { name: 'Product', codeRev: 'v1.0.0' });
|
||||
await registerObjectDef(db, { name: 'Product', codeRev: 'v1.0.0' });
|
||||
await expect(async () => {
|
||||
await registerObjectDef(db, { name: 'Product', codeRev: 'v1.0.0' });
|
||||
}).toThrow('Object definition already exists: Product@v1.0.0');
|
||||
|
||||
// Event defs
|
||||
registerEventDef(db, { name: 'ProductCreated', codeRev: 'v1.0.0' });
|
||||
expect(() => {
|
||||
registerEventDef(db, { name: 'ProductCreated', codeRev: 'v1.0.0' });
|
||||
await registerEventDef(db, { name: 'ProductCreated', codeRev: 'v1.0.0' });
|
||||
await expect(async () => {
|
||||
await registerEventDef(db, { name: 'ProductCreated', codeRev: 'v1.0.0' });
|
||||
}).toThrow('Event definition already exists: ProductCreated@v1.0.0');
|
||||
|
||||
// Projection defs
|
||||
@@ -192,9 +192,9 @@ describe('Definition Layer', () => {
|
||||
|
||||
it('6. queries definitions by code_rev', async () => {
|
||||
// Register multiple definitions across different code_rev values
|
||||
registerEventDef(db, { name: 'OrderCreated', codeRev: 'v1.0.0' });
|
||||
registerEventDef(db, { name: 'OrderUpdated', codeRev: 'v1.0.0' });
|
||||
registerEventDef(db, { name: 'OrderDeleted', codeRev: 'v2.0.0' });
|
||||
await registerEventDef(db, { name: 'OrderCreated', codeRev: 'v1.0.0' });
|
||||
await registerEventDef(db, { name: 'OrderUpdated', codeRev: 'v1.0.0' });
|
||||
await registerEventDef(db, { name: 'OrderDeleted', codeRev: 'v2.0.0' });
|
||||
|
||||
await registerProjectionDef(db, {
|
||||
name: 'OrderStats',
|
||||
@@ -209,8 +209,8 @@ describe('Definition Layer', () => {
|
||||
codeRev: 'v2.0.0',
|
||||
});
|
||||
|
||||
const v1Events = listEventDefs(db, { codeRev: 'v1.0.0' });
|
||||
const v2Events = listEventDefs(db, { codeRev: 'v2.0.0' });
|
||||
const v1Events = await listEventDefs(db, { codeRev: 'v1.0.0' });
|
||||
const v2Events = await listEventDefs(db, { codeRev: 'v2.0.0' });
|
||||
|
||||
expect(v1Events).toHaveLength(2);
|
||||
expect(v1Events.map((e) => e.name).sort()).toEqual([
|
||||
@@ -221,8 +221,8 @@ describe('Definition Layer', () => {
|
||||
expect(v2Events).toHaveLength(1);
|
||||
expect(v2Events[0].name).toBe('OrderDeleted');
|
||||
|
||||
const v1Projections = listProjectionDefs(db, { codeRev: 'v1.0.0' });
|
||||
const v2Projections = listProjectionDefs(db, { codeRev: 'v2.0.0' });
|
||||
const v1Projections = await listProjectionDefs(db, { codeRev: 'v1.0.0' });
|
||||
const v2Projections = await listProjectionDefs(db, { codeRev: 'v2.0.0' });
|
||||
|
||||
expect(v1Projections).toHaveLength(1);
|
||||
expect(v1Projections[0].name).toBe('OrderStats');
|
||||
@@ -298,12 +298,12 @@ describe('Definition Layer', () => {
|
||||
|
||||
it('9. comprehensive integration test', async () => {
|
||||
// Register a complete set of definitions
|
||||
const objDef = registerObjectDef(db, {
|
||||
const objDef = await registerObjectDef(db, {
|
||||
name: 'Invoice',
|
||||
codeRev: 'v1.2.0',
|
||||
});
|
||||
|
||||
const eventDef = registerEventDef(db, {
|
||||
const eventDef = await registerEventDef(db, {
|
||||
name: 'InvoiceCreated',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -340,13 +340,13 @@ describe('Definition Layer', () => {
|
||||
});
|
||||
|
||||
// Verify all definitions can be retrieved
|
||||
expect(getObjectDef(db, 'Invoice', 'v1.2.0')).toEqual(objDef);
|
||||
expect(getEventDef(db, 'InvoiceCreated', 'v1.2.0')).toEqual(eventDef);
|
||||
expect(getProjectionDef(db, 'InvoiceSummary', 'v1.2.0')).toEqual(projDef);
|
||||
expect(await getObjectDef(db, 'Invoice', 'v1.2.0')).toEqual(objDef);
|
||||
expect(await getEventDef(db, 'InvoiceCreated', 'v1.2.0')).toEqual(eventDef);
|
||||
expect(await getProjectionDef(db, 'InvoiceSummary', 'v1.2.0')).toEqual(projDef);
|
||||
|
||||
// Verify listing works
|
||||
const events = listEventDefs(db, { codeRev: 'v1.2.0' });
|
||||
const projections = listProjectionDefs(db, { codeRev: 'v1.2.0' });
|
||||
const events = await listEventDefs(db, { codeRev: 'v1.2.0' });
|
||||
const projections = await listProjectionDefs(db, { codeRev: 'v1.2.0' });
|
||||
|
||||
expect(events.find((e) => e.name === 'InvoiceCreated')).toBeDefined();
|
||||
expect(projections.find((p) => p.name === 'InvoiceSummary')).toBeDefined();
|
||||
|
||||
@@ -24,9 +24,9 @@ describe('Council v2 E2E', () => {
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
try {
|
||||
store?.close();
|
||||
await store?.close();
|
||||
} catch {}
|
||||
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -37,8 +37,8 @@ describe('Council v2 E2E', () => {
|
||||
description: string,
|
||||
repoDir: string,
|
||||
) {
|
||||
const hash = store.putObject(description);
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject(description);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'coding.__start__',
|
||||
key: topicId,
|
||||
@@ -80,13 +80,13 @@ describe('Council v2 E2E', () => {
|
||||
}
|
||||
|
||||
// Verify both closed
|
||||
const closedEvents = store.queryByKind('coding.closer');
|
||||
const closedEvents = await store.queryByKind('coding.closer');
|
||||
expect(closedEvents.length).toBe(2);
|
||||
const closedIds = closedEvents.map((e) => e.key);
|
||||
expect(closedIds.sort()).toEqual(['task-a', 'task-b']);
|
||||
|
||||
// task-b should have been coded twice
|
||||
const codedEvents = store.queryByKind('coding.coder');
|
||||
const codedEvents = await store.queryByKind('coding.coder');
|
||||
const taskBCoded = codedEvents.filter((e) => e.key === 'task-b');
|
||||
expect(taskBCoded.length).toBe(2);
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ function writeVitalAndRebuild(
|
||||
key: string,
|
||||
data: unknown,
|
||||
): SurvivalSnapshot {
|
||||
const hash = stores.vitals.putObject(data);
|
||||
stores.vitals.appendEvent({
|
||||
const hash = await stores.vitals.putObject(data);
|
||||
await stores.vitals.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'vital',
|
||||
key,
|
||||
@@ -76,9 +76,9 @@ describe('E2E survival chain', () => {
|
||||
stores = makeStores(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stores.system.close();
|
||||
stores.vitals.close();
|
||||
afterEach(async () => {
|
||||
await stores.system.close();
|
||||
await stores.vitals.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('E2E survival chain', () => {
|
||||
const now = Date.now();
|
||||
|
||||
// Record a promote event within the rollback window
|
||||
stores.system.appendEvent({
|
||||
await stores.system.appendEvent({
|
||||
occurredAt: now - 60_000, // 1 minute ago
|
||||
kind: 'promote',
|
||||
codeRev: 'v2',
|
||||
@@ -234,7 +234,7 @@ describe('E2E survival chain', () => {
|
||||
|
||||
// Record enough error events to exceed ROLLBACK_ERROR_THRESHOLD
|
||||
for (let i = 0; i < ROLLBACK_ERROR_THRESHOLD; i++) {
|
||||
stores.system.appendEvent({
|
||||
await stores.system.appendEvent({
|
||||
occurredAt: now - 30_000 + i * 1000,
|
||||
kind: 'error',
|
||||
meta: JSON.stringify({ message: `error ${i}` }),
|
||||
@@ -265,7 +265,7 @@ describe('E2E survival chain', () => {
|
||||
// Simulate MAX_RESTART_COUNT restarts already recorded in events
|
||||
const now = Date.now();
|
||||
for (let i = 0; i < MAX_RESTART_COUNT; i++) {
|
||||
stores.system.appendEvent({
|
||||
await stores.system.appendEvent({
|
||||
occurredAt: now - (MAX_RESTART_COUNT - i) * 10_000,
|
||||
kind: 'effect',
|
||||
meta: JSON.stringify({ type: 'restart-service', service: 'upulse' }),
|
||||
@@ -354,8 +354,8 @@ describe('E2E survival chain', () => {
|
||||
const initialData = { diskPct: 40, memoryPct: 50, cpuPct: 10, swapPct: 0 };
|
||||
const alarmedData = { diskPct: 96, memoryPct: 50, cpuPct: 10, swapPct: 0 }; // disk crisis
|
||||
|
||||
const h0 = stores.vitals.putObject(initialData);
|
||||
stores.vitals.appendEvent({
|
||||
const h0 = await stores.vitals.putObject(initialData);
|
||||
await stores.vitals.appendEvent({
|
||||
occurredAt: Date.now() - 5000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -370,8 +370,8 @@ describe('E2E survival chain', () => {
|
||||
wakeTriggered = true;
|
||||
};
|
||||
|
||||
const h1 = stores.vitals.putObject(alarmedData);
|
||||
stores.vitals.appendEvent({
|
||||
const h1 = await stores.vitals.putObject(alarmedData);
|
||||
await stores.vitals.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -382,7 +382,7 @@ describe('E2E survival chain', () => {
|
||||
.queryByKind('vital', { key: 'system', limit: 12 })
|
||||
.map((v) => ({
|
||||
...v,
|
||||
data: v.hash ? stores.vitals.getObject(v.hash) : null,
|
||||
await data: v.hash ? stores.vitals.getObject(v.hash) : null,
|
||||
}));
|
||||
const shouldWakeResult = latestVitals.some(
|
||||
(v) => v.data && (v.data as { diskPct?: number }).diskPct! > 95,
|
||||
@@ -426,8 +426,8 @@ describe('E2E survival chain', () => {
|
||||
['llm', llmData],
|
||||
['network', networkData],
|
||||
] as const) {
|
||||
const hash = stores.vitals.putObject(data);
|
||||
stores.vitals.appendEvent({
|
||||
const hash = await stores.vitals.putObject(data);
|
||||
await stores.vitals.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'vital',
|
||||
key,
|
||||
|
||||
@@ -54,15 +54,15 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.close();
|
||||
afterEach(async () => {
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ── 1. Basic event ID properties ──────────────────────────────
|
||||
|
||||
test('1: appendEvent returns positive integer id', () => {
|
||||
const event = store.appendEvent({
|
||||
test('1: appendEvent returns positive integer id', async () => {
|
||||
const event = await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'tick',
|
||||
key: 'sys',
|
||||
@@ -72,10 +72,10 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 2. Sequential monotonic IDs ──────────────────────────────
|
||||
|
||||
test('2: 100 events produce strictly increasing integer IDs', () => {
|
||||
test('2: 100 events produce strictly increasing integer IDs', async () => {
|
||||
const ids: number[] = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const e = store.appendEvent({
|
||||
const e = await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: i % 3 === 0 ? 'tick' : i % 3 === 1 ? 'collect' : 'effect',
|
||||
key: `k${i}`,
|
||||
@@ -93,14 +93,14 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 3. Batch insert preserves int IDs ──────────────────────────
|
||||
|
||||
test('3: appendEvents batch returns sequential integer IDs', () => {
|
||||
test('3: appendEvents batch returns sequential integer IDs', async () => {
|
||||
const batch = Array.from({ length: 50 }, (_, i) => ({
|
||||
occurredAt: Date.now() + i,
|
||||
kind: 'vital' as const,
|
||||
key: `sensor_${i}`,
|
||||
}));
|
||||
|
||||
const results = store.appendEvents(batch);
|
||||
const results = await store.appendEvents(batch);
|
||||
expect(results.length).toBe(50);
|
||||
|
||||
for (const r of results) {
|
||||
@@ -114,15 +114,15 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 4. Object ID is integer ────────────────────────────────────
|
||||
|
||||
test('4: createObject returns integer id, getObjectInstance roundtrips', () => {
|
||||
const objId = store.createObject({
|
||||
test('4: createObject returns integer id, getObjectInstance roundtrips', async () => {
|
||||
const objId = await store.createObject({
|
||||
objectType: 'session',
|
||||
externalId: 'sess_abc',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
assertIntId(objId);
|
||||
|
||||
const instance = store.getObjectInstance(objId);
|
||||
const instance = await store.getObjectInstance(objId);
|
||||
expect(instance).not.toBeNull();
|
||||
expect(instance!.id).toBe(objId);
|
||||
assertIntId(instance!.id);
|
||||
@@ -130,15 +130,15 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 5. Event→Object FK roundtrip ──────────────────────────────
|
||||
|
||||
test('5: event.objectId FK references object.id as integer', () => {
|
||||
const objId = store.createObject({
|
||||
test('5: event.objectId FK references object.id as integer', async () => {
|
||||
const objId = await store.createObject({
|
||||
objectType: 'task',
|
||||
externalId: 'task_42',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
assertIntId(objId);
|
||||
|
||||
const event = store.appendEvent({
|
||||
const event = await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'task-created',
|
||||
key: 'task_42',
|
||||
@@ -147,7 +147,7 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
assertIntId(event.id);
|
||||
expect(event.objectId).toBe(objId);
|
||||
|
||||
const latest = store.getLatest('task-created', 'task_42');
|
||||
const latest = await store.getLatest('task-created', 'task_42');
|
||||
expect(latest).not.toBeNull();
|
||||
assertIntId(latest!.id);
|
||||
expect(latest!.objectId).toBe(objId);
|
||||
@@ -156,11 +156,11 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 6. getAfter cursor with int IDs ────────────────────────────
|
||||
|
||||
test('6: getAfter(id) cursor correctly paginates with int IDs', () => {
|
||||
test('6: getAfter(id) cursor correctly paginates with int IDs', async () => {
|
||||
const events = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
events.push(
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000 + i * 100,
|
||||
kind: i % 2 === 0 ? 'tick' : 'collect',
|
||||
key: `k${i}`,
|
||||
@@ -171,7 +171,7 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
const cursor = events[4]!.id;
|
||||
assertIntId(cursor);
|
||||
|
||||
const after = store.getAfter(cursor);
|
||||
const after = await store.getAfter(cursor);
|
||||
expect(after.length).toBe(15);
|
||||
|
||||
for (const e of after) {
|
||||
@@ -187,14 +187,14 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 7. getAfter with kind filter ──────────────────────────────
|
||||
|
||||
test('7: getAfter(id, {kind}) filters correctly with int IDs', () => {
|
||||
const e1 = store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'collect' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 4000, kind: 'effect' });
|
||||
store.appendEvent({ occurredAt: 5000, kind: 'tick' });
|
||||
test('7: getAfter(id, {kind}) filters correctly with int IDs', async () => {
|
||||
const e1 = await store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'collect' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 4000, kind: 'effect' });
|
||||
await store.appendEvent({ occurredAt: 5000, kind: 'tick' });
|
||||
|
||||
const afterTicks = store.getAfter(e1.id, { kind: 'tick' });
|
||||
const afterTicks = await store.getAfter(e1.id, { kind: 'tick' });
|
||||
expect(afterTicks.length).toBe(2);
|
||||
|
||||
for (const e of afterTicks) {
|
||||
@@ -206,17 +206,17 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 8. queryByKind returns int IDs ─────────────────────────────
|
||||
|
||||
test('8: queryByKind results all have integer IDs', () => {
|
||||
test('8: queryByKind results all have integer IDs', async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000 + i * 100,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: store.putObject({ cpu: i * 10 }),
|
||||
await hash: store.putObject({ cpu: i * 10 }),
|
||||
});
|
||||
}
|
||||
|
||||
const results = store.queryByKind('vital', { key: 'cpu', limit: 5 });
|
||||
const results = await store.queryByKind('vital', { key: 'cpu', limit: 5 });
|
||||
expect(results.length).toBe(5);
|
||||
|
||||
for (const r of results) {
|
||||
@@ -226,15 +226,15 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 9. getRecent returns int IDs ───────────────────────────────
|
||||
|
||||
test('9: getRecent returns records with integer IDs in desc order', () => {
|
||||
test('9: getRecent returns records with integer IDs in desc order', async () => {
|
||||
for (let i = 0; i < 15; i++) {
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000 + i * 100,
|
||||
kind: 'tick',
|
||||
});
|
||||
}
|
||||
|
||||
const recent = store.getRecent(10);
|
||||
const recent = await store.getRecent(10);
|
||||
expect(recent.length).toBe(10);
|
||||
|
||||
for (const r of recent) {
|
||||
@@ -249,32 +249,32 @@ describe('E2E T9: INT ID lifecycle', () => {
|
||||
|
||||
// ── 10. IDs survive close/reopen ───────────────────────────────
|
||||
|
||||
test('10: IDs continue incrementing after store close/reopen', () => {
|
||||
const e1 = store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
const e2 = store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
test('10: IDs continue incrementing after store close/reopen', async () => {
|
||||
const e1 = await store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
const e2 = await store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
assertIntId(e1.id);
|
||||
assertIntId(e2.id);
|
||||
|
||||
const lastId = e2.id;
|
||||
store.close();
|
||||
await store.close();
|
||||
|
||||
const store2 = createStore({
|
||||
eventsDbPath: join(tmpDir, 'events.db'),
|
||||
objectsDir: join(tmpDir, 'objects'),
|
||||
});
|
||||
|
||||
const e3 = store2.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
const e3 = await store2.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
assertIntId(e3.id);
|
||||
expect(e3.id).toBeGreaterThan(lastId);
|
||||
|
||||
// getAfter from original ID still works
|
||||
const after = store2.getAfter(e1.id);
|
||||
const after = await store2.getAfter(e1.id);
|
||||
expect(after.length).toBe(2);
|
||||
for (const e of after) {
|
||||
assertIntId(e.id);
|
||||
}
|
||||
|
||||
store2.close();
|
||||
await store2.close();
|
||||
|
||||
// Reassign so afterEach doesn't double-close
|
||||
store = createStore({
|
||||
@@ -298,26 +298,26 @@ describe('E2E T9: INT ID with ScopedStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scopedStore.close();
|
||||
afterEach(async () => {
|
||||
await scopedStore.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('11: different scopes have independent ID sequences', () => {
|
||||
test('11: different scopes have independent ID sequences', async () => {
|
||||
const scopeA = scopedStore.scope('alpha');
|
||||
const scopeB = scopedStore.scope('beta');
|
||||
|
||||
const aEvents = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
aEvents.push(
|
||||
scopeA.appendEvent({ occurredAt: Date.now(), kind: 'tick' }),
|
||||
await scopeA.appendEvent({ occurredAt: Date.now(), kind: 'tick' }),
|
||||
);
|
||||
}
|
||||
|
||||
const bEvents = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
bEvents.push(
|
||||
scopeB.appendEvent({ occurredAt: Date.now(), kind: 'tick' }),
|
||||
await scopeB.appendEvent({ occurredAt: Date.now(), kind: 'tick' }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -337,17 +337,17 @@ describe('E2E T9: INT ID with ScopedStore', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('12: object IDs are integers in scoped stores', () => {
|
||||
test('12: object IDs are integers in scoped stores', async () => {
|
||||
const s = scopedStore.scope('test-scope');
|
||||
|
||||
const objId = s.createObject({
|
||||
const objId = await s.createObject({
|
||||
objectType: 'user',
|
||||
externalId: 'u1',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
assertIntId(objId);
|
||||
|
||||
const event = s.appendEvent({
|
||||
const event = await s.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'user-created',
|
||||
key: 'u1',
|
||||
@@ -356,7 +356,7 @@ describe('E2E T9: INT ID with ScopedStore', () => {
|
||||
assertIntId(event.id);
|
||||
expect(event.objectId).toBe(objId);
|
||||
|
||||
const instance = s.getObjectInstance(objId);
|
||||
const instance = await s.getObjectInstance(objId);
|
||||
expect(instance).not.toBeNull();
|
||||
assertIntId(instance!.id);
|
||||
});
|
||||
@@ -381,22 +381,22 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.close();
|
||||
vitalsStore.close();
|
||||
afterEach(async () => {
|
||||
await store.close();
|
||||
await vitalsStore.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('13: promote → collect → rebuildSnapshot uses int IDs throughout', () => {
|
||||
const promoteEvent = store.appendEvent({
|
||||
test('13: promote → collect → rebuildSnapshot uses int IDs throughout', async () => {
|
||||
const promoteEvent = await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'promote',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
assertIntId(promoteEvent.id);
|
||||
|
||||
const collectHash = store.putObject({ diskPct: 40, memoryPct: 60 });
|
||||
const collectEvent = store.appendEvent({
|
||||
const collectHash = await store.putObject({ diskPct: 40, memoryPct: 60 });
|
||||
const collectEvent = await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -406,7 +406,7 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
assertIntId(collectEvent.id);
|
||||
expect(collectEvent.id).toBeGreaterThan(promoteEvent.id);
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
expect(epoch).not.toBeNull();
|
||||
assertIntId(epoch!.id);
|
||||
|
||||
@@ -419,9 +419,9 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
expect(snapshot.system.data.memoryPct).toBe(60);
|
||||
});
|
||||
|
||||
test('14: vitals store events use int IDs, prioritized in snapshot', () => {
|
||||
const vitalHash = vitalsStore.putObject({ memoryPct: 85 });
|
||||
const vitalEvent = vitalsStore.appendEvent({
|
||||
test('14: vitals store events use int IDs, prioritized in snapshot', async () => {
|
||||
const vitalHash = await vitalsStore.putObject({ memoryPct: 85 });
|
||||
const vitalEvent = await vitalsStore.appendEvent({
|
||||
occurredAt: 3000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -429,8 +429,8 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
});
|
||||
assertIntId(vitalEvent.id);
|
||||
|
||||
const collectHash = store.putObject({ memoryPct: 50 });
|
||||
store.appendEvent({
|
||||
const collectHash = await store.putObject({ memoryPct: 50 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -451,12 +451,12 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
});
|
||||
|
||||
test('15: rules see int IDs via effects written to store', async () => {
|
||||
const vitalHash = vitalsStore.putObject({
|
||||
const vitalHash = await vitalsStore.putObject({
|
||||
diskPct: 40,
|
||||
memoryPct: 60,
|
||||
cpuPct: 20,
|
||||
});
|
||||
vitalsStore.appendEvent({
|
||||
await vitalsStore.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -490,8 +490,8 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
|
||||
// Write effects back to store (simulating what runPulse does)
|
||||
for (const effect of effects) {
|
||||
const effectHash = store.putObject(effect);
|
||||
const effectEvent = store.appendEvent({
|
||||
const effectHash = await store.putObject(effect);
|
||||
const effectEvent = await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'effect',
|
||||
key: effectHash,
|
||||
@@ -503,17 +503,17 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
}
|
||||
|
||||
// Verify effect events have int IDs
|
||||
const effectEvents = store.queryByKind('effect');
|
||||
const effectEvents = await store.queryByKind('effect');
|
||||
expect(effectEvents.length).toBeGreaterThan(0);
|
||||
for (const e of effectEvents) {
|
||||
assertIntId(e.id);
|
||||
}
|
||||
});
|
||||
|
||||
test('16: rollback epoch references int IDs', () => {
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
const h1 = store.putObject({ value: 'v1-state' });
|
||||
store.appendEvent({
|
||||
test('16: rollback epoch references int IDs', async () => {
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
const h1 = await store.putObject({ value: 'v1-state' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1100,
|
||||
kind: 'collect',
|
||||
key: 'data',
|
||||
@@ -521,9 +521,9 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
const h2 = store.putObject({ value: 'v2-state' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
const h2 = await store.putObject({ value: 'v2-state' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 2100,
|
||||
kind: 'collect',
|
||||
key: 'data',
|
||||
@@ -531,20 +531,20 @@ describe('E2E T9: INT ID through rebuildSnapshot + rules', () => {
|
||||
codeRev: 'v2',
|
||||
});
|
||||
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 3000,
|
||||
kind: 'rollback',
|
||||
codeRev: 'v1',
|
||||
meta: JSON.stringify({ to: 'v1', from: 'v2' }),
|
||||
});
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
expect(epoch).not.toBeNull();
|
||||
assertIntId(epoch!.id);
|
||||
expect(epoch!.codeRev).toBe('v1');
|
||||
|
||||
// getAfter on epoch.id returns correct events
|
||||
const afterEpoch = store.getAfter(epoch!.id, {
|
||||
const afterEpoch = await store.getAfter(epoch!.id, {
|
||||
kind: 'collect',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ function makeStore(tmpDir: string, name = 'events'): PulseStore {
|
||||
}
|
||||
|
||||
function writeEffectEvent(store: PulseStore, effect: TestEffect): string {
|
||||
const hash = store.putObject(effect);
|
||||
const event = store.appendEvent({
|
||||
const hash = await store.putObject(effect);
|
||||
const event = await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'effect',
|
||||
key: hash,
|
||||
@@ -47,8 +47,8 @@ describe('executorLoop', () => {
|
||||
store = makeStore(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.close();
|
||||
afterEach(async () => {
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -158,12 +158,12 @@ describe('executorLoop', () => {
|
||||
await loopDone;
|
||||
|
||||
// effect-executing should have been written with key = effectEventId
|
||||
const executing = store.getLatest('effect-executing', effectEventId);
|
||||
const executing = await store.getLatest('effect-executing', effectEventId);
|
||||
expect(executing).not.toBeNull();
|
||||
expect(executing?.key).toBe(effectEventId);
|
||||
|
||||
// effect-acked should have been written with key = effectEventId
|
||||
const acked = store.getLatest('effect-acked', effectEventId);
|
||||
const acked = await store.getLatest('effect-acked', effectEventId);
|
||||
expect(acked).not.toBeNull();
|
||||
expect(acked?.key).toBe(effectEventId);
|
||||
});
|
||||
@@ -189,7 +189,7 @@ describe('executorLoop', () => {
|
||||
await loopDone;
|
||||
|
||||
// effect-failed should be recorded
|
||||
const failed = store.getLatest('effect-failed', effectEventId);
|
||||
const failed = await store.getLatest('effect-failed', effectEventId);
|
||||
expect(failed).not.toBeNull();
|
||||
expect(failed?.key).toBe(effectEventId);
|
||||
|
||||
@@ -198,7 +198,7 @@ describe('executorLoop', () => {
|
||||
expect(meta.error).toContain('executor boom');
|
||||
|
||||
// effect-acked must NOT be written
|
||||
const acked = store.getLatest('effect-acked', effectEventId);
|
||||
const acked = await store.getLatest('effect-acked', effectEventId);
|
||||
expect(acked).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -41,52 +41,52 @@ describe('gcVitals', () => {
|
||||
teardown();
|
||||
});
|
||||
|
||||
it('should archive events older than 7 days', () => {
|
||||
it('should archive events older than 7 days', async () => {
|
||||
const { vitals } = env;
|
||||
const now = Date.now();
|
||||
const eightDaysAgo = now - 8 * 86_400_000;
|
||||
|
||||
// Insert old events
|
||||
for (let i = 0; i < 10; i++) {
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: eightDaysAgo + i * 1000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
});
|
||||
}
|
||||
// Insert recent events
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: now - 1000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
});
|
||||
|
||||
const result = gcVitals(vitals);
|
||||
const result = await gcVitals(vitals);
|
||||
expect(result.archivedCount).toBe(10);
|
||||
// Recent event should remain
|
||||
const remaining = vitals.queryByKind('vital', {});
|
||||
const remaining = await vitals.queryByKind('vital', {});
|
||||
expect(remaining.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should downsample events in 1h-24h tier to 1 per 5 min', () => {
|
||||
it('should downsample events in 1h-24h tier to 1 per 5 min', async () => {
|
||||
const { vitals } = env;
|
||||
const now = Date.now();
|
||||
const twoHoursAgo = now - 2 * 3_600_000;
|
||||
|
||||
// Insert 120 events over 2 hours (1 per minute)
|
||||
for (let i = 0; i < 120; i++) {
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: twoHoursAgo + i * 60_000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
});
|
||||
}
|
||||
|
||||
const before = vitals.queryByKind('vital', {}).length;
|
||||
const result = gcVitals(vitals);
|
||||
const before = (await vitals.queryByKind('vital', {})).length;
|
||||
const result = await gcVitals(vitals);
|
||||
expect(result.downsampledCount).toBeGreaterThan(0);
|
||||
|
||||
const after = vitals.queryByKind('vital', {}).length;
|
||||
const after = (await vitals.queryByKind('vital', {})).length;
|
||||
// Should have fewer events after downsample
|
||||
expect(after).toBeLessThan(before);
|
||||
// Events in the <1h window should be untouched (60 events),
|
||||
@@ -96,22 +96,22 @@ describe('gcVitals', () => {
|
||||
expect(after).toBeGreaterThanOrEqual(60);
|
||||
});
|
||||
|
||||
it('should downsample events in 24h-7d tier to 1 per hour', () => {
|
||||
it('should downsample events in 24h-7d tier to 1 per hour', async () => {
|
||||
const { vitals } = env;
|
||||
const now = Date.now();
|
||||
const threeDaysAgo = now - 3 * 86_400_000;
|
||||
|
||||
// Insert 72 events over 3 days (1 per hour)
|
||||
for (let i = 0; i < 72; i++) {
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: threeDaysAgo + i * 3_600_000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
});
|
||||
}
|
||||
|
||||
const _before = vitals.queryByKind('vital', {});
|
||||
const result = gcVitals(vitals);
|
||||
const _before = await vitals.queryByKind('vital', {});
|
||||
const result = await gcVitals(vitals);
|
||||
|
||||
// Events in 24h-7d tier should be kept at 1/hour (already at that rate)
|
||||
// Events in 1h-24h tier should be kept at 1/5min
|
||||
@@ -121,30 +121,30 @@ describe('gcVitals', () => {
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should not touch events less than 1h old', () => {
|
||||
it('should not touch events less than 1h old', async () => {
|
||||
const { vitals } = env;
|
||||
const now = Date.now();
|
||||
|
||||
// Insert recent events (last 30 min, every 15s)
|
||||
for (let i = 0; i < 120; i++) {
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: now - 30 * 60_000 + i * 15_000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
});
|
||||
}
|
||||
|
||||
const result = gcVitals(vitals);
|
||||
const result = await gcVitals(vitals);
|
||||
expect(result.downsampledCount).toBe(0);
|
||||
expect(result.archivedCount).toBe(0);
|
||||
|
||||
const remaining = vitals.queryByKind('vital', {});
|
||||
const remaining = await vitals.queryByKind('vital', {});
|
||||
expect(remaining.length).toBe(120);
|
||||
});
|
||||
|
||||
it('should handle empty store gracefully', () => {
|
||||
it('should handle empty store gracefully', async () => {
|
||||
const { vitals } = env;
|
||||
const result = gcVitals(vitals);
|
||||
const result = await gcVitals(vitals);
|
||||
expect(result.downsampledCount).toBe(0);
|
||||
expect(result.archivedCount).toBe(0);
|
||||
});
|
||||
@@ -161,12 +161,12 @@ describe('gcOrphanObjects', () => {
|
||||
teardown();
|
||||
});
|
||||
|
||||
it('should delete objects not referenced by any event', () => {
|
||||
it('should delete objects not referenced by any event', async () => {
|
||||
const { system, objectsDir } = env;
|
||||
|
||||
// Create a referenced object
|
||||
const hash = system.putObject({ data: 'referenced' });
|
||||
system.appendEvent({
|
||||
const hash = await system.putObject({ data: 'referenced' });
|
||||
await system.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'collect',
|
||||
key: 'test',
|
||||
@@ -176,11 +176,11 @@ describe('gcOrphanObjects', () => {
|
||||
// Create an orphan object file
|
||||
writeFileSync(join(objectsDir, 'orphan123.json'), '{"data":"orphan"}');
|
||||
|
||||
const deleted = gcOrphanObjects([system], objectsDir);
|
||||
const deleted = await gcOrphanObjects([system], objectsDir);
|
||||
expect(deleted).toBe(1);
|
||||
|
||||
// Referenced object should still exist
|
||||
const obj = system.getObject(hash);
|
||||
const obj = await system.getObject(hash);
|
||||
expect(obj).not.toBeNull();
|
||||
|
||||
// Orphan should be gone
|
||||
@@ -188,40 +188,40 @@ describe('gcOrphanObjects', () => {
|
||||
expect(files).not.toContain('orphan123.json');
|
||||
});
|
||||
|
||||
it('should not delete objects referenced by events', () => {
|
||||
it('should not delete objects referenced by events', async () => {
|
||||
const { system, objectsDir } = env;
|
||||
|
||||
const hash1 = system.putObject({ data: 'a' });
|
||||
const hash2 = system.putObject({ data: 'b' });
|
||||
system.appendEvent({
|
||||
const hash1 = await system.putObject({ data: 'a' });
|
||||
const hash2 = await system.putObject({ data: 'b' });
|
||||
await system.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'collect',
|
||||
key: 'a',
|
||||
hash: hash1,
|
||||
});
|
||||
system.appendEvent({
|
||||
await system.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'effect',
|
||||
key: 'b',
|
||||
hash: hash2,
|
||||
});
|
||||
|
||||
const deleted = gcOrphanObjects([system], objectsDir);
|
||||
const deleted = await gcOrphanObjects([system], objectsDir);
|
||||
expect(deleted).toBe(0);
|
||||
|
||||
const files = readdirSync(objectsDir);
|
||||
expect(files.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle empty objects directory', () => {
|
||||
it('should handle empty objects directory', async () => {
|
||||
const { system, objectsDir } = env;
|
||||
const deleted = gcOrphanObjects([system], objectsDir);
|
||||
const deleted = await gcOrphanObjects([system], objectsDir);
|
||||
expect(deleted).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle non-existent objects directory', () => {
|
||||
it('should handle non-existent objects directory', async () => {
|
||||
const { system } = env;
|
||||
const deleted = gcOrphanObjects([system], '/nonexistent/path');
|
||||
const deleted = await gcOrphanObjects([system], '/nonexistent/path');
|
||||
expect(deleted).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -237,13 +237,13 @@ describe('runGc', () => {
|
||||
teardown();
|
||||
});
|
||||
|
||||
it('should run full GC cycle and write gc event to system store', () => {
|
||||
it('should run full GC cycle and write gc event to system store', async () => {
|
||||
const { vitals, system, objectsDir } = env;
|
||||
const now = Date.now();
|
||||
|
||||
// Add some old vitals
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: now - 8 * 86_400_000 + i * 1000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -253,7 +253,7 @@ describe('runGc', () => {
|
||||
// Add orphan object
|
||||
writeFileSync(join(objectsDir, 'deadbeef.json'), '{}');
|
||||
|
||||
const result = runGc({
|
||||
const result = await runGc({
|
||||
vitalsStore: vitals,
|
||||
systemStore: system,
|
||||
allStores: [system, vitals],
|
||||
@@ -265,7 +265,7 @@ describe('runGc', () => {
|
||||
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Check gc event was written
|
||||
const gcEvents = system.queryByKind('gc', {});
|
||||
const gcEvents = await system.queryByKind('gc', {});
|
||||
expect(gcEvents.length).toBe(1);
|
||||
expect(gcEvents[0].key).toBe('vitals');
|
||||
const meta = JSON.parse(gcEvents[0].meta!);
|
||||
@@ -285,12 +285,12 @@ describe('createGcTrigger', () => {
|
||||
teardown();
|
||||
});
|
||||
|
||||
it('should trigger GC after N ticks', () => {
|
||||
it('should trigger GC after N ticks', async () => {
|
||||
const { vitals, system, objectsDir } = env;
|
||||
const now = Date.now();
|
||||
|
||||
// Add old data to clean up
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: now - 8 * 86_400_000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -307,26 +307,30 @@ describe('createGcTrigger', () => {
|
||||
// Tick 1, 2: no GC
|
||||
trigger();
|
||||
trigger();
|
||||
expect(system.queryByKind('gc', {}).length).toBe(0);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect((await system.queryByKind('gc', {})).length).toBe(0);
|
||||
|
||||
// Tick 3: GC fires
|
||||
trigger();
|
||||
expect(system.queryByKind('gc', {}).length).toBe(1);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
expect((await system.queryByKind('gc', {})).length).toBe(1);
|
||||
|
||||
// Tick 4, 5: no GC again
|
||||
trigger();
|
||||
trigger();
|
||||
expect(system.queryByKind('gc', {}).length).toBe(1);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect((await system.queryByKind('gc', {})).length).toBe(1);
|
||||
|
||||
// Tick 6: GC fires again
|
||||
trigger();
|
||||
expect(system.queryByKind('gc', {}).length).toBe(2);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
expect((await system.queryByKind('gc', {})).length).toBe(2);
|
||||
});
|
||||
|
||||
it('should be a no-op when gc is disabled', () => {
|
||||
it('should be a no-op when gc is disabled', async () => {
|
||||
const { vitals, system, objectsDir } = env;
|
||||
|
||||
vitals.appendEvent({
|
||||
await vitals.appendEvent({
|
||||
occurredAt: Date.now() - 8 * 86_400_000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -341,8 +345,9 @@ describe('createGcTrigger', () => {
|
||||
});
|
||||
|
||||
for (let i = 0; i < 300; i++) trigger();
|
||||
expect(system.queryByKind('gc', {}).length).toBe(0);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect((await system.queryByKind('gc', {})).length).toBe(0);
|
||||
// Old event should still be there
|
||||
expect(vitals.queryByKind('vital', {}).length).toBe(1);
|
||||
expect((await vitals.queryByKind('vital', {})).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
+134
-134
@@ -203,13 +203,13 @@ describe('rebuildSnapshot', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
store.close();
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('rebuilds snapshot from collect events', () => {
|
||||
const hash = store.putObject({ memoryPct: 72, cpuIdlePct: 85 });
|
||||
store.appendEvent({
|
||||
it('rebuilds snapshot from collect events', async () => {
|
||||
const hash = await store.putObject({ memoryPct: 72, cpuIdlePct: 85 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -227,16 +227,16 @@ describe('rebuildSnapshot', () => {
|
||||
expect(snapshot.system.refreshedAt).toBe(1000);
|
||||
});
|
||||
|
||||
it('returns latest collect for each key', () => {
|
||||
const h1 = store.putObject({ value: 1 });
|
||||
const h2 = store.putObject({ value: 2 });
|
||||
store.appendEvent({
|
||||
it('returns latest collect for each key', async () => {
|
||||
const h1 = await store.putObject({ value: 1 });
|
||||
const h2 = await store.putObject({ value: 2 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: h1,
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -261,16 +261,16 @@ describe('rebuildSnapshot', () => {
|
||||
expect(snapshot.nonexistent).toBe(undefined);
|
||||
});
|
||||
|
||||
it('multiple sense keys each get their latest collect', () => {
|
||||
const h1 = store.putObject({ mem: 50 });
|
||||
const h2 = store.putObject({ cpu: 80 });
|
||||
store.appendEvent({
|
||||
it('multiple sense keys each get their latest collect', async () => {
|
||||
const h1 = await store.putObject({ mem: 50 });
|
||||
const h2 = await store.putObject({ cpu: 80 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'memory',
|
||||
hash: h1,
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1500,
|
||||
kind: 'collect',
|
||||
key: 'cpu',
|
||||
@@ -313,14 +313,14 @@ describe('runPulse effects', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
store.close();
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('all effects (including collect) are passed to execute', async () => {
|
||||
// Pre-populate with data so snapshot has something
|
||||
const initHash = store.putObject({ v: 1 });
|
||||
store.appendEvent({
|
||||
const initHash = await store.putObject({ v: 1 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'sys',
|
||||
@@ -367,7 +367,7 @@ describe('runPulse effects', () => {
|
||||
expect(executedEffects.some((e) => e.kind === 'notify')).toBeTruthy();
|
||||
|
||||
// Verify effect events were recorded
|
||||
const effectEvents = store.queryByKind('effect');
|
||||
const effectEvents = await store.queryByKind('effect');
|
||||
expect(effectEvents.length >= 1).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -382,7 +382,7 @@ describe('runPulse effects', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
|
||||
const tickEvents = store.queryByKind('tick');
|
||||
const tickEvents = await store.queryByKind('tick');
|
||||
expect(tickEvents.length >= 1).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -423,8 +423,8 @@ describe('Moore machine property', () => {
|
||||
|
||||
try {
|
||||
// Pre-populate with initial value
|
||||
const initHash = store.putObject({ v: 1 });
|
||||
store.appendEvent({
|
||||
const initHash = await store.putObject({ v: 1 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'sys',
|
||||
@@ -440,8 +440,8 @@ describe('Moore machine property', () => {
|
||||
expect(before.sys.data.v).toBe(1);
|
||||
|
||||
// Simulate what runPulse does: execute a collect effect
|
||||
const newHash = store.putObject({ v: 999 });
|
||||
store.appendEvent({
|
||||
const newHash = await store.putObject({ v: 999 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'collect',
|
||||
key: 'sys',
|
||||
@@ -455,7 +455,7 @@ describe('Moore machine property', () => {
|
||||
const after = rebuildSnapshot<S>(store, ['sys']);
|
||||
expect(after.sys.data.v).toBe(999);
|
||||
} finally {
|
||||
store.close();
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -490,7 +490,7 @@ describe('findEffectiveEpoch', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
store.close();
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -498,44 +498,44 @@ describe('findEffectiveEpoch', () => {
|
||||
expect(findEffectiveEpoch(store)).toBe(null);
|
||||
});
|
||||
|
||||
it('with promote → returns it', () => {
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
it('with promote → returns it', async () => {
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
expect(epoch).toBeTruthy();
|
||||
expect(epoch?.codeRev).toBe('v1');
|
||||
});
|
||||
|
||||
it('multiple promotes → returns latest', () => {
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
it('multiple promotes → returns latest', async () => {
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
expect(epoch).toBeTruthy();
|
||||
expect(epoch?.codeRev).toBe('v2');
|
||||
});
|
||||
|
||||
it('with rollback → returns rolled-back-to promote', () => {
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
it('with rollback → returns rolled-back-to promote', async () => {
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 3000,
|
||||
kind: 'rollback',
|
||||
codeRev: 'v1',
|
||||
meta: JSON.stringify({ to: 'v1', from: 'v2' }),
|
||||
});
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
expect(epoch).toBeTruthy();
|
||||
expect(epoch?.codeRev).toBe('v1');
|
||||
});
|
||||
|
||||
it('rollback with only codeRev (no meta.to) → uses codeRev', () => {
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
it('rollback with only codeRev (no meta.to) → uses codeRev', async () => {
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 3000,
|
||||
kind: 'rollback',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
expect(epoch).toBeTruthy();
|
||||
expect(epoch?.codeRev).toBe('v1');
|
||||
});
|
||||
@@ -557,24 +557,24 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
store.close();
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('only reads events after epoch with matching code_rev', () => {
|
||||
const h1 = store.putObject({ value: 'v1-data' });
|
||||
const h2 = store.putObject({ value: 'v2-data' });
|
||||
it('only reads events after epoch with matching code_rev', async () => {
|
||||
const h1 = await store.putObject({ value: 'v1-data' });
|
||||
const h2 = await store.putObject({ value: 'v2-data' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1100,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: h1,
|
||||
codeRev: 'v1',
|
||||
});
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 2100,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -583,7 +583,7 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
});
|
||||
|
||||
// Using v1 epoch should get v1 data
|
||||
const epochV1 = store.getLatestWhere({ kind: 'promote', codeRev: 'v1' });
|
||||
const epochV1 = await store.getLatestWhere({ kind: 'promote', codeRev: 'v1' });
|
||||
const snapshotV1 = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -591,7 +591,7 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
expect(snapshotV1.system.data.value).toBe('v1-data');
|
||||
|
||||
// Using v2 epoch should get v2 data
|
||||
const epochV2 = store.getLatestWhere({ kind: 'promote', codeRev: 'v2' });
|
||||
const epochV2 = await store.getLatestWhere({ kind: 'promote', codeRev: 'v2' });
|
||||
const snapshotV2 = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -599,20 +599,20 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
expect(snapshotV2.system.data.value).toBe('v2-data');
|
||||
});
|
||||
|
||||
it('v2 events not visible in v1 epoch', () => {
|
||||
const h1 = store.putObject({ value: 'v1-data' });
|
||||
const h2 = store.putObject({ value: 'v2-data' });
|
||||
it('v2 events not visible in v1 epoch', async () => {
|
||||
const h1 = await store.putObject({ value: 'v1-data' });
|
||||
const h2 = await store.putObject({ value: 'v2-data' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1100,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: h1,
|
||||
codeRev: 'v1',
|
||||
});
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 2100,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -621,7 +621,7 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
});
|
||||
|
||||
// v1 epoch should NOT see v2 collect (different code_rev)
|
||||
const epochV1 = store.getLatestWhere({ kind: 'promote', codeRev: 'v1' });
|
||||
const epochV1 = await store.getLatestWhere({ kind: 'promote', codeRev: 'v1' });
|
||||
const snapshot = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -629,11 +629,11 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
expect(snapshot.system.data.value).toBe('v1-data');
|
||||
});
|
||||
|
||||
it('migrate events used for initial snapshot after promote', () => {
|
||||
const hMigrated = store.putObject({ value: 'migrated' });
|
||||
it('migrate events used for initial snapshot after promote', async () => {
|
||||
const hMigrated = await store.putObject({ value: 'migrated' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1001,
|
||||
kind: 'migrate',
|
||||
key: 'system',
|
||||
@@ -641,7 +641,7 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
codeRev: 'v2',
|
||||
});
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
const snapshot = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -649,11 +649,11 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
expect(snapshot.system.data.value).toBe('migrated');
|
||||
});
|
||||
|
||||
it('init events used when no collect or migrate exist', () => {
|
||||
const hInit = store.putObject({ value: 'initial' });
|
||||
it('init events used when no collect or migrate exist', async () => {
|
||||
const hInit = await store.putObject({ value: 'initial' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1001,
|
||||
kind: 'init',
|
||||
key: 'system',
|
||||
@@ -661,7 +661,7 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
const snapshot = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -669,27 +669,27 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
expect(snapshot.system.data.value).toBe('initial');
|
||||
});
|
||||
|
||||
it('collect takes priority over migrate and init', () => {
|
||||
const hInit = store.putObject({ value: 'initial' });
|
||||
const hMigrate = store.putObject({ value: 'migrated' });
|
||||
const hCollect = store.putObject({ value: 'collected' });
|
||||
it('collect takes priority over migrate and init', async () => {
|
||||
const hInit = await store.putObject({ value: 'initial' });
|
||||
const hMigrate = await store.putObject({ value: 'migrated' });
|
||||
const hCollect = await store.putObject({ value: 'collected' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1001,
|
||||
kind: 'init',
|
||||
key: 'system',
|
||||
hash: hInit,
|
||||
codeRev: 'v2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1002,
|
||||
kind: 'migrate',
|
||||
key: 'system',
|
||||
hash: hMigrate,
|
||||
codeRev: 'v2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1003,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -697,7 +697,7 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
codeRev: 'v2',
|
||||
});
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
const snapshot = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -705,16 +705,16 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
expect(snapshot.system.data.value).toBe('collected');
|
||||
});
|
||||
|
||||
it('no epoch → falls back to latest collect (backward compat)', () => {
|
||||
const h1 = store.putObject({ value: 'first' });
|
||||
const h2 = store.putObject({ value: 'second' });
|
||||
store.appendEvent({
|
||||
it('no epoch → falls back to latest collect (backward compat)', async () => {
|
||||
const h1 = await store.putObject({ value: 'first' });
|
||||
const h2 = await store.putObject({ value: 'second' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: h1,
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -728,34 +728,34 @@ describe('rebuildSnapshot with epoch', () => {
|
||||
expect(snapshot.system.data.value).toBe('second');
|
||||
});
|
||||
|
||||
it('rollback epoch reads correct version data', () => {
|
||||
const h1 = store.putObject({ value: 'v1-data' });
|
||||
const h2 = store.putObject({ value: 'v2-data' });
|
||||
it('rollback epoch reads correct version data', async () => {
|
||||
const h1 = await store.putObject({ value: 'v1-data' });
|
||||
const h2 = await store.putObject({ value: 'v2-data' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1100,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: h1,
|
||||
codeRev: 'v1',
|
||||
});
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 2100,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: h2,
|
||||
codeRev: 'v2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 3000,
|
||||
kind: 'rollback',
|
||||
codeRev: 'v1',
|
||||
meta: JSON.stringify({ to: 'v1', from: 'v2' }),
|
||||
});
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
expect(epoch).toBeTruthy();
|
||||
expect(epoch?.codeRev).toBe('v1');
|
||||
|
||||
@@ -788,22 +788,22 @@ describe('rebuildSnapshot vitals priority', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
store.close();
|
||||
vitalsStore.close();
|
||||
await store.close();
|
||||
await vitalsStore.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reads latest from vitals store over system events', () => {
|
||||
const hEvent = store.putObject({ value: 'from-event' });
|
||||
const hVital = vitalsStore.putObject({ value: 'from-vital' });
|
||||
it('reads latest from vitals store over system events', async () => {
|
||||
const hEvent = await store.putObject({ value: 'from-event' });
|
||||
const hVital = await vitalsStore.putObject({ value: 'from-vital' });
|
||||
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: hEvent,
|
||||
});
|
||||
vitalsStore.appendEvent({
|
||||
await vitalsStore.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
@@ -818,9 +818,9 @@ describe('rebuildSnapshot vitals priority', () => {
|
||||
expect(snapshot.system.refreshedAt).toBe(2000);
|
||||
});
|
||||
|
||||
it('falls back to system events when vitals empty', () => {
|
||||
const h = store.putObject({ value: 'event-only' });
|
||||
store.appendEvent({
|
||||
it('falls back to system events when vitals empty', async () => {
|
||||
const h = await store.putObject({ value: 'event-only' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
@@ -834,26 +834,26 @@ describe('rebuildSnapshot vitals priority', () => {
|
||||
expect(snapshot.system.data.value).toBe('event-only');
|
||||
});
|
||||
|
||||
it('vitals priority with epoch — still reads vitals first', () => {
|
||||
const hVital = vitalsStore.putObject({ value: 'vital-data' });
|
||||
const hEvent = store.putObject({ value: 'event-data' });
|
||||
it('vitals priority with epoch — still reads vitals first', async () => {
|
||||
const hVital = await vitalsStore.putObject({ value: 'vital-data' });
|
||||
const hEvent = await store.putObject({ value: 'event-data' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v1' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1100,
|
||||
kind: 'collect',
|
||||
key: 'system',
|
||||
hash: hEvent,
|
||||
codeRev: 'v1',
|
||||
});
|
||||
vitalsStore.appendEvent({
|
||||
await vitalsStore.appendEvent({
|
||||
occurredAt: 1200,
|
||||
kind: 'vital',
|
||||
key: 'system',
|
||||
hash: hVital,
|
||||
});
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
const snapshot = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -861,11 +861,11 @@ describe('rebuildSnapshot vitals priority', () => {
|
||||
expect(snapshot.system.data.value).toBe('vital-data');
|
||||
});
|
||||
|
||||
it('falls back to migrate events when no vitals', () => {
|
||||
const hMigrate = store.putObject({ value: 'migrated' });
|
||||
it('falls back to migrate events when no vitals', async () => {
|
||||
const hMigrate = await store.putObject({ value: 'migrated' });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v2' });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'promote', codeRev: 'v2' });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1001,
|
||||
kind: 'migrate',
|
||||
key: 'system',
|
||||
@@ -873,7 +873,7 @@ describe('rebuildSnapshot vitals priority', () => {
|
||||
codeRev: 'v2',
|
||||
});
|
||||
|
||||
const epoch = findEffectiveEpoch(store);
|
||||
const epoch = await findEffectiveEpoch(store);
|
||||
const snapshot = rebuildSnapshot<{
|
||||
timestamp: number;
|
||||
system: Sensed<{ value: string }>;
|
||||
@@ -898,13 +898,13 @@ describe('runPulse execute-driven collect', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
store.close();
|
||||
await store.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('execute can write events for collect effects', async () => {
|
||||
const initHash = store.putObject({ v: 1 });
|
||||
store.appendEvent({
|
||||
const initHash = await store.putObject({ v: 1 });
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'sys',
|
||||
@@ -928,8 +928,8 @@ describe('runPulse execute-driven collect', () => {
|
||||
for (const effect of effects) {
|
||||
if (effect.kind === 'collect' && effect.key === 'sys') {
|
||||
const data = { v: 99 };
|
||||
const hash = store.putObject(data);
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject(data);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'collect',
|
||||
key: 'sys',
|
||||
@@ -946,7 +946,7 @@ describe('runPulse execute-driven collect', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
const collectEvents = store.queryByKind('collect', { key: 'sys' });
|
||||
const collectEvents = await store.queryByKind('collect', { key: 'sys' });
|
||||
expect(collectEvents.length >= 2).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -968,14 +968,14 @@ describe('runPulse with ScopedStore', () => {
|
||||
afterEach(async () => {
|
||||
// Wait for async watcher/tick loops to settle before closing DBs
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
scopedStore.close();
|
||||
await scopedStore.close();
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('tick and effect events are written to _system scope', async () => {
|
||||
const systemStore = scopedStore.scope('_system');
|
||||
const initHash = systemStore.putObject({ v: 1 });
|
||||
systemStore.appendEvent({
|
||||
const initHash = await systemStore.putObject({ v: 1 });
|
||||
await systemStore.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'sys',
|
||||
@@ -1003,15 +1003,15 @@ describe('runPulse with ScopedStore', () => {
|
||||
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
|
||||
const tickEvents = systemStore.queryByKind('tick');
|
||||
const tickEvents = await systemStore.queryByKind('tick');
|
||||
expect(tickEvents.length >= 1).toBeTruthy();
|
||||
|
||||
const effectEvents = systemStore.queryByKind('effect');
|
||||
const effectEvents = await systemStore.queryByKind('effect');
|
||||
expect(effectEvents.length >= 1).toBeTruthy();
|
||||
|
||||
// _vitals scope should NOT have tick/effect events
|
||||
const vitalsStore = scopedStore.scope('_vitals');
|
||||
const vitalsTicks = vitalsStore.queryByKind('tick');
|
||||
const vitalsTicks = await vitalsStore.queryByKind('tick');
|
||||
expect(vitalsTicks.length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -1039,13 +1039,13 @@ describe('runPulse with ScopedStore', () => {
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
|
||||
// Vitals should be in _vitals scope
|
||||
const vitalEvents = vitalsStore.queryByKind('vital', {
|
||||
const vitalEvents = await vitalsStore.queryByKind('vital', {
|
||||
key: 'test_metric',
|
||||
});
|
||||
expect(vitalEvents.length >= 1).toBeTruthy();
|
||||
|
||||
// _system scope should NOT have vital events from the watcher
|
||||
const systemVitals = systemStore.queryByKind('vital', {
|
||||
const systemVitals = await systemStore.queryByKind('vital', {
|
||||
key: 'test_metric',
|
||||
});
|
||||
expect(systemVitals.length).toBe(0);
|
||||
@@ -1056,8 +1056,8 @@ describe('runPulse with ScopedStore', () => {
|
||||
const vitalsStore = scopedStore.scope('_vitals');
|
||||
|
||||
// Write a vital to _vitals scope
|
||||
const vitalHash = vitalsStore.putObject({ v: 99 });
|
||||
vitalsStore.appendEvent({
|
||||
const vitalHash = await vitalsStore.putObject({ v: 99 });
|
||||
await vitalsStore.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'vital',
|
||||
key: 'sys',
|
||||
@@ -1065,8 +1065,8 @@ describe('runPulse with ScopedStore', () => {
|
||||
});
|
||||
|
||||
// Write an older collect to _system scope
|
||||
const collectHash = systemStore.putObject({ v: 1 });
|
||||
systemStore.appendEvent({
|
||||
const collectHash = await systemStore.putObject({ v: 1 });
|
||||
await systemStore.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'collect',
|
||||
key: 'sys',
|
||||
@@ -1134,6 +1134,6 @@ describe('runPulse with ScopedStore', () => {
|
||||
expect(executedEffects.length >= 1).toBeTruthy();
|
||||
expect(executedEffects.some((e) => e.kind === 'notify')).toBeTruthy();
|
||||
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,13 +29,13 @@ describe('objects table (createStore)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.close();
|
||||
afterEach(async () => {
|
||||
await store.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('1. createObject returns an integer id', () => {
|
||||
const id = store.createObject({
|
||||
it('1. createObject returns an integer id', async () => {
|
||||
const id = await store.createObject({
|
||||
objectType: 'user',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
@@ -44,13 +44,13 @@ describe('objects table (createStore)', () => {
|
||||
expect(id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('2. getObjectInstance retrieves the created object', () => {
|
||||
const id = store.createObject({
|
||||
it('2. getObjectInstance retrieves the created object', async () => {
|
||||
const id = await store.createObject({
|
||||
objectType: 'user',
|
||||
externalId: 'usr_123',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
const obj = store.getObjectInstance(id);
|
||||
const obj = await store.getObjectInstance(id);
|
||||
expect(obj).not.toBeNull();
|
||||
expect(obj!.id).toBe(id);
|
||||
expect(obj!.objectType).toBe('user');
|
||||
@@ -59,31 +59,31 @@ describe('objects table (createStore)', () => {
|
||||
expect(typeof obj!.createdAt).toBe('number');
|
||||
});
|
||||
|
||||
it('3. queryObjectsByType filters by type', () => {
|
||||
store.createObject({ objectType: 'user', externalId: 'a', codeRev: 'v1' });
|
||||
store.createObject({ objectType: 'user', externalId: 'b', codeRev: 'v1' });
|
||||
store.createObject({
|
||||
it('3. queryObjectsByType filters by type', async () => {
|
||||
await store.createObject({ objectType: 'user', externalId: 'a', codeRev: 'v1' });
|
||||
await store.createObject({ objectType: 'user', externalId: 'b', codeRev: 'v1' });
|
||||
await store.createObject({
|
||||
objectType: 'order',
|
||||
externalId: 'o1',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
const users = store.queryObjectsByType('user');
|
||||
const users = await store.queryObjectsByType('user');
|
||||
expect(users.length).toBe(2);
|
||||
expect(users.every((o) => o.objectType === 'user')).toBe(true);
|
||||
|
||||
const orders = store.queryObjectsByType('order');
|
||||
const orders = await store.queryObjectsByType('order');
|
||||
expect(orders.length).toBe(1);
|
||||
expect(orders[0]!.objectType).toBe('order');
|
||||
});
|
||||
|
||||
it('4. externalId + same type is idempotent (returns existing id)', () => {
|
||||
const id1 = store.createObject({
|
||||
it('4. externalId + same type is idempotent (returns existing id)', async () => {
|
||||
const id1 = await store.createObject({
|
||||
objectType: 'user',
|
||||
externalId: 'usr_dup',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
const id2 = store.createObject({
|
||||
const id2 = await store.createObject({
|
||||
objectType: 'user',
|
||||
externalId: 'usr_dup',
|
||||
codeRev: 'v2',
|
||||
@@ -91,18 +91,18 @@ describe('objects table (createStore)', () => {
|
||||
expect(id1).toBe(id2);
|
||||
|
||||
// Only one row should exist
|
||||
const users = store.queryObjectsByType('user');
|
||||
const users = await store.queryObjectsByType('user');
|
||||
expect(users.length).toBe(1);
|
||||
});
|
||||
|
||||
it('5. appendEvent with objectId links event to object', () => {
|
||||
const objId = store.createObject({
|
||||
it('5. appendEvent with objectId links event to object', async () => {
|
||||
const objId = await store.createObject({
|
||||
objectType: 'task',
|
||||
externalId: 'tsk_1',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
const event = store.appendEvent({
|
||||
const event = await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'task-created',
|
||||
key: 'tsk_1',
|
||||
@@ -112,13 +112,13 @@ describe('objects table (createStore)', () => {
|
||||
expect(event.objectId).toBe(objId);
|
||||
|
||||
// Verify through getLatest
|
||||
const latest = store.getLatest('task-created', 'tsk_1');
|
||||
const latest = await store.getLatest('task-created', 'tsk_1');
|
||||
expect(latest).not.toBeNull();
|
||||
expect(latest!.objectId).toBe(objId);
|
||||
});
|
||||
|
||||
it('7. appendEvent without objectId keeps object_id null (backward compat)', () => {
|
||||
const event = store.appendEvent({
|
||||
it('7. appendEvent without objectId keeps object_id null (backward compat)', async () => {
|
||||
const event = await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'tick',
|
||||
key: 'sys',
|
||||
@@ -126,18 +126,18 @@ describe('objects table (createStore)', () => {
|
||||
|
||||
expect(event.objectId).toBeUndefined();
|
||||
|
||||
const latest = store.getLatest('tick', 'sys');
|
||||
const latest = await store.getLatest('tick', 'sys');
|
||||
expect(latest).not.toBeNull();
|
||||
expect(latest!.objectId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getObjectInstance returns null for non-existent id', () => {
|
||||
expect(store.getObjectInstance(99999)).toBeNull();
|
||||
it('getObjectInstance returns null for non-existent id', async () => {
|
||||
await expect(store.getObjectInstance(99999)).toBeNull();
|
||||
});
|
||||
|
||||
it('createObject without externalId creates distinct rows', () => {
|
||||
const id1 = store.createObject({ objectType: 'ephemeral', codeRev: 'v1' });
|
||||
const id2 = store.createObject({ objectType: 'ephemeral', codeRev: 'v1' });
|
||||
it('createObject without externalId creates distinct rows', async () => {
|
||||
const id1 = await store.createObject({ objectType: 'ephemeral', codeRev: 'v1' });
|
||||
const id2 = await store.createObject({ objectType: 'ephemeral', codeRev: 'v1' });
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
});
|
||||
@@ -156,46 +156,46 @@ describe('objects table (ScopedStore)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scopedStore.close();
|
||||
afterEach(async () => {
|
||||
await scopedStore.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('6. objects in different scopes are independent', () => {
|
||||
it('6. objects in different scopes are independent', async () => {
|
||||
const scopeA = scopedStore.scope('scope-a');
|
||||
const scopeB = scopedStore.scope('scope-b');
|
||||
|
||||
scopeA.createObject({
|
||||
await scopeA.createObject({
|
||||
objectType: 'user',
|
||||
externalId: 'u1',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
scopeA.createObject({
|
||||
await scopeA.createObject({
|
||||
objectType: 'user',
|
||||
externalId: 'u2',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
scopeB.createObject({
|
||||
await scopeB.createObject({
|
||||
objectType: 'user',
|
||||
externalId: 'u1',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
const usersA = scopeA.queryObjectsByType('user');
|
||||
const usersB = scopeB.queryObjectsByType('user');
|
||||
const usersA = await scopeA.queryObjectsByType('user');
|
||||
const usersB = await scopeB.queryObjectsByType('user');
|
||||
expect(usersA.length).toBe(2);
|
||||
expect(usersB.length).toBe(1);
|
||||
});
|
||||
|
||||
it('appendEvent with objectId in scoped store', () => {
|
||||
it('appendEvent with objectId in scoped store', async () => {
|
||||
const s = scopedStore.scope('test-scope');
|
||||
const objId = s.createObject({
|
||||
const objId = await s.createObject({
|
||||
objectType: 'item',
|
||||
externalId: 'item_1',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
const event = s.appendEvent({
|
||||
const event = await s.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'item-updated',
|
||||
key: 'item_1',
|
||||
@@ -203,7 +203,7 @@ describe('objects table (ScopedStore)', () => {
|
||||
});
|
||||
expect(event.objectId).toBe(objId);
|
||||
|
||||
const latest = s.getLatest('item-updated', 'item_1');
|
||||
const latest = await s.getLatest('item-updated', 'item_1');
|
||||
expect(latest!.objectId).toBe(objId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ function makeMockStore(
|
||||
}
|
||||
|
||||
describe('buildPersonasFromEvents', () => {
|
||||
test('registers a single persona', () => {
|
||||
test('registers a single persona', async () => {
|
||||
const store = makeMockStore([
|
||||
{
|
||||
id: 1,
|
||||
@@ -46,7 +46,7 @@ describe('buildPersonasFromEvents', () => {
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const personas = buildPersonasFromEvents(store);
|
||||
const personas = await buildPersonasFromEvents(store);
|
||||
expect(personas.size).toBe(1);
|
||||
const p = personas.get('xiaoju')!;
|
||||
expect(p.personaId).toBe('xiaoju');
|
||||
@@ -57,7 +57,7 @@ describe('buildPersonasFromEvents', () => {
|
||||
expect(p.updatedAt).toBe(1000);
|
||||
});
|
||||
|
||||
test('registers multiple personas', () => {
|
||||
test('registers multiple personas', async () => {
|
||||
const store = makeMockStore([
|
||||
{
|
||||
id: 1,
|
||||
@@ -82,13 +82,13 @@ describe('buildPersonasFromEvents', () => {
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const personas = buildPersonasFromEvents(store);
|
||||
const personas = await buildPersonasFromEvents(store);
|
||||
expect(personas.size).toBe(2);
|
||||
expect(personas.has('xiaoju')).toBe(true);
|
||||
expect(personas.has('cursor')).toBe(true);
|
||||
});
|
||||
|
||||
test('idempotent overwrite on duplicate register', () => {
|
||||
test('idempotent overwrite on duplicate register', async () => {
|
||||
const store = makeMockStore([
|
||||
{
|
||||
id: 1,
|
||||
@@ -113,7 +113,7 @@ describe('buildPersonasFromEvents', () => {
|
||||
}),
|
||||
},
|
||||
]);
|
||||
const personas = buildPersonasFromEvents(store);
|
||||
const personas = await buildPersonasFromEvents(store);
|
||||
expect(personas.size).toBe(1);
|
||||
const p = personas.get('xiaoju')!;
|
||||
expect(p.name).toBe('小橘 v2');
|
||||
@@ -122,7 +122,7 @@ describe('buildPersonasFromEvents', () => {
|
||||
expect(p.registeredAt).toBe(2000);
|
||||
});
|
||||
|
||||
test('persona-updated partial update', () => {
|
||||
test('persona-updated partial update', async () => {
|
||||
const store = makeMockStore([
|
||||
{
|
||||
id: 1,
|
||||
@@ -142,7 +142,7 @@ describe('buildPersonasFromEvents', () => {
|
||||
meta: JSON.stringify({ personaId: 'xiaoju', container: 'cursor' }),
|
||||
},
|
||||
]);
|
||||
const personas = buildPersonasFromEvents(store);
|
||||
const personas = await buildPersonasFromEvents(store);
|
||||
const p = personas.get('xiaoju')!;
|
||||
expect(p.container).toBe('cursor');
|
||||
expect(p.capabilities).toEqual(['ops']); // unchanged
|
||||
@@ -151,9 +151,9 @@ describe('buildPersonasFromEvents', () => {
|
||||
expect(p.registeredAt).toBe(1000);
|
||||
});
|
||||
|
||||
test('empty event stream returns empty Map', () => {
|
||||
test('empty event stream returns empty Map', async () => {
|
||||
const store = makeMockStore([]);
|
||||
const personas = buildPersonasFromEvents(store);
|
||||
const personas = await buildPersonasFromEvents(store);
|
||||
expect(personas.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ function insertTestEvent(
|
||||
return Number(result.lastInsertRowid);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
// Create temp directory
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'pulse-test-'));
|
||||
|
||||
@@ -76,12 +76,12 @@ beforeEach(() => {
|
||||
scopeDb.exec('PRAGMA journal_mode = WAL');
|
||||
scopeDb.exec(EVENTS_SCHEMA);
|
||||
scopeDb.exec(PROJECTIONS_SCHEMA);
|
||||
initDefsSchema(scopeDb);
|
||||
await initDefsSchema(scopeDb);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
// Close databases
|
||||
if (scopeDb) scopeDb.close();
|
||||
if (scopeDb) await scopeDb.close();
|
||||
|
||||
// Clean up temp directory
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
@@ -274,7 +274,7 @@ describe('Projection Engine', () => {
|
||||
await resetProjections(scopeDb, newCodeRev);
|
||||
|
||||
// Check that projection was recalculated with new logic
|
||||
const newState = getProjectionState(scopeDb, 'value-tracker');
|
||||
const newState = await getProjectionState(scopeDb, 'value-tracker');
|
||||
expect(newState).not.toBeNull();
|
||||
expect(newState!.value).toEqual({ product: 6000, count: 3 }); // 10 * 20 * 30 = 6000
|
||||
expect(newState!.codeRev).toBe(newCodeRev);
|
||||
@@ -316,12 +316,12 @@ describe('Projection Engine', () => {
|
||||
|
||||
// Start with new code_rev
|
||||
await resetProjections(scopeDb, newCodeRev);
|
||||
let state = getProjectionState(scopeDb, 'rollback-test');
|
||||
let state = await getProjectionState(scopeDb, 'rollback-test');
|
||||
expect(state!.value).toEqual({ type: 'count', value: 2 }); // counting events
|
||||
|
||||
// Rollback to old code_rev
|
||||
await resetProjections(scopeDb, oldCodeRev);
|
||||
state = getProjectionState(scopeDb, 'rollback-test');
|
||||
state = await getProjectionState(scopeDb, 'rollback-test');
|
||||
expect(state!.value).toEqual({ type: 'sum', value: 15 }); // summing values: 5 + 10
|
||||
expect(state!.codeRev).toBe(oldCodeRev);
|
||||
});
|
||||
@@ -517,7 +517,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
// Promote to V2
|
||||
await resetProjections(scopeDb, v2);
|
||||
state = getProjectionState(scopeDb, 'workflow-test')!;
|
||||
state = await getProjectionState(scopeDb, 'workflow-test')!;
|
||||
expect(state.value).toBe(30); // 1 * 5 * 3 * 2 = 30
|
||||
expect(state.codeRev).toBe(v2);
|
||||
|
||||
@@ -528,7 +528,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
// Rollback to V1
|
||||
await resetProjections(scopeDb, v1);
|
||||
state = getProjectionState(scopeDb, 'workflow-test')!;
|
||||
state = await getProjectionState(scopeDb, 'workflow-test')!;
|
||||
expect(state.value).toBe(20); // 0 + 5 + 3 + 2 + 10 = 20
|
||||
expect(state.codeRev).toBe(v1);
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('Review Fixes', () => {
|
||||
const testDir = '/tmp/pulse-review-fixes-test';
|
||||
const scopeDbPath = `${testDir}/test-scope.db`;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
// Clean up any existing test data
|
||||
try {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
@@ -76,11 +76,11 @@ describe('Review Fixes', () => {
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`);
|
||||
initDefsSchema(scopeDb);
|
||||
await initDefsSchema(scopeDb);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scopeDb?.close();
|
||||
afterEach(async () => {
|
||||
await scopeDb?.close();
|
||||
try {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
@@ -46,7 +46,7 @@ beforeEach(async () => {
|
||||
afterEach(async () => {
|
||||
// Wait for async runPulseV2 loops to settle before cleanup
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
scopedStore?.close();
|
||||
await scopedStore?.close();
|
||||
rmSync(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('RuleDef Declaration', () => {
|
||||
expect(ruleDef.rule).toBe(testRule);
|
||||
});
|
||||
|
||||
test('should validate projection path format', () => {
|
||||
test('should validate projection path format', async () => {
|
||||
const ruleDef: RuleDef<any, any> = {
|
||||
name: 'invalid-paths',
|
||||
projections: ['valid/path', 'invalid-path', 'also/valid/path'],
|
||||
@@ -79,7 +79,7 @@ describe('RuleDef Declaration', () => {
|
||||
};
|
||||
|
||||
// Test that buildSnapshotFromProjections handles invalid format gracefully
|
||||
const snapshot = buildSnapshotFromProjections(
|
||||
const snapshot = await buildSnapshotFromProjections(
|
||||
scopedStore,
|
||||
ruleDef.projections,
|
||||
);
|
||||
@@ -99,7 +99,7 @@ describe('Cross-Scope Projection Reading', () => {
|
||||
const vitalsScope = scopedStore.scope('_vitals');
|
||||
const nekoScope = scopedStore.scope('neko');
|
||||
|
||||
vitalsScope.appendEvent({
|
||||
await vitalsScope.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'metric',
|
||||
key: 'cpu',
|
||||
@@ -107,7 +107,7 @@ describe('Cross-Scope Projection Reading', () => {
|
||||
codeRev: CODEREV,
|
||||
});
|
||||
|
||||
nekoScope.appendEvent({
|
||||
await nekoScope.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'session',
|
||||
codeRev: CODEREV,
|
||||
@@ -147,7 +147,7 @@ describe('Cross-Scope Projection Reading', () => {
|
||||
await foldProjection(nekoDb, 'neko', 'session_count', CODEREV);
|
||||
|
||||
// Build snapshot from projections
|
||||
const snapshot = buildSnapshotFromProjections(scopedStore, [
|
||||
const snapshot = await buildSnapshotFromProjections(scopedStore, [
|
||||
'_vitals/cpu_usage',
|
||||
'neko/session_count',
|
||||
]);
|
||||
@@ -163,7 +163,7 @@ describe('Cross-Scope Projection Reading', () => {
|
||||
describe('Snapshot Key Format', () => {
|
||||
test('should use scope/name format consistently as keys', async () => {
|
||||
const testScope = scopedStore.scope('test_scope');
|
||||
testScope.appendEvent({
|
||||
await testScope.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'test',
|
||||
codeRev: CODEREV,
|
||||
@@ -184,7 +184,7 @@ describe('Snapshot Key Format', () => {
|
||||
|
||||
await foldProjection(testDb, 'test_scope', 'test_metric', CODEREV);
|
||||
|
||||
const snapshot = buildSnapshotFromProjections(scopedStore, [
|
||||
const snapshot = await buildSnapshotFromProjections(scopedStore, [
|
||||
'test_scope/test_metric',
|
||||
]);
|
||||
|
||||
@@ -275,7 +275,7 @@ describe('Onion Middleware Order', () => {
|
||||
// ── Test 6: Graceful Unregistered Projection Handling ─────────
|
||||
|
||||
describe('Graceful Unregistered Projection Handling', () => {
|
||||
test('should handle missing projections with null value and warning', () => {
|
||||
test('should handle missing projections with null value and warning', async () => {
|
||||
// Mock console.warn to capture warnings
|
||||
const originalWarn = console.warn;
|
||||
const warnings: string[] = [];
|
||||
@@ -283,7 +283,7 @@ describe('Graceful Unregistered Projection Handling', () => {
|
||||
warnings.push(args.join(' '));
|
||||
};
|
||||
|
||||
const snapshot = buildSnapshotFromProjections(scopedStore, [
|
||||
const snapshot = await buildSnapshotFromProjections(scopedStore, [
|
||||
'nonexistent/projection',
|
||||
'another/missing',
|
||||
]);
|
||||
@@ -307,7 +307,7 @@ describe('Graceful Unregistered Projection Handling', () => {
|
||||
console.warn = originalWarn;
|
||||
});
|
||||
|
||||
test('should handle scope database errors gracefully', () => {
|
||||
test('should handle scope database errors gracefully', async () => {
|
||||
const originalWarn = console.warn;
|
||||
const warnings: string[] = [];
|
||||
console.warn = (...args: any[]) => {
|
||||
@@ -315,7 +315,7 @@ describe('Graceful Unregistered Projection Handling', () => {
|
||||
};
|
||||
|
||||
// Force an error by trying to access invalid scope name
|
||||
const snapshot = buildSnapshotFromProjections(scopedStore, [
|
||||
const snapshot = await buildSnapshotFromProjections(scopedStore, [
|
||||
'invalid-scope-name!/projection',
|
||||
]);
|
||||
|
||||
@@ -339,7 +339,7 @@ describe('E2E Tick Loop', () => {
|
||||
async () => {
|
||||
// Write initial event (triggers scope db creation with def tables)
|
||||
const testScope = scopedStore.scope('test');
|
||||
testScope.appendEvent({
|
||||
await testScope.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'increment',
|
||||
codeRev: CODEREV,
|
||||
|
||||
@@ -23,8 +23,8 @@ describe('createScopedStore', () => {
|
||||
scopedStore = createScopedStore({ basePath, objectsDir });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scopedStore.close();
|
||||
afterEach(async () => {
|
||||
await scopedStore.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -37,9 +37,9 @@ describe('createScopedStore', () => {
|
||||
|
||||
// ── 2. scope() returns a usable PulseStore ──────────────────
|
||||
|
||||
it('2. scope("_system") returns a PulseStore that can append/query events', () => {
|
||||
it('2. scope("_system") returns a PulseStore that can append/query events', async () => {
|
||||
const store = scopedStore.scope('_system');
|
||||
const rec = store.appendEvent({
|
||||
const rec = await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'tick',
|
||||
key: 'sys',
|
||||
@@ -48,7 +48,7 @@ describe('createScopedStore', () => {
|
||||
expect(typeof rec.id).toBe('number');
|
||||
expect(rec.kind).toBe('tick');
|
||||
|
||||
const latest = store.getLatest('tick');
|
||||
const latest = await store.getLatest('tick');
|
||||
expect(latest).toBeTruthy();
|
||||
expect(latest?.occurredAt).toBe(1000);
|
||||
});
|
||||
@@ -73,18 +73,18 @@ describe('createScopedStore', () => {
|
||||
|
||||
// ── 5. scope isolation ──────────────────────────────────────
|
||||
|
||||
it('5. events in different scopes are isolated', () => {
|
||||
it('5. events in different scopes are isolated', async () => {
|
||||
const system = scopedStore.scope('_system');
|
||||
const neko = scopedStore.scope('neko');
|
||||
|
||||
system.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
system.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
neko.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
await system.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
await system.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
await neko.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
|
||||
expect(system.getRecent(10).length).toBe(2);
|
||||
expect(neko.getRecent(10).length).toBe(1);
|
||||
expect(system.hasEvents()).toBe(true);
|
||||
expect(neko.hasEvents()).toBe(true);
|
||||
await expect(system.getRecent(10).length).toBe(2);
|
||||
await expect(neko.getRecent(10).length).toBe(1);
|
||||
await expect(system.hasEvents()).toBe(true);
|
||||
await expect(neko.hasEvents()).toBe(true);
|
||||
});
|
||||
|
||||
// ── 6. listScopes() ─────────────────────────────────────────
|
||||
@@ -111,28 +111,28 @@ describe('createScopedStore', () => {
|
||||
|
||||
// ── 8. CAS shared across scopes ────────────────────────────
|
||||
|
||||
it('8. CAS objects/ directory is shared across scopes', () => {
|
||||
it('8. CAS objects/ directory is shared across scopes', async () => {
|
||||
const system = scopedStore.scope('_system');
|
||||
const neko = scopedStore.scope('neko');
|
||||
|
||||
const data = { shared: true, value: 42 };
|
||||
const hash = system.putObject(data);
|
||||
const hash = await system.putObject(data);
|
||||
|
||||
const fromNeko = neko.getObject(hash);
|
||||
const fromNeko = await neko.getObject(hash);
|
||||
expect(fromNeko).toEqual(data);
|
||||
});
|
||||
|
||||
// ── 9. ScopedStore-level CAS ────────────────────────────────
|
||||
|
||||
it('9. ScopedStore.putObject/getObject work independently of scopes', () => {
|
||||
it('9. ScopedStore.putObject/getObject work independently of scopes', async () => {
|
||||
const data = { top: 'level' };
|
||||
const hash = scopedStore.putObject(data);
|
||||
const hash = await scopedStore.putObject(data);
|
||||
|
||||
expect(scopedStore.getObject(hash)).toEqual(data);
|
||||
await expect(scopedStore.getObject(hash)).toEqual(data);
|
||||
|
||||
// Also accessible from any scope
|
||||
const store = scopedStore.scope('_system');
|
||||
expect(store.getObject(hash)).toEqual(data);
|
||||
await expect(store.getObject(hash)).toEqual(data);
|
||||
});
|
||||
|
||||
// ── 10. scope name validation: valid names ──────────────────
|
||||
@@ -175,63 +175,63 @@ describe('createScopedStore', () => {
|
||||
|
||||
// ── 14. close() closes all open scope dbs ───────────────────
|
||||
|
||||
it('14. close() closes all open scope databases', () => {
|
||||
it('14. close() closes all open scope databases', async () => {
|
||||
const system = scopedStore.scope('_system');
|
||||
const neko = scopedStore.scope('neko');
|
||||
|
||||
system.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
neko.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
await system.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
await neko.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
|
||||
scopedStore.close();
|
||||
await scopedStore.close();
|
||||
|
||||
// After close, accessing the db should throw
|
||||
expect(() =>
|
||||
system.appendEvent({ occurredAt: 3000, kind: 'tick' }),
|
||||
await system.appendEvent({ occurredAt: 3000, kind: 'tick' }),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
neko.appendEvent({ occurredAt: 3000, kind: 'tick' }),
|
||||
await neko.appendEvent({ occurredAt: 3000, kind: 'tick' }),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
// ── 15. full PulseStore API on scope ────────────────────────
|
||||
|
||||
it('15. scope PulseStore supports appendEvents (batch)', () => {
|
||||
it('15. scope PulseStore supports appendEvents (batch)', async () => {
|
||||
const store = scopedStore.scope('_system');
|
||||
const results = store.appendEvents([
|
||||
const results = await store.appendEvents([
|
||||
{ occurredAt: 1000, kind: 'tick' },
|
||||
{ occurredAt: 2000, kind: 'collect', key: 'cpu' },
|
||||
{ occurredAt: 3000, kind: 'effect' },
|
||||
]);
|
||||
|
||||
expect(results.length).toBe(3);
|
||||
expect(store.getRecent(10).length).toBe(3);
|
||||
await expect(store.getRecent(10).length).toBe(3);
|
||||
});
|
||||
|
||||
// ── 16. scope PulseStore supports queryByKind ───────────────
|
||||
|
||||
it('16. scope PulseStore supports queryByKind with filters', () => {
|
||||
it('16. scope PulseStore supports queryByKind with filters', async () => {
|
||||
const store = scopedStore.scope('_system');
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick', codeRev: 'v1' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'collect', key: 'cpu' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick', codeRev: 'v2' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick', codeRev: 'v1' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'collect', key: 'cpu' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick', codeRev: 'v2' });
|
||||
|
||||
const ticks = store.queryByKind('tick');
|
||||
const ticks = await store.queryByKind('tick');
|
||||
expect(ticks.length).toBe(2);
|
||||
|
||||
const v1Ticks = store.queryByKind('tick', { codeRev: 'v1' });
|
||||
const v1Ticks = await store.queryByKind('tick', { codeRev: 'v1' });
|
||||
expect(v1Ticks.length).toBe(1);
|
||||
expect(v1Ticks[0]?.occurredAt).toBe(1000);
|
||||
});
|
||||
|
||||
// ── 17. scope PulseStore supports getAfter ──────────────────
|
||||
|
||||
it('17. scope PulseStore supports getAfter', () => {
|
||||
it('17. scope PulseStore supports getAfter', async () => {
|
||||
const store = scopedStore.scope('_system');
|
||||
const e1 = store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'collect' });
|
||||
const e1 = await store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'collect' });
|
||||
|
||||
const after = store.getAfter(e1.id);
|
||||
const after = await store.getAfter(e1.id);
|
||||
expect(after.length).toBe(2);
|
||||
expect(after[0]?.occurredAt).toBe(2000);
|
||||
expect(after[1]?.occurredAt).toBe(3000);
|
||||
@@ -239,20 +239,20 @@ describe('createScopedStore', () => {
|
||||
|
||||
// ── 18. scope PulseStore supports getLatestWhere ────────────
|
||||
|
||||
it('18. scope PulseStore supports getLatestWhere', () => {
|
||||
it('18. scope PulseStore supports getLatestWhere', async () => {
|
||||
const store = scopedStore.scope('_system');
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'promote',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'promote',
|
||||
codeRev: 'v2',
|
||||
});
|
||||
|
||||
const latest = store.getLatestWhere({ kind: 'promote', codeRev: 'v1' });
|
||||
const latest = await store.getLatestWhere({ kind: 'promote', codeRev: 'v1' });
|
||||
expect(latest).toBeTruthy();
|
||||
expect(latest?.codeRev).toBe('v1');
|
||||
expect(latest?.occurredAt).toBe(1000);
|
||||
@@ -260,31 +260,31 @@ describe('createScopedStore', () => {
|
||||
|
||||
// ── 19. archiveEvents / downsampleEvents on scope PulseStore ──
|
||||
|
||||
it('19. scope PulseStore supports archiveEvents and downsampleEvents', () => {
|
||||
it('19. scope PulseStore supports archiveEvents and downsampleEvents', async () => {
|
||||
const store = scopedStore.scope('_vitals');
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'h1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'h2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 5000,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'h3',
|
||||
});
|
||||
|
||||
const archived = store.archiveEvents(3000);
|
||||
const archived = await store.archiveEvents(3000);
|
||||
expect(archived).toBe(2);
|
||||
|
||||
const remaining = store.queryByKind('vital', { key: 'cpu' });
|
||||
const remaining = await store.queryByKind('vital', { key: 'cpu' });
|
||||
expect(remaining.length).toBe(1);
|
||||
expect(remaining[0]?.hash).toBe('h3');
|
||||
});
|
||||
@@ -303,20 +303,20 @@ describe('createStore backward compatibility', () => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('20. createStore accepts (and ignores) legacy vitalsDbPath option', () => {
|
||||
it('20. createStore accepts (and ignores) legacy vitalsDbPath option', async () => {
|
||||
const store = createStore({
|
||||
eventsDbPath: join(dir, 'events.db'),
|
||||
vitalsDbPath: join(dir, 'vitals.db'),
|
||||
objectsDir: join(dir, 'objects'),
|
||||
});
|
||||
|
||||
const rec = store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
const rec = await store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
expect(typeof rec.id).toBe('number');
|
||||
|
||||
const data = { test: true };
|
||||
const hash = store.putObject(data);
|
||||
expect(store.getObject(hash)).toEqual(data);
|
||||
const hash = await store.putObject(data);
|
||||
await expect(store.getObject(hash)).toEqual(data);
|
||||
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
});
|
||||
|
||||
+134
-134
@@ -27,13 +27,13 @@ describe('createStore (events table + CAS)', () => {
|
||||
|
||||
// ── 1. Schema 创建 ───────────────────────────────────────────
|
||||
|
||||
it('1. Schema: createStore creates DB with events table', () => {
|
||||
it('1. Schema: createStore creates DB with events table', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
expect(existsSync(eventsDbPath)).toBeTruthy();
|
||||
expect(existsSync(objectsDir)).toBeTruthy();
|
||||
// Verify events table exists by querying it
|
||||
store.getRecent(1); // would throw if table doesn't exist
|
||||
store.close();
|
||||
await store.getRecent(1); // would throw if table doesn't exist
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 2. appendEvent ────────────────────────────────────────────
|
||||
@@ -56,12 +56,12 @@ describe('createStore (events table + CAS)', () => {
|
||||
|
||||
// ── 3. ID 唯一性 ───────────────────────────────────────────
|
||||
|
||||
it('3. ID uniqueness: 100 consecutive appends produce 100 unique ids', () => {
|
||||
it('3. ID uniqueness: 100 consecutive appends produce 100 unique ids', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
const ids = new Set<number>();
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const rec = store.appendEvent({
|
||||
const rec = await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'tick',
|
||||
});
|
||||
@@ -69,17 +69,17 @@ describe('createStore (events table + CAS)', () => {
|
||||
}
|
||||
|
||||
expect(ids.size).toBe(100);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 4. AUTOINCREMENT 有序性 ──────────────────────────────────
|
||||
|
||||
it('4. AUTOINCREMENT ordering: all ids are unique and sequential', () => {
|
||||
it('4. AUTOINCREMENT ordering: all ids are unique and sequential', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
const ids: number[] = [];
|
||||
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const rec = store.appendEvent({
|
||||
const rec = await store.appendEvent({
|
||||
occurredAt: i * 1000,
|
||||
kind: 'tick',
|
||||
});
|
||||
@@ -93,77 +93,77 @@ describe('createStore (events table + CAS)', () => {
|
||||
expect(ids[i]!).toBeGreaterThan(ids[i - 1]!);
|
||||
}
|
||||
// getAfter with the first id should return the rest
|
||||
const afterFirst = store.getAfter(ids[0]!);
|
||||
const afterFirst = await store.getAfter(ids[0]!);
|
||||
expect(afterFirst.length).toBe(19);
|
||||
expect(afterFirst[0]!.id).toBe(ids[1]!);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 5. getLatest(kind) ────────────────────────────────────────
|
||||
|
||||
it('5. getLatest(kind) returns the most recent event of that kind', () => {
|
||||
it('5. getLatest(kind) returns the most recent event of that kind', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'a' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'tick', key: 'b' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick', key: 'c' });
|
||||
store.appendEvent({ occurredAt: 2500, kind: 'collect' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'a' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'tick', key: 'b' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick', key: 'c' });
|
||||
await store.appendEvent({ occurredAt: 2500, kind: 'collect' });
|
||||
|
||||
const latest = store.getLatest('tick');
|
||||
const latest = await store.getLatest('tick');
|
||||
expect(latest).toBeTruthy();
|
||||
expect(latest?.occurredAt).toBe(3000);
|
||||
expect(latest?.kind).toBe('tick');
|
||||
expect(latest?.key).toBe('c');
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 6. getLatest(kind, key) ───────────────────────────────────
|
||||
|
||||
it('6. getLatest(kind, key) filters by key', () => {
|
||||
it('6. getLatest(kind, key) filters by key', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'cpu' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'tick', key: 'mem' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick', key: 'cpu' });
|
||||
store.appendEvent({ occurredAt: 4000, kind: 'tick', key: 'mem' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'cpu' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'tick', key: 'mem' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick', key: 'cpu' });
|
||||
await store.appendEvent({ occurredAt: 4000, kind: 'tick', key: 'mem' });
|
||||
|
||||
const latestCpu = store.getLatest('tick', 'cpu');
|
||||
const latestCpu = await store.getLatest('tick', 'cpu');
|
||||
expect(latestCpu).toBeTruthy();
|
||||
expect(latestCpu?.occurredAt).toBe(3000);
|
||||
expect(latestCpu?.key).toBe('cpu');
|
||||
|
||||
const latestMem = store.getLatest('tick', 'mem');
|
||||
const latestMem = await store.getLatest('tick', 'mem');
|
||||
expect(latestMem).toBeTruthy();
|
||||
expect(latestMem?.occurredAt).toBe(4000);
|
||||
expect(latestMem?.key).toBe('mem');
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 7. getLatestWhere + codeRev ───────────────────────────────
|
||||
|
||||
it('7. getLatestWhere filters by codeRev', () => {
|
||||
it('7. getLatestWhere filters by codeRev', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'tick',
|
||||
key: 'cpu',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'tick',
|
||||
key: 'cpu',
|
||||
codeRev: 'v2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 3000,
|
||||
kind: 'tick',
|
||||
key: 'cpu',
|
||||
codeRev: 'v1',
|
||||
});
|
||||
|
||||
const latestV2 = store.getLatestWhere({
|
||||
const latestV2 = await store.getLatestWhere({
|
||||
kind: 'tick',
|
||||
key: 'cpu',
|
||||
codeRev: 'v2',
|
||||
@@ -172,7 +172,7 @@ describe('createStore (events table + CAS)', () => {
|
||||
expect(latestV2?.occurredAt).toBe(2000);
|
||||
expect(latestV2?.codeRev).toBe('v2');
|
||||
|
||||
const latestV1 = store.getLatestWhere({
|
||||
const latestV1 = await store.getLatestWhere({
|
||||
kind: 'tick',
|
||||
key: 'cpu',
|
||||
codeRev: 'v1',
|
||||
@@ -180,113 +180,113 @@ describe('createStore (events table + CAS)', () => {
|
||||
expect(latestV1).toBeTruthy();
|
||||
expect(latestV1?.occurredAt).toBe(3000);
|
||||
expect(latestV1?.codeRev).toBe('v1');
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 8. getRecent ──────────────────────────────────────────────
|
||||
|
||||
it('8. getRecent returns newest N events in descending order', () => {
|
||||
it('8. getRecent returns newest N events in descending order', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
store.appendEvent({ occurredAt: i * 1000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: i * 1000, kind: 'tick' });
|
||||
}
|
||||
|
||||
const recent = store.getRecent(3);
|
||||
const recent = await store.getRecent(3);
|
||||
expect(recent.length).toBe(3);
|
||||
expect(recent[0]?.occurredAt).toBe(10000);
|
||||
expect(recent[1]?.occurredAt).toBe(9000);
|
||||
expect(recent[2]?.occurredAt).toBe(8000);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 9. queryByKind ────────────────────────────────────────────
|
||||
|
||||
it('9. queryByKind returns events of specified kind', () => {
|
||||
it('9. queryByKind returns events of specified kind', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'collect' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 4000, kind: 'error' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'collect' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 4000, kind: 'error' });
|
||||
|
||||
const ticks = store.queryByKind('tick');
|
||||
const ticks = await store.queryByKind('tick');
|
||||
expect(ticks.length).toBe(2);
|
||||
expect(ticks.every((e) => e.kind === 'tick')).toBeTruthy();
|
||||
// Newest first
|
||||
expect(ticks[0]?.occurredAt).toBe(3000);
|
||||
expect(ticks[1]?.occurredAt).toBe(1000);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 10. queryByKind + since ───────────────────────────────────
|
||||
|
||||
it('10. queryByKind with since filters by occurred_at', () => {
|
||||
it('10. queryByKind with since filters by occurred_at', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
store.appendEvent({ occurredAt: 4000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: 4000, kind: 'tick' });
|
||||
|
||||
const result = store.queryByKind('tick', { since: 2500 });
|
||||
const result = await store.queryByKind('tick', { since: 2500 });
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0]?.occurredAt).toBe(4000);
|
||||
expect(result[1]?.occurredAt).toBe(3000);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 11. queryByKind + key ─────────────────────────────────────
|
||||
|
||||
it('11. queryByKind with key filter', () => {
|
||||
it('11. queryByKind with key filter', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'cpu' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'tick', key: 'mem' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick', key: 'cpu' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'cpu' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'tick', key: 'mem' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick', key: 'cpu' });
|
||||
|
||||
const result = store.queryByKind('tick', { key: 'cpu' });
|
||||
const result = await store.queryByKind('tick', { key: 'cpu' });
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.every((e) => e.key === 'cpu')).toBeTruthy();
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 12. queryByKind + codeRev ─────────────────────────────────
|
||||
|
||||
it('12. queryByKind with codeRev filter', () => {
|
||||
it('12. queryByKind with codeRev filter', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick', codeRev: 'abc123' });
|
||||
store.appendEvent({ occurredAt: 2000, kind: 'tick', codeRev: 'def456' });
|
||||
store.appendEvent({ occurredAt: 3000, kind: 'tick', codeRev: 'abc123' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick', codeRev: 'abc123' });
|
||||
await store.appendEvent({ occurredAt: 2000, kind: 'tick', codeRev: 'def456' });
|
||||
await store.appendEvent({ occurredAt: 3000, kind: 'tick', codeRev: 'abc123' });
|
||||
|
||||
const result = store.queryByKind('tick', { codeRev: 'abc123' });
|
||||
const result = await store.queryByKind('tick', { codeRev: 'abc123' });
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.every((e) => e.codeRev === 'abc123')).toBeTruthy();
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 13. getAfter ──────────────────────────────────────────────
|
||||
|
||||
it('13. getAfter returns events after a specific id', () => {
|
||||
it('13. getAfter returns events after a specific id', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
const e1 = store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
const e2 = store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
const _e3 = store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
const _e4 = store.appendEvent({ occurredAt: 4000, kind: 'collect' });
|
||||
const e1 = await store.appendEvent({ occurredAt: 1000, kind: 'tick' });
|
||||
const e2 = await store.appendEvent({ occurredAt: 2000, kind: 'tick' });
|
||||
const _e3 = await store.appendEvent({ occurredAt: 3000, kind: 'tick' });
|
||||
const _e4 = await store.appendEvent({ occurredAt: 4000, kind: 'collect' });
|
||||
|
||||
const after = store.getAfter(e2.id);
|
||||
const after = await store.getAfter(e2.id);
|
||||
expect(after.length).toBe(2);
|
||||
// ASC order
|
||||
expect(after[0]?.occurredAt).toBe(3000);
|
||||
expect(after[1]?.occurredAt).toBe(4000);
|
||||
|
||||
// With kind filter
|
||||
const afterTick = store.getAfter(e1.id, { kind: 'tick' });
|
||||
const afterTick = await store.getAfter(e1.id, { kind: 'tick' });
|
||||
expect(afterTick.length).toBe(2);
|
||||
expect(afterTick.every((e) => e.kind === 'tick')).toBeTruthy();
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 14. hasEvents ─────────────────────────────────────────────
|
||||
@@ -326,40 +326,40 @@ describe('createStore (events table + CAS)', () => {
|
||||
|
||||
// ── 16. CAS 去重 ──────────────────────────────────────────────
|
||||
|
||||
it('16. CAS dedup: same data produces same hash', () => {
|
||||
it('16. CAS dedup: same data produces same hash', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
const data = { hello: 'world' };
|
||||
const hash1 = store.putObject(data);
|
||||
const hash2 = store.putObject({ hello: 'world' });
|
||||
const hash1 = await store.putObject(data);
|
||||
const hash2 = await store.putObject({ hello: 'world' });
|
||||
expect(hash1).toBe(hash2);
|
||||
|
||||
// Different data produces different hash
|
||||
const hash3 = store.putObject({ hello: 'other' });
|
||||
const hash3 = await store.putObject({ hello: 'other' });
|
||||
expect(hash1).not.toBe(hash3);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 17. 多种 kind 并存 ────────────────────────────────────────
|
||||
|
||||
it('17. Multiple kinds coexist: tick/collect/effect/error', () => {
|
||||
it('17. Multiple kinds coexist: tick/collect/effect/error', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'sys' });
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'collect', key: 'cpu' });
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'effect', key: 'notify' });
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'error', key: 'timeout' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick', key: 'sys' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'collect', key: 'cpu' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'effect', key: 'notify' });
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'error', key: 'timeout' });
|
||||
|
||||
expect(store.queryByKind('tick').length).toBe(1);
|
||||
expect(store.queryByKind('collect').length).toBe(1);
|
||||
expect(store.queryByKind('effect').length).toBe(1);
|
||||
expect(store.queryByKind('error').length).toBe(1);
|
||||
expect(store.getRecent().length).toBe(4);
|
||||
store.close();
|
||||
expect((await store.queryByKind('tick')).length).toBe(1);
|
||||
expect((await store.queryByKind('collect')).length).toBe(1);
|
||||
expect((await store.queryByKind('effect')).length).toBe(1);
|
||||
expect((await store.queryByKind('error')).length).toBe(1);
|
||||
expect((await store.getRecent()).length).toBe(4);
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── 18. appendEvents 批量 ─────────────────────────────────────
|
||||
|
||||
it('18. appendEvents batch inserts in a transaction', () => {
|
||||
it('18. appendEvents batch inserts in a transaction', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
|
||||
const events = [
|
||||
@@ -368,7 +368,7 @@ describe('createStore (events table + CAS)', () => {
|
||||
{ occurredAt: 3000, kind: 'effect' as const, key: 'c' },
|
||||
];
|
||||
|
||||
const results = store.appendEvents(events);
|
||||
const results = await store.appendEvents(events);
|
||||
expect(results.length).toBe(3);
|
||||
|
||||
// Each result should have a unique integer id
|
||||
@@ -376,21 +376,21 @@ describe('createStore (events table + CAS)', () => {
|
||||
expect(ids.size).toBe(3);
|
||||
|
||||
// All should be retrievable
|
||||
expect(store.getRecent(10).length).toBe(3);
|
||||
expect((await store.getRecent(10)).length).toBe(3);
|
||||
|
||||
// Empty batch doesn't throw
|
||||
const empty = store.appendEvents([]);
|
||||
const empty = await store.appendEvents([]);
|
||||
expect(empty.length).toBe(0);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
// ── Extra: appendEvent with all optional fields ───────────────
|
||||
|
||||
it('appendEvent stores all optional fields (hash, codeRev, meta)', () => {
|
||||
it('appendEvent stores all optional fields (hash, codeRev, meta)', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
const hash = store.putObject({ data: 'test' });
|
||||
const hash = await store.putObject({ data: 'test' });
|
||||
|
||||
const rec = store.appendEvent({
|
||||
const rec = await store.appendEvent({
|
||||
occurredAt: 5000,
|
||||
kind: 'tick',
|
||||
key: 'system',
|
||||
@@ -404,35 +404,35 @@ describe('createStore (events table + CAS)', () => {
|
||||
expect(rec.meta).toBe(JSON.stringify({ duration: 42 }));
|
||||
|
||||
// Verify via getLatest
|
||||
const latest = store.getLatest('tick', 'system');
|
||||
const latest = await store.getLatest('tick', 'system');
|
||||
expect(latest).toBeTruthy();
|
||||
expect(latest?.hash).toBe(hash);
|
||||
expect(latest?.codeRev).toBe('abc123');
|
||||
expect(latest?.meta).toBe(JSON.stringify({ duration: 42 }));
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('getLatest returns null for non-existent kind', () => {
|
||||
it('getLatest returns null for non-existent kind', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
expect(store.getLatest('nonexistent')).toBe(null);
|
||||
store.close();
|
||||
expect(await store.getLatest('nonexistent')).toBe(null);
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('getLatestWhere returns null when no match', () => {
|
||||
it('getLatestWhere returns null when no match', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
store.appendEvent({ occurredAt: 1000, kind: 'tick', codeRev: 'v1' });
|
||||
expect(store.getLatestWhere({ kind: 'tick', codeRev: 'v99' })).toBeNull();
|
||||
store.close();
|
||||
await store.appendEvent({ occurredAt: 1000, kind: 'tick', codeRev: 'v1' });
|
||||
expect(await store.getLatestWhere({ kind: 'tick', codeRev: 'v99' })).toBeNull();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('queryByKind with limit', () => {
|
||||
it('queryByKind with limit', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
store.appendEvent({ occurredAt: i * 1000, kind: 'tick' });
|
||||
await store.appendEvent({ occurredAt: i * 1000, kind: 'tick' });
|
||||
}
|
||||
const result = store.queryByKind('tick', { limit: 3 });
|
||||
const result = await store.queryByKind('tick', { limit: 3 });
|
||||
expect(result.length).toBe(3);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -453,126 +453,126 @@ describe('archiveEvents and downsampleEvents', () => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('archiveEvents deletes events older than threshold', () => {
|
||||
it('archiveEvents deletes events older than threshold', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1000,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'h1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 2000,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'h2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 5000,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'h3',
|
||||
});
|
||||
|
||||
const archived = store.archiveEvents(3000);
|
||||
const archived = await store.archiveEvents(3000);
|
||||
expect(archived).toBe(2);
|
||||
|
||||
const remaining = store.queryByKind('vital', { key: 'cpu' });
|
||||
const remaining = await store.queryByKind('vital', { key: 'cpu' });
|
||||
expect(remaining.length).toBe(1);
|
||||
expect(remaining[0]?.hash).toBe('h3');
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('downsampleEvents keeps one per interval window', () => {
|
||||
it('downsampleEvents keeps one per interval window', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
// Window 0: [0-999], Window 1: [1000-1999]
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 100,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'w0_1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 500,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'w0_2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 900,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'w0_3',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1100,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'w1_1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1500,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'w1_2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 1900,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'w1_3',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 5000,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'recent',
|
||||
});
|
||||
|
||||
const deleted = store.downsampleEvents('vital', 'cpu', 1000, 3000);
|
||||
const deleted = await store.downsampleEvents('vital', 'cpu', 1000, 3000);
|
||||
expect(deleted).toBe(4);
|
||||
|
||||
const remaining = store.queryByKind('vital', { key: 'cpu', limit: 10 });
|
||||
const remaining = await store.queryByKind('vital', { key: 'cpu', limit: 10 });
|
||||
expect(remaining.length).toBe(3);
|
||||
const hashes = remaining.map((v) => v.hash);
|
||||
expect(hashes.includes('w0_3')).toBeTruthy();
|
||||
expect(hashes.includes('w1_3')).toBeTruthy();
|
||||
expect(hashes.includes('recent')).toBeTruthy();
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('downsampleEvents only affects specified kind+key', () => {
|
||||
it('downsampleEvents only affects specified kind+key', async () => {
|
||||
const store = createStore({ eventsDbPath, objectsDir });
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 100,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'v1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 500,
|
||||
kind: 'vital',
|
||||
key: 'cpu',
|
||||
hash: 'v2',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 100,
|
||||
kind: 'tick',
|
||||
key: 'cpu',
|
||||
hash: 't1',
|
||||
});
|
||||
store.appendEvent({
|
||||
await store.appendEvent({
|
||||
occurredAt: 500,
|
||||
kind: 'vital',
|
||||
key: 'mem',
|
||||
hash: 'm1',
|
||||
});
|
||||
|
||||
const deleted = store.downsampleEvents('vital', 'cpu', 1000, 3000);
|
||||
const deleted = await store.downsampleEvents('vital', 'cpu', 1000, 3000);
|
||||
expect(deleted).toBe(1);
|
||||
|
||||
expect(store.queryByKind('tick').length).toBe(1);
|
||||
expect(store.queryByKind('vital', { key: 'mem' }).length).toBe(1);
|
||||
store.close();
|
||||
expect((await store.queryByKind('tick')).length).toBe(1);
|
||||
expect((await store.queryByKind('vital', { key: 'mem' })).length).toBe(1);
|
||||
await store.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,14 +27,14 @@ describe('coding-tdd WorkflowType', () => {
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
store?.close();
|
||||
await store?.close();
|
||||
} catch {}
|
||||
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function trigger(topicId: string, title: string, description: string) {
|
||||
const hash = store.putObject(description);
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject(description);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'coding-tdd.__start__',
|
||||
key: topicId,
|
||||
|
||||
@@ -24,9 +24,9 @@ describe('CodingTask WorkflowType', () => {
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
try {
|
||||
store?.close();
|
||||
await store?.close();
|
||||
} catch {}
|
||||
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -37,8 +37,8 @@ describe('CodingTask WorkflowType', () => {
|
||||
description: string,
|
||||
repoDir: string,
|
||||
) {
|
||||
const hash = store.putObject(description);
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject(description);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'coding.__start__',
|
||||
key: topicId,
|
||||
@@ -67,7 +67,7 @@ describe('CodingTask WorkflowType', () => {
|
||||
expect(r1.executed).toMatchObject([
|
||||
{ topicId: 'task-1', role: 'architect' },
|
||||
]);
|
||||
expect(store.queryByKind('coding.architect').length).toBe(1);
|
||||
await expect(store.queryByKind('coding.architect').length).toBe(1);
|
||||
|
||||
const r2 = await rule.tick();
|
||||
expect(r2.executed).toMatchObject([{ topicId: 'task-1', role: 'coder' }]);
|
||||
@@ -117,7 +117,7 @@ describe('CodingTask WorkflowType', () => {
|
||||
await rule.tick(); // coder
|
||||
await rule.tick(); // reviewer (rejects, retryCount=0)
|
||||
|
||||
const reviewerEvents = store.queryByKind('coding.reviewer');
|
||||
const reviewerEvents = await store.queryByKind('coding.reviewer');
|
||||
expect(reviewerEvents.length).toBe(1);
|
||||
const meta = JSON.parse(reviewerEvents[0].meta!);
|
||||
expect(meta.verdict).toBe('rejected');
|
||||
@@ -198,12 +198,12 @@ describe('CodingTask WorkflowType', () => {
|
||||
triggerCoding('task-3', 'CAS test', 'Verify CAS storage', '/tmp/repo');
|
||||
await rule.tick(); // architect
|
||||
|
||||
const events = store.queryByKind('coding.architect');
|
||||
const events = await store.queryByKind('coding.architect');
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0].hash).toBeTruthy();
|
||||
|
||||
// CAS content should be a string (not object)
|
||||
const content = store.getObject(events[0].hash!);
|
||||
const content = await store.getObject(events[0].hash!);
|
||||
expect(typeof content).toBe('string');
|
||||
});
|
||||
|
||||
@@ -227,11 +227,11 @@ describe('CodingTask WorkflowType', () => {
|
||||
);
|
||||
await rule.tick();
|
||||
|
||||
expect(logStore.queryByKind('coding.role-started').length).toBe(1);
|
||||
expect(logStore.queryByKind('coding.role-completed').length).toBe(1);
|
||||
expect(store.queryByKind('coding.role-started').length).toBe(0);
|
||||
await expect(logStore.queryByKind('coding.role-started').length).toBe(1);
|
||||
await expect(logStore.queryByKind('coding.role-completed').length).toBe(1);
|
||||
await expect(store.queryByKind('coding.role-started').length).toBe(0);
|
||||
} finally {
|
||||
logStore.close();
|
||||
await logStore.close();
|
||||
rmSync(logTmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('cursor-health', () => {
|
||||
stmt.run(baseTime + offset);
|
||||
}
|
||||
|
||||
db.close();
|
||||
await db.close();
|
||||
return tempDbPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@ describe('Meta Workflow', () => {
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
|
||||
// Seed START
|
||||
const hash = store.putObject('build a demo workflow');
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject('build a demo workflow');
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'meta.__start__',
|
||||
key: 'test-1',
|
||||
@@ -152,10 +152,10 @@ describe('Meta Workflow', () => {
|
||||
'promoter',
|
||||
]);
|
||||
|
||||
const events = store.getAfter(0);
|
||||
const events = await store.getAfter(0);
|
||||
expect(events.length).toBe(6); // __start__ + 5 roles
|
||||
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
|
||||
test('mock: reviewer rejection → retry coder', async () => {
|
||||
@@ -193,8 +193,8 @@ describe('Meta Workflow', () => {
|
||||
});
|
||||
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
const hash = store.putObject('test retry');
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject('test retry');
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'meta.__start__',
|
||||
key: 'retry-1',
|
||||
@@ -218,6 +218,6 @@ describe('Meta Workflow', () => {
|
||||
'tester',
|
||||
'promoter',
|
||||
]);
|
||||
store.close();
|
||||
await store.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ function tmpStore(): { store: Store; cleanup: () => void } {
|
||||
eventsDbPath: `${dir}/events.db`,
|
||||
objectsDir: `${dir}/objects`,
|
||||
});
|
||||
return { store, cleanup: () => store.close() };
|
||||
await return { store, cleanup: () => store.close() };
|
||||
}
|
||||
|
||||
describe('Report Workflow', () => {
|
||||
@@ -47,8 +47,8 @@ describe('Report Workflow', () => {
|
||||
{ id: 5, role: 'closer', offsetMs: 10000, durationMs: 1000 },
|
||||
],
|
||||
});
|
||||
const hash = store.putObject(timeline);
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject(timeline);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'report.__start__',
|
||||
key: 'test-report',
|
||||
@@ -66,10 +66,10 @@ describe('Report Workflow', () => {
|
||||
expect(r2.executed[0].role).toBe('renderer');
|
||||
|
||||
// Verify renderer output is HTML
|
||||
const events = store.getAfter(0);
|
||||
const events = await store.getAfter(0);
|
||||
const rendererEvt = events.find((e) => e.kind === 'report.renderer');
|
||||
expect(rendererEvt).toBeDefined();
|
||||
const html = store.getObject(rendererEvt!.hash!);
|
||||
const html = await store.getObject(rendererEvt!.hash!);
|
||||
expect(typeof html).toBe('string');
|
||||
expect((html as string).includes('<html>')).toBe(true);
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ describe('coder-cursor role', () => {
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
try {
|
||||
store?.close();
|
||||
await store?.close();
|
||||
} catch {}
|
||||
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -41,14 +41,14 @@ describe('coder-cursor role', () => {
|
||||
) => {
|
||||
const architectMsg = chain.find((m) => m.role === 'architect');
|
||||
const architectContent = (architectMsg?.content as any) ?? {};
|
||||
const hash = s.putObject({
|
||||
const hash = await s.putObject({
|
||||
content: 'Mock implementation done',
|
||||
artifacts: {
|
||||
filesChanged: architectContent.artifacts?.targetFiles ?? [],
|
||||
testsPassed: true,
|
||||
},
|
||||
});
|
||||
s.appendEvent({
|
||||
await s.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'coding.coder',
|
||||
key: topicId,
|
||||
@@ -77,12 +77,12 @@ describe('coder-cursor role', () => {
|
||||
|
||||
await mockCoderFn(chain, 'coder-test-1', store);
|
||||
|
||||
const events = store.getAfter(0);
|
||||
const events = await store.getAfter(0);
|
||||
const coded = events.find((e) => e.kind === 'coding.coder');
|
||||
expect(coded).toBeDefined();
|
||||
expect(coded!.hash).toBeTruthy();
|
||||
|
||||
const content = store.getObject(coded!.hash!) as any;
|
||||
const content = await store.getObject(coded!.hash!) as any;
|
||||
expect(content.content).toBe('Mock implementation done');
|
||||
expect(content.artifacts.filesChanged).toEqual(['src/utils.ts']);
|
||||
expect(content.artifacts.testsPassed).toBe(true);
|
||||
|
||||
@@ -23,9 +23,9 @@ describe('reviewer-cursor role', () => {
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
try {
|
||||
store?.close();
|
||||
await store?.close();
|
||||
} catch {}
|
||||
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -38,8 +38,8 @@ describe('reviewer-cursor role', () => {
|
||||
topicId: string,
|
||||
s: PulseStore,
|
||||
) => {
|
||||
const hash = s.putObject({ content: 'LGTM, code looks good. APPROVED.' });
|
||||
s.appendEvent({
|
||||
const hash = await s.putObject({ content: 'LGTM, code looks good. APPROVED.' });
|
||||
await s.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'coding.reviewer',
|
||||
key: topicId,
|
||||
@@ -77,14 +77,14 @@ describe('reviewer-cursor role', () => {
|
||||
|
||||
await mockReviewerFn(chain, 'review-test-1', store);
|
||||
|
||||
const events = store.getAfter(0);
|
||||
const events = await store.getAfter(0);
|
||||
const reviewed = events.find((e) => e.kind === 'coding.reviewer');
|
||||
expect(reviewed).toBeDefined();
|
||||
expect(reviewed!.hash).toBeTruthy();
|
||||
const meta = JSON.parse(reviewed!.meta!);
|
||||
expect(meta.verdict).toBe('approved');
|
||||
|
||||
const content = store.getObject(reviewed!.hash!) as any;
|
||||
const content = await store.getObject(reviewed!.hash!) as any;
|
||||
expect(content.content).toContain('APPROVED');
|
||||
});
|
||||
|
||||
@@ -102,8 +102,8 @@ describe('reviewer-cursor role', () => {
|
||||
.includes('rejected')
|
||||
? 'rejected'
|
||||
: 'approved';
|
||||
const hash = s.putObject({ content: output });
|
||||
s.appendEvent({
|
||||
const hash = await s.putObject({ content: output });
|
||||
await s.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: 'coding.reviewer',
|
||||
key: topicId,
|
||||
@@ -133,7 +133,7 @@ describe('reviewer-cursor role', () => {
|
||||
|
||||
await mockReviewerFn(chain, 'review-test-2', store);
|
||||
|
||||
const events = store.getAfter(0);
|
||||
const events = await store.getAfter(0);
|
||||
const reviewed = events.find((e) => e.kind === 'coding.reviewer');
|
||||
const meta = JSON.parse(reviewed!.meta!);
|
||||
expect(meta.verdict).toBe('rejected');
|
||||
|
||||
@@ -29,12 +29,12 @@ describe('createWorkflowRule', () => {
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
try {
|
||||
store?.close();
|
||||
await store?.close();
|
||||
} catch {}
|
||||
try {
|
||||
logStore?.close();
|
||||
await logStore?.close();
|
||||
} catch {}
|
||||
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -45,8 +45,8 @@ describe('createWorkflowRule', () => {
|
||||
content: string,
|
||||
meta?: Record<string, unknown>,
|
||||
) {
|
||||
const hash = store.putObject(content);
|
||||
store.appendEvent({
|
||||
const hash = await store.putObject(content);
|
||||
await store.appendEvent({
|
||||
occurredAt: Date.now(),
|
||||
kind: `${name}.__start__`,
|
||||
key: topicId,
|
||||
@@ -87,21 +87,21 @@ describe('createWorkflowRule', () => {
|
||||
expect(r1.executed).toMatchObject([{ topicId: 't1', role: 'echo' }]);
|
||||
|
||||
// Verify adapter wrote the event
|
||||
const echoEvents = store.queryByKind('echo.echo');
|
||||
const echoEvents = await store.queryByKind('echo.echo');
|
||||
expect(echoEvents.length).toBe(1);
|
||||
expect(echoEvents[0].hash).toBeTruthy();
|
||||
|
||||
// Verify CAS content
|
||||
const content = store.getObject(echoEvents[0].hash!);
|
||||
const content = await store.getObject(echoEvents[0].hash!);
|
||||
expect(content).toBe('Echo: hello');
|
||||
|
||||
const r2 = await rule.tick();
|
||||
expect(r2.executed).toEqual([]);
|
||||
|
||||
// Verify logging
|
||||
const started = logStore.queryByKind('echo.role-started');
|
||||
const started = await logStore.queryByKind('echo.role-started');
|
||||
expect(started.length).toBe(1);
|
||||
expect(store.queryByKind('echo.role-started').length).toBe(0);
|
||||
await expect(store.queryByKind('echo.role-started').length).toBe(0);
|
||||
});
|
||||
|
||||
it('role failure is logged, does not crash tick', async () => {
|
||||
@@ -128,7 +128,7 @@ describe('createWorkflowRule', () => {
|
||||
const r1 = await rule.tick();
|
||||
expect(r1.executed).toEqual([]);
|
||||
|
||||
const failed = logStore.queryByKind('fail.role-failed');
|
||||
const failed = await logStore.queryByKind('fail.role-failed');
|
||||
expect(failed.length).toBe(1);
|
||||
expect(JSON.parse(failed[0].meta!).error).toBe('kaboom');
|
||||
});
|
||||
@@ -154,7 +154,7 @@ describe('createWorkflowRule', () => {
|
||||
|
||||
const r1 = await rule.tick();
|
||||
expect(r1.executed).toMatchObject([{ topicId: 't1', role: 'echo' }]);
|
||||
expect(store.queryByKind('echo.role-started').length).toBe(0);
|
||||
await expect(store.queryByKind('echo.role-started').length).toBe(0);
|
||||
});
|
||||
|
||||
it('Moore diff prevents re-execution', async () => {
|
||||
@@ -280,7 +280,7 @@ describe('createWorkflowRule', () => {
|
||||
|
||||
await rule.tick();
|
||||
|
||||
const events = store.queryByKind('nm.closer');
|
||||
const events = await store.queryByKind('nm.closer');
|
||||
expect(events.length).toBe(1);
|
||||
// adapter writes undefined when meta is null
|
||||
expect(events[0].meta).toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user