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();
  });
});