agent-ecosystem/test/features/member-work-sync/main/createMemberWorkSyncFeature.test.ts
2026-05-02 20:24:46 +03:00

220 lines
6.6 KiB
TypeScript

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