File size: 3,363 Bytes
f0634fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | import { describe, expect, it } from 'vitest';
import { SyncDescriptor } from '#/_base/di/descriptors';
import { FiberState } from '#/_base/di/fiber';
import { createDecorator } from '#/_base/di/instantiation';
import { InstantiationService } from '#/_base/di/instantiationService';
import { Service } from '#/_base/di/service';
import { ServiceCollection } from '#/_base/di/serviceCollection';
import { IFeatureManager } from '#/app/feature/featureManager';
import { FeatureManagerService } from '#/app/feature/featureManagerService';
interface IGizmo {
tag: string;
}
const IGizmo = createDecorator<IGizmo>('feature-gizmo');
class Gizmo extends Service {
readonly tag = 'gizmo';
}
function host(): { ix: InstantiationService; manager: IFeatureManager } {
const ix = new InstantiationService(
new ServiceCollection([IFeatureManager, new SyncDescriptor(FeatureManagerService)]),
true,
);
return { ix, manager: ix.invokeFunction((a) => a.get(IFeatureManager)) };
}
describe('FeatureManager — dynamic unit assembly at App scope (§5.10)', () => {
it('assembles a token-bound unit, introspects it, and retracts it', async () => {
const { ix, manager } = host();
const events: number[] = [];
manager.onDidChangeUnits(() => events.push(events.length));
const handle = manager.provideUnit(IGizmo, Gizmo);
expect(handle.state).toBe(FiberState.Active);
expect(ix.invokeFunction((a) => a.get(IGizmo)).tag).toBe('gizmo');
const infos = manager.units();
expect(infos).toHaveLength(1);
expect(infos[0]).toMatchObject({ name: 'Gizmo', state: FiberState.Active, meta: {} });
expect(typeof infos[0]!.uid).toBe('number');
expect(events.length).toBe(1);
await manager.unprovideUnit('Gizmo');
expect(manager.units()).toHaveLength(0);
expect(() => ix.invokeFunction((a) => a.get(IGizmo))).toThrow(/unknown service/);
expect(events.length).toBe(2);
ix.dispose();
});
it('reloads a managed unit with new config via updateUnit', async () => {
const { ix, manager } = host();
const configs: unknown[] = [];
class Configured extends Service {
constructor() {
super();
configs.push(this.config);
}
}
manager.provideUnit(IGizmo, Configured, { config: 1 });
ix.invokeFunction((a) => a.get(IGizmo));
expect(configs).toEqual([1]);
await manager.updateUnit('Configured', 2);
expect(configs).toEqual([1, 2]);
await expect(manager.updateUnit('unknown-unit')).rejects.toThrow(/not managed/);
ix.dispose();
});
it('retracts every managed unit when the manager dies', async () => {
const { ix, manager } = host();
manager.provideUnit(IGizmo, Gizmo);
ix.invokeFunction((a) => a.get(IGizmo));
ix.dispose();
await expect(Promise.resolve()).resolves.toBeUndefined();
});
it('carries a recipe-declared static meta into introspection', () => {
const { ix, manager } = host();
class Documented extends Service {
static readonly meta = { summary: 'a documented unit' };
}
manager.provideUnit(Documented);
expect(manager.units()[0]).toMatchObject({
name: 'Documented',
meta: { summary: 'a documented unit' },
});
manager.provideUnit(IGizmo, Gizmo);
expect(manager.units().find((unit) => unit.name === 'Gizmo')!.meta).toEqual({});
ix.dispose();
});
});
|