refactor(plugins): async DB-touching methods; transactions through UnitOfWork

This commit is contained in:
jubnl
2026-09-22 00:00:53 +02:00
parent e413308227
commit e28dc6de28
68 changed files with 743 additions and 706 deletions
@@ -260,7 +260,7 @@ describe('PluginRuntimeService (M2 end-to-end)', () => {
testDb.prepare("INSERT INTO plugins (id, status, enabled, granted_permissions, config) VALUES ('booter','error',1,'[]','{}')").run();
const rt = await createPluginRuntime(new DatabaseService(dbConn));
rt.onApplicationBootstrap(); // fire-and-forget spawn
await rt.onApplicationBootstrap(); // fire-and-forget spawn
for (let i = 0; i < 40 && !rt.isActive('booter'); i++) await new Promise((r) => setTimeout(r, 50));
expect(rt.isActive('booter')).toBe(true);
await rt.deactivate('booter');
@@ -272,7 +272,7 @@ describe('PluginRuntimeService (M2 end-to-end)', () => {
testDb.prepare("INSERT INTO plugins (id, status, enabled, granted_permissions, config) VALUES ('sleeper','inactive',0,'[]','{}')").run();
const rt = await createPluginRuntime(new DatabaseService(dbConn));
rt.onApplicationBootstrap();
await rt.onApplicationBootstrap();
await new Promise((r) => setTimeout(r, 300));
expect(rt.isActive('sleeper')).toBe(false);
});
@@ -280,8 +280,8 @@ describe('PluginRuntimeService (M2 end-to-end)', () => {
it('outboundHostsOf extracts declared http:outbound hosts', async () => {
testDb.prepare("INSERT INTO plugins (id, status, granted_permissions, config) VALUES ('net','inactive','[\"db:own\",\"http:outbound:api.x.com\",\"http:outbound:*.y.com\"]','{}')").run();
const rt = await createPluginRuntime(new DatabaseService(dbConn));
expect(rt.outboundHostsOf('net')).toEqual(['api.x.com', '*.y.com']);
expect(rt.outboundHostsOf('missing')).toEqual([]);
expect(await rt.outboundHostsOf('net')).toEqual(['api.x.com', '*.y.com']);
expect(await rt.outboundHostsOf('missing')).toEqual([]);
});
it('uninstall removes the code, DB rows, settings and (with deleteData) data', async () => {
@@ -525,9 +525,9 @@ describe('PluginRuntimeService inter-plugin (exports + events)', () => {
await expect(runtime.callPlugin('consumer', 'lib', 'nope', {}, 7)).rejects.toThrow(/does not export/);
});
it('emitPluginEvent validates the declared event and fans out without throwing', () => {
expect(() => runtime.emitPluginEvent('lib', 'ping', { x: 1 })).not.toThrow();
expect(() => runtime.emitPluginEvent('lib', 'not-declared', {})).toThrow(/does not declare event/);
it('emitPluginEvent validates the declared event and fans out without throwing', async () => {
await expect(runtime.emitPluginEvent('lib', 'ping', { x: 1 })).resolves.toBeUndefined();
await expect(runtime.emitPluginEvent('lib', 'not-declared', {})).rejects.toThrow(/does not declare event/);
});
});
@@ -431,13 +431,13 @@ describe('PluginRegistryService', () => {
});
// ── Sideload (upload your own plugin) ────────────────────────────────────────
it('sideload: stage + commit installs an uploaded plugin INACTIVE as local:upload', () => {
it('sideload: stage + commit installs an uploaded plugin INACTIVE as local:upload', async () => {
const zip = makeArtifact({ id: 'my-upload', name: 'Uploaded', version: '2.0.0', type: 'widget', permissions: ['db:own'] });
const staged = svc.stageUpload(zip);
expect(staged.id).toBe('my-upload');
expect(staged.version).toBe('2.0.0');
svc.commitUpload(staged);
await svc.commitUpload(staged);
const row = testDb.prepare('SELECT status, source_repo, reviewed_at, version FROM plugins WHERE id = ?').get('my-upload') as
{ status: string; source_repo: string | null; reviewed_at: string | null; version: string } | undefined;
@@ -458,11 +458,11 @@ describe('PluginRegistryService', () => {
expect(() => svc.stageUpload(empty)).toThrow(/trek-plugin\.json/);
});
it('sideload: forces INACTIVE even when replacing a plugin that was active', () => {
it('sideload: forces INACTIVE even when replacing a plugin that was active', async () => {
const zip = () => makeArtifact({ id: 'my-upload', name: 'Uploaded', version: '2.0.0', type: 'widget', permissions: ['db:own'] });
svc.commitUpload(svc.stageUpload(zip())); // first install
await svc.commitUpload(svc.stageUpload(zip())); // first install
testDb.prepare("UPDATE plugins SET status = 'active', enabled = 1 WHERE id = 'my-upload'").run(); // admin activated it
svc.commitUpload(svc.stageUpload(zip())); // re-upload replaces the code
await svc.commitUpload(svc.stageUpload(zip())); // re-upload replaces the code
const row = testDb.prepare('SELECT status, enabled FROM plugins WHERE id = ?').get('my-upload') as { status: string; enabled: number };
expect(row.status).toBe('inactive'); // discoverPlugins keeps the old status; commitUpload floors it back to inactive
expect(row.enabled).toBe(0);
@@ -1076,7 +1076,7 @@ describe('an update block does not outlive the registry relationship', () => {
// The admin now uploads the plugin by hand.
const upload = makeArtifact({ id: 'flight-tracker', name: 'Flight', version: '9.9.9', type: 'widget', permissions: ['db:own'] });
svc.commitUpload(svc.stageUpload(upload));
await svc.commitUpload(svc.stageUpload(upload));
const row = testDb.prepare("SELECT source_repo, author_pubkey, update_block_code, update_block_version FROM plugins WHERE id='flight-tracker'").get() as Record<string, unknown>;
expect(row.source_repo).toBe('local:upload');
@@ -43,7 +43,7 @@ function makeSupervisor(events: Array<{ topic: string; data: unknown }>, tuning:
const deps: HostDeps = {
data: new PluginDataDb(id),
callPlugin: async () => undefined,
emitPluginEvent: (event, payload) => broadcasts.push({ id, event, payload }),
emitPluginEvent: async (event, payload) => { broadcasts.push({ id, event, payload }); },
};
// Only db.* is exercised from a child here; the rest of the surface has its own
// unit suites and would drag every domain service into this integration test.