@soleri/core 9.5.0 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/adapters/claude-code-adapter.d.ts +27 -0
  2. package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
  3. package/dist/adapters/claude-code-adapter.js +111 -0
  4. package/dist/adapters/claude-code-adapter.js.map +1 -0
  5. package/dist/adapters/index.d.ts +9 -0
  6. package/dist/adapters/index.d.ts.map +1 -0
  7. package/dist/adapters/index.js +10 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/registry.d.ts +21 -0
  10. package/dist/adapters/registry.d.ts.map +1 -0
  11. package/dist/adapters/registry.js +44 -0
  12. package/dist/adapters/registry.js.map +1 -0
  13. package/dist/adapters/types.d.ts +93 -0
  14. package/dist/adapters/types.d.ts.map +1 -0
  15. package/dist/adapters/types.js +10 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/brain/brain.d.ts +12 -1
  18. package/dist/brain/brain.d.ts.map +1 -1
  19. package/dist/brain/brain.js +106 -44
  20. package/dist/brain/brain.js.map +1 -1
  21. package/dist/brain/intelligence.d.ts.map +1 -1
  22. package/dist/brain/intelligence.js +36 -30
  23. package/dist/brain/intelligence.js.map +1 -1
  24. package/dist/chat/agent-loop.js +1 -1
  25. package/dist/chat/agent-loop.js.map +1 -1
  26. package/dist/chat/notifications.d.ts.map +1 -1
  27. package/dist/chat/notifications.js +4 -0
  28. package/dist/chat/notifications.js.map +1 -1
  29. package/dist/control/intent-router.d.ts +1 -0
  30. package/dist/control/intent-router.d.ts.map +1 -1
  31. package/dist/control/intent-router.js +11 -5
  32. package/dist/control/intent-router.js.map +1 -1
  33. package/dist/curator/curator.d.ts +4 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +141 -27
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/index.d.ts +14 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +12 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/llm/llm-client.d.ts.map +1 -1
  42. package/dist/llm/llm-client.js +1 -0
  43. package/dist/llm/llm-client.js.map +1 -1
  44. package/dist/packs/index.d.ts +3 -2
  45. package/dist/packs/index.d.ts.map +1 -1
  46. package/dist/packs/index.js +3 -2
  47. package/dist/packs/index.js.map +1 -1
  48. package/dist/packs/lockfile.d.ts +23 -1
  49. package/dist/packs/lockfile.d.ts.map +1 -1
  50. package/dist/packs/lockfile.js +50 -4
  51. package/dist/packs/lockfile.js.map +1 -1
  52. package/dist/packs/pack-installer.d.ts +10 -0
  53. package/dist/packs/pack-installer.d.ts.map +1 -1
  54. package/dist/packs/pack-installer.js +69 -2
  55. package/dist/packs/pack-installer.js.map +1 -1
  56. package/dist/packs/pack-lifecycle.d.ts +50 -0
  57. package/dist/packs/pack-lifecycle.d.ts.map +1 -0
  58. package/dist/packs/pack-lifecycle.js +91 -0
  59. package/dist/packs/pack-lifecycle.js.map +1 -0
  60. package/dist/packs/types.d.ts +64 -44
  61. package/dist/packs/types.d.ts.map +1 -1
  62. package/dist/packs/types.js +9 -0
  63. package/dist/packs/types.js.map +1 -1
  64. package/dist/persistence/sqlite-provider.d.ts +5 -1
  65. package/dist/persistence/sqlite-provider.d.ts.map +1 -1
  66. package/dist/persistence/sqlite-provider.js +22 -2
  67. package/dist/persistence/sqlite-provider.js.map +1 -1
  68. package/dist/planning/github-projection.d.ts +8 -8
  69. package/dist/planning/github-projection.d.ts.map +1 -1
  70. package/dist/planning/github-projection.js +42 -42
  71. package/dist/planning/github-projection.js.map +1 -1
  72. package/dist/plugins/types.d.ts +21 -21
  73. package/dist/queue/pipeline-runner.d.ts.map +1 -1
  74. package/dist/queue/pipeline-runner.js +4 -0
  75. package/dist/queue/pipeline-runner.js.map +1 -1
  76. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  77. package/dist/runtime/curator-extra-ops.js +9 -1
  78. package/dist/runtime/curator-extra-ops.js.map +1 -1
  79. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  80. package/dist/runtime/facades/memory-facade.js +169 -0
  81. package/dist/runtime/facades/memory-facade.js.map +1 -1
  82. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  83. package/dist/runtime/orchestrate-ops.js +133 -4
  84. package/dist/runtime/orchestrate-ops.js.map +1 -1
  85. package/dist/runtime/runtime.d.ts.map +1 -1
  86. package/dist/runtime/runtime.js +128 -90
  87. package/dist/runtime/runtime.js.map +1 -1
  88. package/dist/runtime/session-briefing.d.ts.map +1 -1
  89. package/dist/runtime/session-briefing.js +44 -11
  90. package/dist/runtime/session-briefing.js.map +1 -1
  91. package/dist/runtime/shutdown-registry.d.ts +36 -0
  92. package/dist/runtime/shutdown-registry.d.ts.map +1 -0
  93. package/dist/runtime/shutdown-registry.js +74 -0
  94. package/dist/runtime/shutdown-registry.js.map +1 -0
  95. package/dist/runtime/types.d.ts +10 -1
  96. package/dist/runtime/types.d.ts.map +1 -1
  97. package/dist/subagent/concurrency-manager.d.ts +29 -0
  98. package/dist/subagent/concurrency-manager.d.ts.map +1 -0
  99. package/dist/subagent/concurrency-manager.js +73 -0
  100. package/dist/subagent/concurrency-manager.js.map +1 -0
  101. package/dist/subagent/dispatcher.d.ts +41 -0
  102. package/dist/subagent/dispatcher.d.ts.map +1 -0
  103. package/dist/subagent/dispatcher.js +259 -0
  104. package/dist/subagent/dispatcher.js.map +1 -0
  105. package/dist/subagent/index.d.ts +14 -0
  106. package/dist/subagent/index.d.ts.map +1 -0
  107. package/dist/subagent/index.js +15 -0
  108. package/dist/subagent/index.js.map +1 -0
  109. package/dist/subagent/orphan-reaper.d.ts +37 -0
  110. package/dist/subagent/orphan-reaper.d.ts.map +1 -0
  111. package/dist/subagent/orphan-reaper.js +71 -0
  112. package/dist/subagent/orphan-reaper.js.map +1 -0
  113. package/dist/subagent/result-aggregator.d.ts +7 -0
  114. package/dist/subagent/result-aggregator.d.ts.map +1 -0
  115. package/dist/subagent/result-aggregator.js +57 -0
  116. package/dist/subagent/result-aggregator.js.map +1 -0
  117. package/dist/subagent/task-checkout.d.ts +36 -0
  118. package/dist/subagent/task-checkout.d.ts.map +1 -0
  119. package/dist/subagent/task-checkout.js +52 -0
  120. package/dist/subagent/task-checkout.js.map +1 -0
  121. package/dist/subagent/types.d.ts +114 -0
  122. package/dist/subagent/types.d.ts.map +1 -0
  123. package/dist/subagent/types.js +9 -0
  124. package/dist/subagent/types.js.map +1 -0
  125. package/dist/subagent/workspace-resolver.d.ts +35 -0
  126. package/dist/subagent/workspace-resolver.d.ts.map +1 -0
  127. package/dist/subagent/workspace-resolver.js +99 -0
  128. package/dist/subagent/workspace-resolver.js.map +1 -0
  129. package/dist/transport/http-server.d.ts.map +1 -1
  130. package/dist/transport/http-server.js +49 -3
  131. package/dist/transport/http-server.js.map +1 -1
  132. package/dist/transport/ws-server.d.ts.map +1 -1
  133. package/dist/transport/ws-server.js +7 -0
  134. package/dist/transport/ws-server.js.map +1 -1
  135. package/dist/vault/linking.d.ts +3 -4
  136. package/dist/vault/linking.d.ts.map +1 -1
  137. package/dist/vault/linking.js +79 -32
  138. package/dist/vault/linking.js.map +1 -1
  139. package/dist/vault/vault-maintenance.d.ts.map +1 -1
  140. package/dist/vault/vault-maintenance.js +7 -14
  141. package/dist/vault/vault-maintenance.js.map +1 -1
  142. package/dist/vault/vault-memories.d.ts.map +1 -1
  143. package/dist/vault/vault-memories.js +19 -9
  144. package/dist/vault/vault-memories.js.map +1 -1
  145. package/dist/vault/vault-schema.d.ts +1 -0
  146. package/dist/vault/vault-schema.d.ts.map +1 -1
  147. package/dist/vault/vault-schema.js +20 -0
  148. package/dist/vault/vault-schema.js.map +1 -1
  149. package/dist/vault/vault.d.ts.map +1 -1
  150. package/dist/vault/vault.js +7 -3
  151. package/dist/vault/vault.js.map +1 -1
  152. package/package.json +8 -2
  153. package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
  154. package/src/__tests__/adapters/registry.test.ts +100 -0
  155. package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
  156. package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
  157. package/src/__tests__/subagent/dispatcher.test.ts +195 -0
  158. package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
  159. package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
  160. package/src/__tests__/subagent/task-checkout.test.ts +86 -0
  161. package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
  162. package/src/adapters/claude-code-adapter.ts +163 -0
  163. package/src/adapters/index.ts +22 -0
  164. package/src/adapters/registry.ts +53 -0
  165. package/src/adapters/types.ts +114 -0
  166. package/src/curator/curator.ts +1 -0
  167. package/src/index.ts +38 -1
  168. package/src/packs/index.ts +5 -1
  169. package/src/packs/lockfile.ts +70 -5
  170. package/src/packs/pack-installer.ts +78 -2
  171. package/src/packs/pack-lifecycle.ts +115 -0
  172. package/src/packs/pack-lockfile.test.ts +1 -1
  173. package/src/packs/pack-system.test.ts +1 -1
  174. package/src/packs/types.ts +40 -2
  175. package/src/persistence/sqlite-provider.ts +26 -2
  176. package/src/runtime/admin-setup-ops.test.ts +9 -4
  177. package/src/runtime/orchestrate-ops.ts +153 -1
  178. package/src/runtime/runtime.ts +15 -0
  179. package/src/runtime/session-briefing.test.ts +94 -2
  180. package/src/runtime/session-briefing.ts +48 -12
  181. package/src/runtime/types.ts +6 -0
  182. package/src/subagent/concurrency-manager.ts +89 -0
  183. package/src/subagent/dispatcher.ts +326 -0
  184. package/src/subagent/index.ts +28 -0
  185. package/src/subagent/orphan-reaper.ts +82 -0
  186. package/src/subagent/result-aggregator.ts +66 -0
  187. package/src/subagent/task-checkout.ts +60 -0
  188. package/src/subagent/types.ts +138 -0
  189. package/src/subagent/workspace-resolver.ts +117 -0
  190. package/src/vault/vault-scaling.test.ts +3 -2
  191. package/vitest.config.ts +2 -0
  192. package/src/hooks/index.ts +0 -6
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ClaudeCodeRuntimeAdapter } from '../../adapters/claude-code-adapter.js';
3
+ import type { AdapterExecutionContext } from '../../adapters/types.js';
4
+
5
+ vi.mock('node:child_process', () => ({
6
+ execSync: vi.fn(),
7
+ }));
8
+
9
+ function makeContext(overrides?: Partial<AdapterExecutionContext>): AdapterExecutionContext {
10
+ return {
11
+ runId: 'test-run-1',
12
+ prompt: 'Hello world',
13
+ workspace: '/tmp/test-workspace',
14
+ ...overrides,
15
+ };
16
+ }
17
+
18
+ describe('ClaudeCodeRuntimeAdapter', () => {
19
+ let adapter: ClaudeCodeRuntimeAdapter;
20
+
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ adapter = new ClaudeCodeRuntimeAdapter();
24
+ });
25
+
26
+ describe('type', () => {
27
+ it('should have type property set to "claude-code"', () => {
28
+ expect(adapter.type).toBe('claude-code');
29
+ });
30
+ });
31
+
32
+ describe('execute()', () => {
33
+ it('should call the dispatch function with correct args', async () => {
34
+ const dispatch = vi.fn().mockResolvedValue({ exitCode: 0 });
35
+ adapter = new ClaudeCodeRuntimeAdapter(dispatch);
36
+ const ctx = makeContext();
37
+
38
+ await adapter.execute(ctx);
39
+
40
+ expect(dispatch).toHaveBeenCalledTimes(1);
41
+ expect(dispatch).toHaveBeenCalledWith('Hello world', '/tmp/test-workspace', undefined);
42
+ });
43
+
44
+ it('should return a proper AdapterExecutionResult shape', async () => {
45
+ const dispatch = vi.fn().mockResolvedValue({ exitCode: 0, output: 'Done' });
46
+ adapter = new ClaudeCodeRuntimeAdapter(dispatch);
47
+
48
+ const result = await adapter.execute(makeContext());
49
+
50
+ expect(result).toHaveProperty('exitCode');
51
+ expect(typeof result.exitCode).toBe('number');
52
+ expect(result.exitCode).toBe(0);
53
+ });
54
+
55
+ it('should handle dispatch that returns an error exit code', async () => {
56
+ const dispatch = vi.fn().mockResolvedValue({ exitCode: 1 });
57
+ adapter = new ClaudeCodeRuntimeAdapter(dispatch);
58
+
59
+ const result = await adapter.execute(makeContext());
60
+
61
+ expect(result.exitCode).toBe(1);
62
+ });
63
+
64
+ it('should work without a dispatch function and return a default result', async () => {
65
+ adapter = new ClaudeCodeRuntimeAdapter();
66
+
67
+ const result = await adapter.execute(makeContext());
68
+
69
+ expect(result).toHaveProperty('exitCode');
70
+ expect(typeof result.exitCode).toBe('number');
71
+ });
72
+ });
73
+
74
+ describe('testEnvironment()', () => {
75
+ it('should return available: true when CLI exists', async () => {
76
+ const { execSync } = await import('node:child_process');
77
+ const mockedExecSync = vi.mocked(execSync);
78
+ mockedExecSync.mockReturnValue('/usr/local/bin/claude' as any);
79
+
80
+ const result = await adapter.testEnvironment();
81
+
82
+ expect(result.available).toBe(true);
83
+ });
84
+
85
+ it('should return available: false when CLI is missing', async () => {
86
+ const { execSync } = await import('node:child_process');
87
+ const mockedExecSync = vi.mocked(execSync);
88
+ mockedExecSync.mockImplementation(() => {
89
+ throw new Error('command not found: claude');
90
+ });
91
+
92
+ const result = await adapter.testEnvironment();
93
+
94
+ expect(result.available).toBe(false);
95
+ });
96
+
97
+ it('should never throw even if the environment check fails', async () => {
98
+ const { execSync } = await import('node:child_process');
99
+ const mockedExecSync = vi.mocked(execSync);
100
+ mockedExecSync.mockImplementation(() => {
101
+ throw new Error('unexpected catastrophic error');
102
+ });
103
+
104
+ // Should not throw — returns a result with available: false
105
+ const result = await adapter.testEnvironment();
106
+ expect(result).toHaveProperty('available');
107
+ expect(result.available).toBe(false);
108
+ });
109
+ });
110
+
111
+ describe('sessionCodec', () => {
112
+ it('should have a sessionCodec property', () => {
113
+ expect(adapter.sessionCodec).toBeDefined();
114
+ });
115
+
116
+ it('should serialize session state to valid JSON', () => {
117
+ const state = {
118
+ adapterType: 'claude-code',
119
+ data: { sessionId: 'sess-123', conversationId: 'conv-456' },
120
+ };
121
+
122
+ const serialized = adapter.sessionCodec!.serialize(state);
123
+
124
+ expect(() => JSON.parse(serialized)).not.toThrow();
125
+ const parsed = JSON.parse(serialized);
126
+ expect(parsed.adapterType).toBe('claude-code');
127
+ });
128
+
129
+ it('should round-trip serialize and deserialize', () => {
130
+ const state = {
131
+ adapterType: 'claude-code',
132
+ data: { sessionId: 'sess-123', conversationId: 'conv-456' },
133
+ };
134
+
135
+ const serialized = adapter.sessionCodec!.serialize(state);
136
+ const deserialized = adapter.sessionCodec!.deserialize(serialized);
137
+
138
+ expect(deserialized).toEqual(state);
139
+ });
140
+
141
+ it('should return a display ID from getDisplayId()', () => {
142
+ const state = {
143
+ adapterType: 'claude-code',
144
+ data: { sessionId: 'sess-123' },
145
+ };
146
+
147
+ const displayId = adapter.sessionCodec!.getDisplayId(state);
148
+
149
+ expect(typeof displayId).toBe('string');
150
+ expect(displayId).toContain('sess-123');
151
+ });
152
+
153
+ it('should throw on invalid JSON in deserialize()', () => {
154
+ expect(() => adapter.sessionCodec!.deserialize('not-valid-json{')).toThrow();
155
+ });
156
+ });
157
+
158
+ describe('syncSkills()', () => {
159
+ it('should be callable without throwing (smoke test)', async () => {
160
+ if (adapter.syncSkills) {
161
+ await expect(adapter.syncSkills([])).resolves.not.toThrow();
162
+ }
163
+ // If syncSkills is not defined, that's also acceptable per the interface
164
+ expect(true).toBe(true);
165
+ });
166
+ });
167
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { RuntimeAdapterRegistry } from '../../adapters/registry.js';
3
+ import type { RuntimeAdapter } from '../../adapters/types.js';
4
+
5
+ function createMockAdapter(type: string): RuntimeAdapter {
6
+ return {
7
+ type,
8
+ execute: vi.fn().mockResolvedValue({ exitCode: 0 }),
9
+ testEnvironment: vi.fn().mockResolvedValue({ available: true }),
10
+ };
11
+ }
12
+
13
+ describe('RuntimeAdapterRegistry', () => {
14
+ let registry: RuntimeAdapterRegistry;
15
+
16
+ beforeEach(() => {
17
+ registry = new RuntimeAdapterRegistry();
18
+ });
19
+
20
+ it('should store an adapter via register() and retrieve it via get()', () => {
21
+ const adapter = createMockAdapter('claude-code');
22
+ registry.register('claude-code', adapter);
23
+
24
+ const result = registry.get('claude-code');
25
+ expect(result).toBe(adapter);
26
+ });
27
+
28
+ it('should throw when getting an unknown adapter type', () => {
29
+ expect(() => registry.get('nonexistent')).toThrow(/unknown adapter type/i);
30
+ });
31
+
32
+ it('should return all registered types via list()', () => {
33
+ registry.register('claude-code', createMockAdapter('claude-code'));
34
+ registry.register('codex', createMockAdapter('codex'));
35
+
36
+ const types = registry.list();
37
+ expect(types).toContain('claude-code');
38
+ expect(types).toContain('codex');
39
+ expect(types).toHaveLength(2);
40
+ });
41
+
42
+ it('should set the default adapter via setDefault()', () => {
43
+ const adapter = createMockAdapter('claude-code');
44
+ registry.register('claude-code', adapter);
45
+ registry.setDefault('claude-code');
46
+
47
+ const result = registry.getDefault();
48
+ expect(result).toBe(adapter);
49
+ });
50
+
51
+ it('should return the default adapter via getDefault()', () => {
52
+ const a1 = createMockAdapter('claude-code');
53
+ const a2 = createMockAdapter('codex');
54
+ registry.register('claude-code', a1);
55
+ registry.register('codex', a2);
56
+ registry.setDefault('codex');
57
+
58
+ expect(registry.getDefault()).toBe(a2);
59
+ });
60
+
61
+ it('should throw when setDefault() is called with an unregistered type', () => {
62
+ expect(() => registry.setDefault('nonexistent')).toThrow(/unregistered type/i);
63
+ });
64
+
65
+ it('should throw when register() is called with a duplicate type', () => {
66
+ registry.register('claude-code', createMockAdapter('claude-code'));
67
+
68
+ expect(() => registry.register('claude-code', createMockAdapter('claude-code'))).toThrow(
69
+ /already registered/i,
70
+ );
71
+ });
72
+
73
+ it('should return an empty array from list() when no adapters are registered', () => {
74
+ expect(registry.list()).toEqual([]);
75
+ });
76
+
77
+ it('should allow multiple adapters to coexist', () => {
78
+ const adapters = ['claude-code', 'codex', 'cursor'];
79
+ adapters.forEach((type) => registry.register(type, createMockAdapter(type)));
80
+
81
+ expect(registry.list()).toHaveLength(3);
82
+ adapters.forEach((type) => {
83
+ expect(registry.get(type).type).toBe(type);
84
+ });
85
+ });
86
+
87
+ it('should throw when getDefault() is called with no default set', () => {
88
+ expect(() => registry.getDefault()).toThrow(/no default/i);
89
+ });
90
+
91
+ it('should include registered type names in error message when get() fails', () => {
92
+ registry.register('codex', createMockAdapter('codex'));
93
+
94
+ expect(() => registry.get('missing')).toThrow(/codex/);
95
+ });
96
+
97
+ it('should show "(none)" in error message when no adapters are registered and get() fails', () => {
98
+ expect(() => registry.get('missing')).toThrow(/\(none\)/);
99
+ });
100
+ });
@@ -0,0 +1,379 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { PackLifecycleManager } from '../../packs/pack-lifecycle.js';
3
+ import { VALID_TRANSITIONS } from '../../packs/types.js';
4
+ import type { PackState } from '../../packs/types.js';
5
+
6
+ // =============================================================================
7
+ // HELPERS
8
+ // =============================================================================
9
+
10
+ let manager: PackLifecycleManager;
11
+
12
+ beforeEach(() => {
13
+ manager = new PackLifecycleManager();
14
+ });
15
+
16
+ // =============================================================================
17
+ // VALID TRANSITIONS
18
+ // =============================================================================
19
+
20
+ describe('PackLifecycleManager — valid transitions', () => {
21
+ it('installed → ready', () => {
22
+ manager.initState('p1', 'installed');
23
+ manager.transition('p1', 'ready');
24
+ expect(manager.getState('p1')).toBe('ready');
25
+ });
26
+
27
+ it('installed → error', () => {
28
+ manager.initState('p1', 'installed');
29
+ manager.transition('p1', 'error', 'activation failed');
30
+ expect(manager.getState('p1')).toBe('error');
31
+ });
32
+
33
+ it('installed → uninstalled', () => {
34
+ manager.initState('p1', 'installed');
35
+ manager.transition('p1', 'uninstalled');
36
+ expect(manager.getState('p1')).toBe('uninstalled');
37
+ });
38
+
39
+ it('ready → ready (self-transition)', () => {
40
+ manager.initState('p1', 'ready');
41
+ manager.transition('p1', 'ready', 'reloaded');
42
+ expect(manager.getState('p1')).toBe('ready');
43
+ });
44
+
45
+ it('ready → disabled', () => {
46
+ manager.initState('p1', 'ready');
47
+ manager.transition('p1', 'disabled');
48
+ expect(manager.getState('p1')).toBe('disabled');
49
+ });
50
+
51
+ it('ready → error', () => {
52
+ manager.initState('p1', 'ready');
53
+ manager.transition('p1', 'error', 'runtime failure');
54
+ expect(manager.getState('p1')).toBe('error');
55
+ });
56
+
57
+ it('ready → upgrade_pending', () => {
58
+ manager.initState('p1', 'ready');
59
+ manager.transition('p1', 'upgrade_pending');
60
+ expect(manager.getState('p1')).toBe('upgrade_pending');
61
+ });
62
+
63
+ it('ready → uninstalled', () => {
64
+ manager.initState('p1', 'ready');
65
+ manager.transition('p1', 'uninstalled');
66
+ expect(manager.getState('p1')).toBe('uninstalled');
67
+ });
68
+
69
+ it('disabled → ready', () => {
70
+ manager.initState('p1', 'disabled');
71
+ manager.transition('p1', 'ready');
72
+ expect(manager.getState('p1')).toBe('ready');
73
+ });
74
+
75
+ it('disabled → uninstalled', () => {
76
+ manager.initState('p1', 'disabled');
77
+ manager.transition('p1', 'uninstalled');
78
+ expect(manager.getState('p1')).toBe('uninstalled');
79
+ });
80
+
81
+ it('error → ready', () => {
82
+ manager.initState('p1', 'error');
83
+ manager.transition('p1', 'ready', 'retry succeeded');
84
+ expect(manager.getState('p1')).toBe('ready');
85
+ });
86
+
87
+ it('error → uninstalled', () => {
88
+ manager.initState('p1', 'error');
89
+ manager.transition('p1', 'uninstalled');
90
+ expect(manager.getState('p1')).toBe('uninstalled');
91
+ });
92
+
93
+ it('upgrade_pending → ready', () => {
94
+ manager.initState('p1', 'upgrade_pending');
95
+ manager.transition('p1', 'ready', 'upgrade complete');
96
+ expect(manager.getState('p1')).toBe('ready');
97
+ });
98
+
99
+ it('upgrade_pending → error', () => {
100
+ manager.initState('p1', 'upgrade_pending');
101
+ manager.transition('p1', 'error', 'upgrade failed');
102
+ expect(manager.getState('p1')).toBe('error');
103
+ });
104
+
105
+ it('upgrade_pending → uninstalled', () => {
106
+ manager.initState('p1', 'upgrade_pending');
107
+ manager.transition('p1', 'uninstalled');
108
+ expect(manager.getState('p1')).toBe('uninstalled');
109
+ });
110
+
111
+ it('uninstalled → installed', () => {
112
+ manager.initState('p1', 'uninstalled');
113
+ manager.transition('p1', 'installed', 'reinstalled');
114
+ expect(manager.getState('p1')).toBe('installed');
115
+ });
116
+ });
117
+
118
+ // =============================================================================
119
+ // INVALID TRANSITIONS
120
+ // =============================================================================
121
+
122
+ describe('PackLifecycleManager — invalid transitions', () => {
123
+ it('installed → disabled throws', () => {
124
+ manager.initState('p1', 'installed');
125
+ expect(() => manager.transition('p1', 'disabled')).toThrow();
126
+ });
127
+
128
+ it('disabled → error throws', () => {
129
+ manager.initState('p1', 'disabled');
130
+ expect(() => manager.transition('p1', 'error')).toThrow();
131
+ });
132
+
133
+ it('uninstalled → ready throws', () => {
134
+ manager.initState('p1', 'uninstalled');
135
+ expect(() => manager.transition('p1', 'ready')).toThrow();
136
+ });
137
+
138
+ it('error → disabled throws', () => {
139
+ manager.initState('p1', 'error');
140
+ expect(() => manager.transition('p1', 'disabled')).toThrow();
141
+ });
142
+
143
+ it('error message includes current state, target, and valid targets', () => {
144
+ manager.initState('p1', 'installed');
145
+ try {
146
+ manager.transition('p1', 'disabled');
147
+ expect.unreachable('should have thrown');
148
+ } catch (err) {
149
+ const msg = (err as Error).message;
150
+ expect(msg).toContain('installed');
151
+ expect(msg).toContain('disabled');
152
+ // Valid targets from 'installed' are: ready, error, uninstalled
153
+ expect(msg).toContain('ready');
154
+ expect(msg).toContain('error');
155
+ expect(msg).toContain('uninstalled');
156
+ }
157
+ });
158
+
159
+ it('transition on untracked pack throws', () => {
160
+ expect(() => manager.transition('ghost', 'ready')).toThrow("Pack 'ghost' is not being tracked");
161
+ });
162
+
163
+ it('installed → upgrade_pending throws', () => {
164
+ manager.initState('p1', 'installed');
165
+ expect(() => manager.transition('p1', 'upgrade_pending')).toThrow();
166
+ });
167
+ });
168
+
169
+ // =============================================================================
170
+ // CORE BEHAVIOR
171
+ // =============================================================================
172
+
173
+ describe('PackLifecycleManager — core behavior', () => {
174
+ it('initState() sets state without validation', () => {
175
+ // initState should accept any state without checking transitions
176
+ manager.initState('p1', 'error');
177
+ expect(manager.getState('p1')).toBe('error');
178
+ });
179
+
180
+ it('initState() overwrites existing state without validation', () => {
181
+ manager.initState('p1', 'installed');
182
+ manager.initState('p1', 'ready');
183
+ expect(manager.getState('p1')).toBe('ready');
184
+ // Transitions should be reset since initState creates a fresh entry
185
+ expect(manager.getTransitions('p1')).toEqual([]);
186
+ });
187
+
188
+ it('getState() returns undefined for unknown pack', () => {
189
+ expect(manager.getState('nonexistent')).toBeUndefined();
190
+ });
191
+
192
+ it('getState() returns correct state after transition', () => {
193
+ manager.initState('p1', 'installed');
194
+ manager.transition('p1', 'ready');
195
+ manager.transition('p1', 'disabled');
196
+ expect(manager.getState('p1')).toBe('disabled');
197
+ });
198
+
199
+ it('getTransitions() returns empty array for new pack', () => {
200
+ manager.initState('p1', 'installed');
201
+ expect(manager.getTransitions('p1')).toEqual([]);
202
+ });
203
+
204
+ it('getTransitions() returns empty array for unknown pack', () => {
205
+ expect(manager.getTransitions('ghost')).toEqual([]);
206
+ });
207
+
208
+ it('getTransitions() records all transitions with timestamps', () => {
209
+ manager.initState('p1', 'installed');
210
+ manager.transition('p1', 'ready', 'activated');
211
+ manager.transition('p1', 'disabled', 'user disabled');
212
+ manager.transition('p1', 'ready', 're-enabled');
213
+
214
+ const transitions = manager.getTransitions('p1');
215
+ expect(transitions).toHaveLength(3);
216
+
217
+ expect(transitions[0].from).toBe('installed');
218
+ expect(transitions[0].to).toBe('ready');
219
+ expect(transitions[0].reason).toBe('activated');
220
+ expect(typeof transitions[0].timestamp).toBe('number');
221
+
222
+ expect(transitions[1].from).toBe('ready');
223
+ expect(transitions[1].to).toBe('disabled');
224
+ expect(transitions[1].reason).toBe('user disabled');
225
+
226
+ expect(transitions[2].from).toBe('disabled');
227
+ expect(transitions[2].to).toBe('ready');
228
+ expect(transitions[2].reason).toBe('re-enabled');
229
+
230
+ // Timestamps should be monotonically non-decreasing
231
+ expect(transitions[1].timestamp).toBeGreaterThanOrEqual(transitions[0].timestamp);
232
+ expect(transitions[2].timestamp).toBeGreaterThanOrEqual(transitions[1].timestamp);
233
+ });
234
+
235
+ it('onTransition() callback fires on every transition', () => {
236
+ const listener = vi.fn();
237
+ manager.onTransition(listener);
238
+ manager.initState('p1', 'installed');
239
+ manager.transition('p1', 'ready', 'activated');
240
+
241
+ expect(listener).toHaveBeenCalledTimes(1);
242
+ expect(listener).toHaveBeenCalledWith('p1', 'installed', 'ready', 'activated');
243
+ });
244
+
245
+ it('onTransition() returns unsubscribe function that works', () => {
246
+ const listener = vi.fn();
247
+ const unsub = manager.onTransition(listener);
248
+
249
+ manager.initState('p1', 'installed');
250
+ manager.transition('p1', 'ready');
251
+ expect(listener).toHaveBeenCalledTimes(1);
252
+
253
+ unsub();
254
+ manager.transition('p1', 'disabled');
255
+ expect(listener).toHaveBeenCalledTimes(1); // not called again
256
+ });
257
+
258
+ it('multiple listeners all fire', () => {
259
+ const listener1 = vi.fn();
260
+ const listener2 = vi.fn();
261
+ const listener3 = vi.fn();
262
+
263
+ manager.onTransition(listener1);
264
+ manager.onTransition(listener2);
265
+ manager.onTransition(listener3);
266
+
267
+ manager.initState('p1', 'installed');
268
+ manager.transition('p1', 'ready');
269
+
270
+ expect(listener1).toHaveBeenCalledTimes(1);
271
+ expect(listener2).toHaveBeenCalledTimes(1);
272
+ expect(listener3).toHaveBeenCalledTimes(1);
273
+ });
274
+
275
+ it('remove() clears pack state', () => {
276
+ manager.initState('p1', 'installed');
277
+ manager.transition('p1', 'ready');
278
+ manager.remove('p1');
279
+
280
+ expect(manager.getState('p1')).toBeUndefined();
281
+ expect(manager.getTransitions('p1')).toEqual([]);
282
+ });
283
+
284
+ it('listAll() returns all tracked packs', () => {
285
+ manager.initState('alpha', 'installed');
286
+ manager.initState('beta', 'ready');
287
+ manager.initState('gamma', 'disabled');
288
+
289
+ const all = manager.listAll();
290
+ expect(all).toHaveLength(3);
291
+ expect(all).toEqual(
292
+ expect.arrayContaining([
293
+ { packId: 'alpha', state: 'installed' },
294
+ { packId: 'beta', state: 'ready' },
295
+ { packId: 'gamma', state: 'disabled' },
296
+ ]),
297
+ );
298
+ });
299
+
300
+ it('listAll() returns empty array when no packs tracked', () => {
301
+ expect(manager.listAll()).toEqual([]);
302
+ });
303
+
304
+ it('reset() clears everything', () => {
305
+ const listener = vi.fn();
306
+ manager.onTransition(listener);
307
+
308
+ manager.initState('p1', 'installed');
309
+ manager.initState('p2', 'ready');
310
+ manager.transition('p1', 'ready');
311
+
312
+ manager.reset();
313
+
314
+ expect(manager.listAll()).toEqual([]);
315
+ expect(manager.getState('p1')).toBeUndefined();
316
+ expect(manager.getState('p2')).toBeUndefined();
317
+
318
+ // Listeners should be cleared too — re-init and transition should not fire old listener
319
+ manager.initState('p3', 'installed');
320
+ manager.transition('p3', 'ready');
321
+ expect(listener).toHaveBeenCalledTimes(1); // only the pre-reset call
322
+ });
323
+
324
+ it('transition reason is optional', () => {
325
+ manager.initState('p1', 'installed');
326
+ manager.transition('p1', 'ready');
327
+
328
+ const transitions = manager.getTransitions('p1');
329
+ expect(transitions[0].reason).toBeUndefined();
330
+ });
331
+ });
332
+
333
+ // =============================================================================
334
+ // BACKWARD COMPATIBILITY
335
+ // =============================================================================
336
+
337
+ describe('PackLifecycleManager — backward compatibility', () => {
338
+ it('PackState includes old PackStatus values (installed, error, uninstalled)', () => {
339
+ // These are the three original PackStatus values — they must remain valid PackState values
340
+ const oldStatuses: PackState[] = ['installed', 'error', 'uninstalled'];
341
+ for (const status of oldStatuses) {
342
+ manager.initState('test', status);
343
+ expect(manager.getState('test')).toBe(status);
344
+ }
345
+ });
346
+
347
+ it('VALID_TRANSITIONS is exported and has all 6 states as keys', () => {
348
+ const expectedStates: PackState[] = [
349
+ 'installed',
350
+ 'ready',
351
+ 'disabled',
352
+ 'error',
353
+ 'upgrade_pending',
354
+ 'uninstalled',
355
+ ];
356
+ const keys = Object.keys(VALID_TRANSITIONS);
357
+ expect(keys).toHaveLength(6);
358
+ for (const state of expectedStates) {
359
+ expect(VALID_TRANSITIONS).toHaveProperty(state);
360
+ expect(Array.isArray(VALID_TRANSITIONS[state])).toBe(true);
361
+ }
362
+ });
363
+
364
+ it('VALID_TRANSITIONS values are all valid PackState values', () => {
365
+ const allStates: PackState[] = [
366
+ 'installed',
367
+ 'ready',
368
+ 'disabled',
369
+ 'error',
370
+ 'upgrade_pending',
371
+ 'uninstalled',
372
+ ];
373
+ for (const [_from, targets] of Object.entries(VALID_TRANSITIONS)) {
374
+ for (const target of targets) {
375
+ expect(allStates).toContain(target);
376
+ }
377
+ }
378
+ });
379
+ });