254 lines
8.6 KiB
TypeScript
254 lines
8.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
getMixedLaunchFallbackRecoveryError,
|
|
TeamProvisioningService,
|
|
} from '@main/services/team/TeamProvisioningService';
|
|
|
|
describe('TeamProvisioningService (launch roster discovery)', () => {
|
|
it('inbox fallback keeps -1 names but drops auto-suffixed -2+ when base exists', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{
|
|
listInboxNames: vi.fn(async () => [
|
|
'dev',
|
|
'dev-1',
|
|
'dev-2',
|
|
'dev-3',
|
|
'user',
|
|
'team-lead',
|
|
'DEV-2',
|
|
]),
|
|
} as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const result = await (svc as unknown as any).resolveLaunchExpectedMembers('t', '{}');
|
|
expect(result.source).toBe('inboxes');
|
|
expect(result.members.map((m: { name: string }) => m.name)).toEqual(['dev', 'dev-1']);
|
|
});
|
|
|
|
it('inbox fallback ignores cross-team pseudo and qualified external names', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{
|
|
listInboxNames: vi.fn(async () => [
|
|
'dev',
|
|
'cross-team:team-alpha-super',
|
|
'cross-team-team-alpha-super',
|
|
'team-alpha-super.user',
|
|
]),
|
|
} as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const result = await (svc as unknown as any).resolveLaunchExpectedMembers('t', '{}');
|
|
expect(result.source).toBe('inboxes');
|
|
expect(result.members.map((m: { name: string }) => m.name)).toEqual(['dev']);
|
|
});
|
|
|
|
it('inbox fallback keeps suffixed name if base is absent', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => ['alice-2']) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const result = await (svc as unknown as any).resolveLaunchExpectedMembers('t', '{}');
|
|
expect(result.source).toBe('inboxes');
|
|
expect(result.members.map((m: { name: string }) => m.name)).toEqual(['alice-2']);
|
|
});
|
|
|
|
it('inbox fallback merges provider/model overrides from config for multimodel reconnect', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => ['bob']) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 't',
|
|
members: [{ name: 'bob', role: 'reviewer', provider: 'codex', model: 'gpt-5.4' }],
|
|
});
|
|
|
|
const result = await (svc as unknown as any).resolveLaunchExpectedMembers('t', configRaw);
|
|
expect(result.source).toBe('inboxes');
|
|
expect(result.members).toEqual([
|
|
{ name: 'bob', role: 'reviewer', workflow: undefined, providerId: 'codex', model: 'gpt-5.4' },
|
|
]);
|
|
expect(result.warning).toContain('best-effort');
|
|
});
|
|
|
|
it('members.meta.json fallback never returns reserved names (user/team-lead)', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => []) } as never,
|
|
{
|
|
getMembers: vi.fn(async () => [
|
|
{ name: 'user', agentType: 'general-purpose' },
|
|
{ name: 'team-lead', agentType: 'team-lead' },
|
|
{ name: 'Alice', role: 'dev', agentType: 'general-purpose' },
|
|
]),
|
|
} as never,
|
|
{} as never
|
|
);
|
|
|
|
const result = await (svc as unknown as any).resolveLaunchExpectedMembers('t', '{}');
|
|
expect(result.source).toBe('members-meta');
|
|
expect(result.members.map((m: { name: string }) => m.name)).toEqual(['Alice']);
|
|
});
|
|
|
|
it('config fallback never returns reserved names (user/team-lead)', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => []) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 't',
|
|
members: [{ name: 'team-lead', agentType: 'team-lead' }, { name: 'user' }, { name: 'bob' }],
|
|
});
|
|
|
|
const result = await (svc as unknown as any).resolveLaunchExpectedMembers('t', configRaw);
|
|
expect(result.source).toBe('config-fallback');
|
|
expect(result.members.map((m: { name: string }) => m.name)).toEqual(['bob']);
|
|
});
|
|
|
|
it('marks pure Claude/Codex legacy config without members.meta.json as repairable', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => []) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 'legacy-pure',
|
|
members: [
|
|
{ name: 'alice', role: 'reviewer', provider: 'anthropic', model: 'claude-opus-4-6' },
|
|
{ name: 'tom', role: 'developer', provider: 'codex', model: 'gpt-5.4' },
|
|
],
|
|
});
|
|
|
|
const report = await (svc as unknown as any).probeLaunchCompatibility(
|
|
'legacy-pure',
|
|
configRaw,
|
|
'anthropic'
|
|
);
|
|
|
|
expect(report).toMatchObject({
|
|
level: 'repairable',
|
|
rosterSource: 'config',
|
|
repairAction: 'materialize-members-meta',
|
|
});
|
|
expect(report.members.map((member: { name: string }) => member.name)).toEqual([
|
|
'alice',
|
|
'tom',
|
|
]);
|
|
});
|
|
|
|
it('rejects inbox fallback when OpenCode metadata is incomplete without members.meta truth', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => ['tom']) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 't',
|
|
members: [{ name: 'tom', role: 'developer', provider: 'opencode' }],
|
|
});
|
|
|
|
await expect(
|
|
(svc as unknown as any).resolveLaunchExpectedMembers('t', configRaw, 'codex')
|
|
).rejects.toThrow(getMixedLaunchFallbackRecoveryError());
|
|
});
|
|
|
|
it('marks complete mixed OpenCode config fallback as repairable', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => []) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 't',
|
|
members: [{ name: 'tom', role: 'developer', provider: 'opencode', model: 'minimax-m2.5-free' }],
|
|
});
|
|
|
|
const report = await (svc as unknown as any).probeLaunchCompatibility('t', configRaw, 'codex');
|
|
expect(report).toMatchObject({
|
|
level: 'repairable',
|
|
rosterSource: 'config',
|
|
repairAction: 'materialize-members-meta',
|
|
});
|
|
expect(report.members).toMatchObject([
|
|
{ name: 'tom', role: 'developer', providerId: 'opencode', model: 'minimax-m2.5-free' },
|
|
]);
|
|
});
|
|
|
|
it('prefers complete mixed OpenCode config over inbox names when members.meta is missing', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => ['tom']) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 't',
|
|
members: [{ name: 'tom', role: 'developer', provider: 'opencode', model: 'minimax-m2.5-free' }],
|
|
});
|
|
|
|
const report = await (svc as unknown as any).probeLaunchCompatibility('t', configRaw, 'codex');
|
|
expect(report).toMatchObject({
|
|
level: 'repairable',
|
|
rosterSource: 'config',
|
|
repairAction: 'materialize-members-meta',
|
|
});
|
|
});
|
|
|
|
it('rejects mixed OpenCode config fallback when the side lane is missing an explicit model', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => []) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 't',
|
|
members: [{ name: 'tom', role: 'developer', provider: 'opencode' }],
|
|
});
|
|
|
|
await expect(
|
|
(svc as unknown as any).resolveLaunchExpectedMembers('t', configRaw, 'codex')
|
|
).rejects.toThrow(getMixedLaunchFallbackRecoveryError());
|
|
});
|
|
|
|
it('rejects config fallback when an OpenCode-looking model is missing an explicit provider', async () => {
|
|
const svc = new TeamProvisioningService(
|
|
{} as never,
|
|
{ listInboxNames: vi.fn(async () => []) } as never,
|
|
{ getMembers: vi.fn(async () => []) } as never,
|
|
{} as never
|
|
);
|
|
|
|
const configRaw = JSON.stringify({
|
|
name: 't',
|
|
members: [{ name: 'tom', role: 'developer', model: 'opencode/minimax-m2.5-free' }],
|
|
});
|
|
|
|
await expect(
|
|
(svc as unknown as any).resolveLaunchExpectedMembers('t', configRaw, 'codex')
|
|
).rejects.toThrow(getMixedLaunchFallbackRecoveryError());
|
|
});
|
|
});
|