Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 6 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
],
"dependencies": {
"@doist/cli-core": "0.20.0",
"@doist/twist-sdk": "2.7.0",
"@doist/twist-sdk": "2.8.1",
"@pnpm/tabtab": "0.5.4",
"chalk": "5.6.2",
"commander": "14.0.3",
Expand Down
3 changes: 2 additions & 1 deletion skills/twist-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ tw search "query" --all # Fetch all result pages
tw user # Show current user info
tw user --json # JSON output
tw user --json --full # Include all fields in JSON output
tw users # List workspace users
tw users # List active workspace users
tw users --search <text> # Filter by name/email
tw users --include-removed # Include users removed from the workspace
tw channels # List active joined workspace channels (alias of: tw channel list)
tw channels --state all # Include archived joined channels too
tw channels --scope discoverable # Active public channels you can see but have not joined
Expand Down
69 changes: 69 additions & 0 deletions src/commands/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,72 @@ describe('user --json', () => {
consoleSpy.mockRestore()
})
})

describe('tw users --include-removed', () => {
const active = {
id: 1,
name: 'Active',
email: 'a@x',
userType: 'USER',
bot: false,
removed: false,
}
const removed = {
id: 2,
name: 'Ghost',
email: 'ghost@x',
userType: 'GUEST',
bot: false,
removed: true,
}

beforeEach(() => {
vi.clearAllMocks()
apiMocks.getCurrentWorkspaceId.mockResolvedValue(1)
})

it('passes includeRemoved: undefined by default so the SDK applies its default filter', async () => {
apiMocks.getWorkspaceUsers.mockResolvedValueOnce([active])
const program = createProgram()
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})

await program.parseAsync(['node', 'tw', 'users'])

expect(apiMocks.getWorkspaceUsers).toHaveBeenCalledWith(1, { includeRemoved: undefined })
expect(consoleSpy.mock.calls.flat().join('\n')).not.toMatch(/\[removed\]/)

consoleSpy.mockRestore()
})

it('passes includeRemoved: true and annotates removed users in text output', async () => {
Comment thread
lmjabreu marked this conversation as resolved.
apiMocks.getWorkspaceUsers.mockResolvedValueOnce([active, removed])
const program = createProgram()
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})

await program.parseAsync(['node', 'tw', 'users', '--include-removed'])

expect(apiMocks.getWorkspaceUsers).toHaveBeenCalledWith(1, { includeRemoved: true })
const lines = consoleSpy.mock.calls.flat().join('\n')
expect(lines).toMatch(/id:2.*Ghost.*\[removed\]/)
expect(lines).not.toMatch(/id:1.*Active.*\[removed\]/)

consoleSpy.mockRestore()
})

it('surfaces removed in curated --json output without --full', async () => {
apiMocks.getWorkspaceUsers.mockResolvedValueOnce([active, removed])
const program = createProgram()
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})

await program.parseAsync(['node', 'tw', 'users', '--include-removed', '--json'])

const parsed = JSON.parse(consoleSpy.mock.calls[0][0])
expect(parsed).toHaveLength(2)
expect(parsed[0]).toMatchObject({ id: 1, removed: false })
expect(parsed[1]).toMatchObject({ id: 2, removed: true })
// Curated, not --full: shortName must not leak in.
expect(parsed[0]).not.toHaveProperty('shortName')

consoleSpy.mockRestore()
})
})
Loading
Loading