import fs from 'fs'; import os from 'os'; import path from 'path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { MEMBER_WORK_SYNC_NUDGE_SIDE_EFFECTS_ENV, buildMemberWorkSyncRuntimeTurnSettledEnvironment, createMemberWorkSyncFeature, resolveMemberWorkSyncNudgeSideEffectsEnabled, } from '@features/member-work-sync/main'; import { RUNTIME_TURN_SETTLED_SPOOL_ROOT_ENV } from '@features/member-work-sync/main/infrastructure/runtimeTurnSettledEnvironment'; const tempRoots: string[] = []; function makeTempRoot(): string { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'member-work-sync-feature-')); tempRoots.push(root); return root; } afterEach(() => { for (const root of tempRoots.splice(0)) { fs.rmSync(root, { recursive: true, force: true }); } }); describe('createMemberWorkSyncFeature composition', () => { it('keeps nudge side effects opt-in even when shadow readiness becomes green', () => { expect(resolveMemberWorkSyncNudgeSideEffectsEnabled({})).toBe(false); expect( resolveMemberWorkSyncNudgeSideEffectsEnabled({ [MEMBER_WORK_SYNC_NUDGE_SIDE_EFFECTS_ENV]: 'maybe', }) ).toBe(false); }); it.each(['1', 'true', 'yes', 'on'])( 'enables nudge side effects only for explicit truthy env value %s', (value) => { expect( resolveMemberWorkSyncNudgeSideEffectsEnabled({ [MEMBER_WORK_SYNC_NUDGE_SIDE_EFFECTS_ENV]: value, }) ).toBe(true); } ); it.each(['0', 'false', 'no', 'off', ''])( 'keeps nudge side effects disabled for explicit falsy env value %s', (value) => { expect( resolveMemberWorkSyncNudgeSideEffectsEnabled({ [MEMBER_WORK_SYNC_NUDGE_SIDE_EFFECTS_ENV]: value, }) ).toBe(false); } ); it('returns an empty dispatch summary when nudge side effects are disabled', async () => { const feature = createMemberWorkSyncFeature({ teamsBasePath: makeTempRoot(), configReader: {} as never, taskReader: {} as never, kanbanManager: {} as never, membersMetaStore: {} as never, nudgeSideEffectsEnabled: false, }); try { await expect(feature.dispatchDueNudges(['team-a'])).resolves.toEqual({ claimed: 0, delivered: 0, superseded: 0, retryable: 0, terminal: 0, }); } finally { await feature.dispose(); } }); it('uses snapshot config reads for startup roster materialization', async () => { const getConfig = vi.fn(async () => ({ members: [] })); const getConfigSnapshot = vi.fn(async () => ({ members: [{ name: 'alice' }], })); const feature = createMemberWorkSyncFeature({ teamsBasePath: makeTempRoot(), configReader: { getConfig, getConfigSnapshot, } as never, taskReader: {} as never, kanbanManager: {} as never, membersMetaStore: { getMembers: vi.fn(async () => []), } as never, nudgeSideEffectsEnabled: false, }); try { await feature.enqueueStartupScan(['my-team']); expect(getConfigSnapshot).toHaveBeenCalledWith('my-team'); expect(getConfig).not.toHaveBeenCalled(); } finally { await feature.dispose(); } }); it('builds Claude Stop hook settings without requiring nudge side effects', async () => { const root = makeTempRoot(); const feature = createMemberWorkSyncFeature({ teamsBasePath: root, configReader: {} as never, taskReader: {} as never, kanbanManager: {} as never, membersMetaStore: {} as never, nudgeSideEffectsEnabled: false, }); try { const settings = await feature.buildRuntimeTurnSettledHookSettings({ provider: 'claude' }); expect(settings).toMatchObject({ hooks: { Stop: [ { hooks: [ { type: 'command', command: expect.stringContaining('agent-teams:member-work-sync-turn-settled:v1'), }, ], }, ], }, }); await expect( fs.promises.stat( path.join(root, '.member-work-sync/runtime-hooks/bin/turn-settled-hook-v1.sh') ) ).resolves.toMatchObject({ mode: expect.any(Number) }); } finally { await feature.dispose(); } }); it('builds Codex turn-settled environment without requiring nudge side effects', async () => { const root = makeTempRoot(); const feature = createMemberWorkSyncFeature({ teamsBasePath: root, configReader: {} as never, taskReader: {} as never, kanbanManager: {} as never, membersMetaStore: {} as never, nudgeSideEffectsEnabled: false, }); try { const env = await feature.buildRuntimeTurnSettledEnvironment({ provider: 'codex' }); expect(env).toEqual({ [RUNTIME_TURN_SETTLED_SPOOL_ROOT_ENV]: path.join( root, '.member-work-sync/runtime-hooks' ), }); await expect( fs.promises.stat(path.join(root, '.member-work-sync/runtime-hooks/incoming')) ).resolves.toMatchObject({ mode: expect.any(Number) }); } finally { await feature.dispose(); } }); it('builds OpenCode turn-settled environment without requiring nudge side effects', async () => { const root = makeTempRoot(); const feature = createMemberWorkSyncFeature({ teamsBasePath: root, configReader: {} as never, taskReader: {} as never, kanbanManager: {} as never, membersMetaStore: {} as never, nudgeSideEffectsEnabled: false, }); try { const env = await feature.buildRuntimeTurnSettledEnvironment({ provider: 'opencode' }); expect(env).toEqual({ [RUNTIME_TURN_SETTLED_SPOOL_ROOT_ENV]: path.join( root, '.member-work-sync/runtime-hooks' ), }); await expect( fs.promises.stat(path.join(root, '.member-work-sync/runtime-hooks/incoming')) ).resolves.toMatchObject({ mode: expect.any(Number) }); } finally { await feature.dispose(); } }); it('builds OpenCode bridge environment before feature facade initialization', async () => { const root = makeTempRoot(); const env = await buildMemberWorkSyncRuntimeTurnSettledEnvironment({ teamsBasePath: root, provider: 'opencode', }); expect(env).toEqual({ [RUNTIME_TURN_SETTLED_SPOOL_ROOT_ENV]: path.join( root, '.member-work-sync/runtime-hooks' ), }); await expect( fs.promises.stat(path.join(root, '.member-work-sync/runtime-hooks/incoming')) ).resolves.toMatchObject({ mode: expect.any(Number) }); }); });