feat(smartmigration): add initial smartmigration package with MongoDB and S3 migration runner

This commit is contained in:
2026-04-07 17:35:05 +00:00
commit d96c6bcee8
33 changed files with 11443 additions and 0 deletions

81
test/test.mongoledger.ts Normal file
View File

@@ -0,0 +1,81 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { makeTestDb } from './helpers/services.js';
import type * as smartdata from '@push.rocks/smartdata';
import { MongoLedger } from '../ts/ledgers/classes.mongoledger.js';
// Smartdata's CollectionFactory caches collections by class name globally
// (see classes.collection.ts:22), so we MUST share a single db across all
// tests in a file. We isolate tests via unique ledger names — the EasyStore
// stores data keyed by `nameId`, so distinct ledger names live in the same
// underlying mongo collection without interference.
let db: smartdata.SmartdataDb;
let cleanup: () => Promise<void>;
tap.test('setup: connect shared db', async () => {
const r = await makeTestDb('mongoledger');
db = r.db;
cleanup = r.cleanup;
});
tap.test('MongoLedger: init creates an empty ledger', async () => {
const ledger = new MongoLedger(db, 'init-test');
await ledger.init();
const data = await ledger.read();
expect(data.currentVersion).toBeNull();
expect(data.steps).toEqual({});
expect(data.lock.holder).toBeNull();
expect(data.checkpoints).toEqual({});
});
tap.test('MongoLedger: write+read roundtrips', async () => {
const ledger = new MongoLedger(db, 'rw-test');
await ledger.init();
await ledger.write({
currentVersion: '1.5.0',
steps: { foo: { id: 'foo', fromVersion: '1.0.0', toVersion: '1.5.0', status: 'applied', startedAt: 'a', finishedAt: 'b', durationMs: 12 } },
lock: { holder: null, acquiredAt: null, expiresAt: null },
checkpoints: { foo: { progress: 42 } },
});
const round = await ledger.read();
expect(round.currentVersion).toEqual('1.5.0');
expect(round.steps['foo'].durationMs).toEqual(12);
expect((round.checkpoints['foo'] as any).progress).toEqual(42);
});
tap.test('MongoLedger: lock acquire/release', async () => {
const ledger = new MongoLedger(db, 'lock-test');
await ledger.init();
const acquired = await ledger.acquireLock('holder-A', 60_000);
expect(acquired).toBeTrue();
// A second acquire from a different holder should fail.
const acquired2 = await ledger.acquireLock('holder-B', 60_000);
expect(acquired2).toBeFalse();
// Release lets holder-B in.
await ledger.releaseLock('holder-A');
const acquired3 = await ledger.acquireLock('holder-B', 60_000);
expect(acquired3).toBeTrue();
});
tap.test('MongoLedger: expired lock can be stolen', async () => {
const ledger = new MongoLedger(db, 'expire-test');
await ledger.init();
// Acquire with a 1ms TTL so it's instantly expired.
const got = await ledger.acquireLock('stale-holder', 1);
expect(got).toBeTrue();
await new Promise((r) => setTimeout(r, 10));
// Now another holder should be able to take over (lock is expired).
const got2 = await ledger.acquireLock('fresh-holder', 60_000);
expect(got2).toBeTrue();
});
tap.test('cleanup: close shared db', async () => {
await cleanup();
});
export default tap.start();