import { mkdir, mkdtemp, readFile, readdir, rename, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type { RuntimeExtensionCatalogEntry } from '../../shared/runtime-extension-contracts' import { RuntimeExtensionStore, type RuntimeExtensionStoreDependencies } from './runtime-extension-store' const temporaryDirectories: string[] = [] function catalogEntry(version = '1.0.0'): RuntimeExtensionCatalogEntry { return { id: 'test-extension', package: { name: '@goodbuddy/test-extension', version }, displayName: 'Test extension', description: 'A deterministic extension store fixture.' } } async function defaultInstall(input: { destinationDirectory: string }): Promise<{ entrypoint: string; integrity: string }> { const distribution = join(input.destinationDirectory, 'dist') await mkdir(distribution, { recursive: true }) await writeFile(join(distribution, 'index.js'), 'export default {}') return { entrypoint: 'dist/index.js', integrity: `sha512-${Buffer.from('verified').toString('base64')}` } } async function fixture(input: { entries?: RuntimeExtensionCatalogEntry[] install?: RuntimeExtensionStoreDependencies['install'] temporaryIds?: string[] marketplaceEnabled?: boolean } = {}): Promise<{ userDataPath: string store: RuntimeExtensionStore dependencies: RuntimeExtensionStoreDependencies }> { const userDataPath = await mkdtemp( join(tmpdir(), 'goodbuddy-extension-store-') ) temporaryDirectories.push(userDataPath) const entries = input.entries ?? [catalogEntry()] const temporaryIds = input.temporaryIds ?? ['install-one'] const dependencies: RuntimeExtensionStoreDependencies = { catalog: { list: vi.fn(async () => entries) }, install: vi.fn(input.install ?? defaultInstall), now: () => new Date('2026-08-16T00:00:00.000Z'), temporaryId: () => { const id = temporaryIds.shift() if (!id) { throw new Error('No fixture temporary ID remains') } return id } } const store = new RuntimeExtensionStore(userDataPath, dependencies) if (input.marketplaceEnabled ?? true) { await store.apply({ type: 'set-marketplace-enabled', enabled: true }) } return { userDataPath, dependencies, store } } afterEach(async () => { await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }) ) ) }) describe('RuntimeExtensionStore', () => { it('keeps a fresh marketplace disabled without loading the catalog', async () => { const { store, dependencies } = await fixture({ marketplaceEnabled: false }) const entry = catalogEntry() await expect(store.getSnapshot()).resolves.toEqual({ marketplaceEnabled: false, catalog: [], installed: [] }) expect(dependencies.catalog.list).not.toHaveBeenCalled() await expect( store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) ).rejects.toThrow('marketplace is disabled') await expect( store.applyWithResult({ type: 'set-marketplace-enabled', enabled: true }) ).resolves.toMatchObject({ changed: true, snapshot: { marketplaceEnabled: true } }) await expect(store.getSnapshot()).resolves.toMatchObject({ marketplaceEnabled: true, catalog: [entry] }) }) it('keeps the marketplace enabled when migrating installed version 1 state', async () => { const { userDataPath, dependencies } = await fixture({ marketplaceEnabled: false }) const entry = catalogEntry() const extensionDirectory = join( userDataPath, 'runtime-extensions', 'extensions', entry.id ) await mkdir(join(extensionDirectory, 'dist'), { recursive: true }) const entrypoint = join(extensionDirectory, 'dist', 'index.js') await writeFile(entrypoint, 'export default {}') await writeFile( join(userDataPath, 'runtime-extensions', 'store.json'), JSON.stringify({ version: 1, installed: [ { id: entry.id, package: entry.package, entrypoint, installedAt: '2026-08-16T00:00:00.000Z', enabled: true, configuration: {} } ] }), 'utf8' ) const migrated = new RuntimeExtensionStore( userDataPath, dependencies ) await expect(migrated.getSnapshot()).resolves.toMatchObject({ marketplaceEnabled: true, installed: [ expect.objectContaining({ id: entry.id, enabled: true }) ] }) await expect( readFile( join(userDataPath, 'runtime-extensions', 'store.json'), 'utf8' ).then((value) => JSON.parse(value) as unknown) ).resolves.toMatchObject({ version: 2, marketplaceEnabled: true }) }) it('installs one exact, integrity-verified package directory', async () => { const { userDataPath, store, dependencies } = await fixture() const entry = catalogEntry() const snapshot = await store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) const extensionDirectory = join( userDataPath, 'runtime-extensions', 'extensions', entry.id ) expect(snapshot.installed).toEqual([ { id: entry.id, package: entry.package, installedAt: '2026-08-16T00:00:00.000Z', enabled: true, configuration: {}, integrity: `sha512-${Buffer.from('verified').toString('base64')}` } ]) await expect(store.getEnabledExtensions()).resolves.toEqual([ expect.objectContaining({ id: entry.id, entrypoint: join(extensionDirectory, 'dist', 'index.js') }) ]) await expect( readFile(join(extensionDirectory, 'dist', 'index.js'), 'utf8') ).resolves.toBe('export default {}') expect(dependencies.install).toHaveBeenCalledWith( expect.objectContaining({ destinationDirectory: expect.stringMatching( /\.staging[\\/]install-one$/u ) }) ) }) it('hides the marketplace without disabling installed plugins', async () => { const { store, dependencies } = await fixture() const entry = catalogEntry() await store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) vi.mocked(dependencies.catalog.list).mockClear() await expect( store.apply({ type: 'set-marketplace-enabled', enabled: false }) ).resolves.toMatchObject({ marketplaceEnabled: false, catalog: [], installed: [ expect.objectContaining({ id: entry.id, enabled: true }) ] }) expect(dependencies.catalog.list).not.toHaveBeenCalled() await expect(store.getEnabledExtensions()).resolves.toEqual([ expect.objectContaining({ id: entry.id }) ]) }) it('leaves an existing installation untouched when an upgrade fails', async () => { const first = catalogEntry('1.0.0') const second = catalogEntry('2.0.0') const { store, dependencies } = await fixture({ entries: [first], temporaryIds: ['install-one', 'install-two'] }) await store.apply({ type: 'install', extensionId: first.id, package: first.package }) await store.apply({ type: 'configure', extensionId: first.id, configuration: { nested: { value: 1 } } }) await store.apply({ type: 'set-enabled', extensionId: first.id, enabled: true }) vi.mocked(dependencies.catalog.list).mockResolvedValue([second]) vi.mocked(dependencies.install).mockRejectedValueOnce( new Error('Entrypoint contract mismatch') ) await expect( store.apply({ type: 'install', extensionId: second.id, package: second.package }) ).rejects.toThrow('Entrypoint contract mismatch') expect((await store.getSnapshot()).installed[0]).toMatchObject({ package: first.package, enabled: true, configuration: { nested: { value: 1 } } }) }) it('rolls back an interrupted upgrade whose state was not committed', async () => { const first = catalogEntry('1.0.0') const second = catalogEntry('2.0.0') const { userDataPath, store, dependencies } = await fixture({ entries: [first], temporaryIds: ['install-one'] }) await store.apply({ type: 'install', extensionId: first.id, package: first.package }) const root = join(userDataPath, 'runtime-extensions') const statePath = join(root, 'store.json') const before = JSON.parse(await readFile(statePath, 'utf8')) as { version: 2 marketplaceEnabled: boolean installed: Array> } const after = structuredClone(before) after.installed[0] = { ...after.installed[0], package: second.package, installedAt: '2026-08-17T00:00:00.000Z', integrity: `sha512-${Buffer.from('new').toString('base64')}` } const extensionDirectory = join( root, 'extensions', first.id ) const backupDirectory = join( root, '.staging', 'upgrade-crash-previous' ) await rename(extensionDirectory, backupDirectory) await mkdir(join(extensionDirectory, 'dist'), { recursive: true }) await writeFile( join(extensionDirectory, 'dist', 'index.js'), 'export default "new"' ) await writeFile( join(root, '.mutation-journal.json'), JSON.stringify({ version: 1, kind: 'install', extensionId: first.id, temporaryId: 'upgrade-crash', before, after }) ) const recovered = new RuntimeExtensionStore( userDataPath, dependencies ) await expect(recovered.getSnapshot()).resolves.toMatchObject({ installed: [ expect.objectContaining({ package: first.package }) ] }) await expect( readFile(join(extensionDirectory, 'dist', 'index.js'), 'utf8') ).resolves.toBe('export default {}') await expect(readdir(join(root, '.staging'))).resolves.toEqual([]) await expect(readdir(root)).resolves.not.toContain( '.mutation-journal.json' ) }) it('finishes an interrupted committed removal on initialization', async () => { const { userDataPath, store, dependencies } = await fixture({ temporaryIds: ['install-one'] }) const entry = catalogEntry() await store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) const root = join(userDataPath, 'runtime-extensions') const statePath = join(root, 'store.json') const before = JSON.parse(await readFile(statePath, 'utf8')) as { version: 2 marketplaceEnabled: boolean installed: Array> } const after = { ...before, installed: [] } const trashDirectory = join( root, '.staging', 'remove-crash-removed' ) await rename( join(root, 'extensions', entry.id), trashDirectory ) await writeFile(statePath, JSON.stringify(after)) await writeFile( join(root, '.mutation-journal.json'), JSON.stringify({ version: 1, kind: 'remove', extensionId: entry.id, temporaryId: 'remove-crash', before, after }) ) const recovered = new RuntimeExtensionStore( userDataPath, dependencies ) await expect(recovered.getSnapshot()).resolves.toMatchObject({ installed: [] }) await expect(readdir(join(root, '.staging'))).resolves.toEqual([]) await expect(readdir(join(root, 'extensions'))).resolves.toEqual([]) await expect(readdir(root)).resolves.not.toContain( '.mutation-journal.json' ) }) it('cleans only safe unjournaled managed staging directories', async () => { const { userDataPath, dependencies } = await fixture({ marketplaceEnabled: false }) const root = join(userDataPath, 'runtime-extensions') const staging = join(root, '.staging') const abandoned = '00000000-0000-4000-8000-000000000101' const abandonedBackup = '00000000-0000-4000-8000-000000000102-previous' const unrelated = 'user-staging-backup' const outside = join(userDataPath, 'outside-staging') const linked = '00000000-0000-4000-8000-000000000103-removed' await mkdir(staging, { recursive: true }) await Promise.all([ mkdir(join(staging, abandoned)), mkdir(join(staging, abandonedBackup)), mkdir(join(staging, unrelated)), mkdir(outside) ]) await writeFile(join(staging, abandoned, 'partial.js'), 'stale') await writeFile(join(staging, unrelated, 'keep.txt'), 'keep') await writeFile(join(outside, 'keep.txt'), 'outside') await symlink(outside, join(staging, linked), 'junction') const recovered = new RuntimeExtensionStore( userDataPath, dependencies ) await recovered.getSnapshot() expect(await readdir(staging)).toEqual( expect.arrayContaining([unrelated, linked]) ) expect(await readdir(staging)).not.toContain(abandoned) expect(await readdir(staging)).not.toContain(abandonedBackup) await expect( readFile(join(staging, unrelated, 'keep.txt'), 'utf8') ).resolves.toBe('keep') await expect( readFile(join(outside, 'keep.txt'), 'utf8') ).resolves.toBe('outside') }) it('rejects installer entrypoints outside the managed package', async () => { const entry = catalogEntry() const fixtureValue = await fixture({ entries: [entry], install: async () => ({ entrypoint: '../outside.js' }) }) await expect( fixtureValue.store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) ).rejects.toThrow('invalid entrypoint') expect( (await fixtureValue.store.getSnapshot()).installed ).toEqual([]) }) it('configures, launches, and disables startup failures', async () => { const { store } = await fixture() const entry = catalogEntry() await store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) await store.apply({ type: 'configure', extensionId: entry.id, configuration: { endpoint: 'https://example.com', options: { retries: 2, tags: ['one', 'two'] } } }) await expect(store.getEnabledExtensions()).resolves.toEqual([ expect.objectContaining({ id: entry.id, configuration: { endpoint: 'https://example.com', options: { retries: 2, tags: ['one', 'two'] } } }) ]) await store.markStartupFailed([entry.id, 'not-installed']) expect((await store.getSnapshot()).installed[0]).toMatchObject({ enabled: false, lastError: 'startup-failed' }) await expect(store.getEnabledExtensions()).resolves.toEqual([]) }) it('reports semantic no-op mutations without refreshing the catalog', async () => { const { store, dependencies } = await fixture() const entry = catalogEntry() await store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) await store.apply({ type: 'configure', extensionId: entry.id, configuration: { first: 1, second: 2 } }) vi.mocked(dependencies.catalog.list).mockClear() await expect( store.applyWithResult({ type: 'configure', extensionId: entry.id, configuration: { second: 2, first: 1 } }) ).resolves.toMatchObject({ changed: false }) await expect( store.applyWithResult({ type: 'set-enabled', extensionId: entry.id, enabled: true }) ).resolves.toMatchObject({ changed: false }) expect(dependencies.catalog.list).not.toHaveBeenCalled() }) it('keeps installed extensions manageable while the catalog is offline', async () => { const { store, dependencies } = await fixture() const entry = catalogEntry() await store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) vi.mocked(dependencies.catalog.list).mockRejectedValue( new Error('npm registry unavailable') ) await expect(store.getSnapshot()).resolves.toMatchObject({ catalog: [], catalogError: 'npm registry unavailable', installed: [ expect.objectContaining({ id: entry.id, enabled: true }) ] }) vi.mocked(dependencies.catalog.list).mockClear() await expect( store.apply({ type: 'set-enabled', extensionId: entry.id, enabled: false }) ).resolves.toMatchObject({ catalog: [], catalogError: 'npm registry unavailable', installed: [ expect.objectContaining({ id: entry.id, enabled: false }) ] }) expect(dependencies.catalog.list).not.toHaveBeenCalled() }) it('serializes mutations and removes only its managed extension directory', async () => { const { userDataPath, store } = await fixture() const entry = catalogEntry() const outsidePath = join(userDataPath, 'outside.txt') await writeFile(outsidePath, 'preserve me') await store.apply({ type: 'install', extensionId: entry.id, package: entry.package }) const configured = store.apply({ type: 'configure', extensionId: entry.id, configuration: { order: 1 } }) const enabled = store.apply({ type: 'set-enabled', extensionId: entry.id, enabled: true }) await Promise.all([configured, enabled]) expect((await store.getSnapshot()).installed[0]).toMatchObject({ enabled: true, configuration: { order: 1 } }) await store.apply({ type: 'remove', extensionId: entry.id }) await expect(readFile(outsidePath, 'utf8')).resolves.toBe('preserve me') expect((await store.getSnapshot()).installed).toEqual([]) await expect( readdir(join(userDataPath, 'runtime-extensions', 'extensions')) ).resolves.toEqual([]) }) })