import { describe, expect, it, vi } from 'vitest'; import { getRegistry } from '@jackwener/opencli/registry'; import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors'; import { buildListAddMemberRow } from './list-add.js'; describe('twitter list-add registration', () => { it('registers the list-add command with the expected shape', () => { const cmd = getRegistry().get('twitter/list-add'); expect(cmd?.func).toBeTypeOf('function'); expect(cmd?.columns).toEqual(['listId', 'username', 'userId', 'status', 'message']); const listIdArg = cmd?.args?.find((a) => a.name === 'listId'); expect(listIdArg).toBeTruthy(); expect(listIdArg?.required).toBe(true); expect(listIdArg?.positional).toBe(true); }); it('keeps the x.com root navigation before pre-target GraphQL calls', async () => { const cmd = getRegistry().get('twitter/list-add'); const page = { goto: vi.fn().mockResolvedValue(undefined), wait: vi.fn().mockResolvedValue(undefined), getCookies: vi.fn().mockResolvedValue([{ name: 'ct0', value: 'token' }]), evaluate: vi.fn() .mockResolvedValueOnce(null) // UserByScreenName queryId fallback .mockResolvedValueOnce('user-1') .mockResolvedValueOnce(null) // ListsManagement queryId fallback .mockResolvedValueOnce({}), }; await expect(cmd.func(page, { listId: '123', username: 'alice' })) .rejects .toThrow(/List 123 not found/); expect(page.goto).toHaveBeenCalledWith('https://x.com'); expect(page.goto).toHaveBeenCalledTimes(1); expect(page.wait).toHaveBeenCalledWith(3); expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://x.com' }); }); it('rejects invalid user input before navigation', async () => { const cmd = getRegistry().get('twitter/list-add'); const page = { goto: vi.fn(), wait: vi.fn(), getCookies: vi.fn(), evaluate: vi.fn(), }; await expect(cmd.func(page, { listId: 'abc', username: 'alice' })).rejects.toBeInstanceOf(ArgumentError); await expect(cmd.func(page, { listId: '123', username: '' })).rejects.toBeInstanceOf(ArgumentError); expect(page.goto).not.toHaveBeenCalled(); }); it('builds success rows when member_count increases despite non-fatal decode errors', () => { const row = buildListAddMemberRow({ addResult: { httpOk: true, status: 200, mc: 11, isMember: true, errors: [{ path: ['data', 'list', 'default_banner_media_results'], message: 'decode failed' }], }, memberCountBefore: 10, listId: '123', username: 'alice', userId: '42', }); expect(row).toMatchObject({ listId: '123', username: 'alice', userId: '42', status: 'success', }); expect(row.message).toContain('member_count 10 → 11'); }); it('treats unchanged member_count as noop only when membership is confirmed', () => { const row = buildListAddMemberRow({ addResult: { httpOk: true, status: 200, mc: 10, isMember: true, errors: null }, memberCountBefore: 10, listId: '123', username: 'alice', userId: '42', }); expect(row.status).toBe('noop'); expect(row.message).toBe('@alice is already a member of list 123'); }); it('fails typed when member_count is unchanged but membership is not confirmed', () => { expect(() => buildListAddMemberRow({ addResult: { httpOk: true, status: 200, mc: 10, isMember: false, errors: null }, memberCountBefore: 10, listId: '123', username: 'alice', userId: '42', })).toThrow(/membership was not confirmed/); }); it('fails typed when member_count decreases unexpectedly', () => { expect(() => buildListAddMemberRow({ addResult: { httpOk: true, status: 200, mc: 9, isMember: true, errors: null }, memberCountBefore: 10, listId: '123', username: 'alice', userId: '42', })).toThrow(/decreased unexpectedly/); }); it('fails typed when GraphQL response has no usable member_count', () => { expect(() => buildListAddMemberRow({ addResult: { httpOk: true, status: 200, mc: undefined, isMember: null, errors: [{ message: 'List is unavailable', path: ['data', 'list'] }], }, memberCountBefore: 10, listId: '123', username: 'alice', userId: '42', })).toThrow(/List is unavailable/); expect(() => buildListAddMemberRow({ addResult: { httpOk: true, status: 200, mc: null, isMember: null, errors: { message: 'not an array' } }, memberCountBefore: 10, listId: '123', username: 'alice', userId: '42', })).toThrow(/no member_count/); }); });