test(cli): validate mentions handle

This commit is contained in:
Peter Steinberger
2025-12-26 12:34:16 +01:00
parent 3df7969b16
commit 6669ba9912
4 changed files with 49 additions and 5 deletions
+6 -1
View File
@@ -27,6 +27,7 @@ bird replies 1234567890123456789
# Search + mentions
bird search "from:steipete" -n 5
bird mentions -n 5
bird mentions --user @steipete -n 5
```
## Commands
@@ -38,7 +39,7 @@ bird mentions -n 5
- `bird replies <tweet-id-or-url> [--json]` — list replies to a tweet.
- `bird thread <tweet-id-or-url> [--json]` — show the full conversation thread.
- `bird search "<query>" [-n count] [--json]` — search for tweets matching a query.
- `bird mentions [-n count] [--json]` — find tweets mentioning @clawdbot.
- `bird mentions [-n count] [--user @handle] [--json]` — find tweets mentioning a user (defaults to the authenticated user).
- `bird whoami` — print which Twitter account your cookies belong to.
- `bird check` — show which credentials are available and where they were sourced from.
@@ -92,6 +93,10 @@ Environment shortcuts:
- `--json` prints raw tweet objects for read/replies/thread/search/mentions.
- `read` returns full text for Notes and Articles when present.
## Version
`bird --version` prints `package.json` version plus current git sha when available, e.g. `0.1.1 (3df7969b)`.
## Media uploads (Sweetistics only)
- Attach media with `--media` (repeatable) and optional `--alt` per item.
+8 -3
View File
@@ -19,7 +19,7 @@ import { resolveCliInvocation } from './lib/cli-args.js';
import { resolveCredentials } from './lib/cookies.js';
import { type EngineMode, resolveEngineMode, shouldUseSweetistics } from './lib/engine.js';
import { extractTweetId } from './lib/extract-tweet-id.js';
import { normalizeHandle } from './lib/normalize-handle.js';
import { mentionsQueryFromUserOption, normalizeHandle } from './lib/normalize-handle.js';
import { SweetisticsClient } from './lib/sweetistics-client.js';
import { type TweetData, TwitterClient } from './lib/twitter-client.js';
import { getCliVersion } from './lib/version.js';
@@ -817,8 +817,13 @@ program
const engine = resolveEngineMode(opts.engine);
const useSweetistics = shouldUseSweetistics(engine, Boolean(sweetistics.apiKey));
const explicitHandle = normalizeHandle(cmdOpts.user);
let query: string | null = explicitHandle ? `@${explicitHandle}` : null;
const fromUserOpt = mentionsQueryFromUserOption(cmdOpts.user);
if (fromUserOpt.error) {
console.error(`❌ ${fromUserOpt.error}`);
process.exit(2);
}
let query: string | null = fromUserOpt.query;
if (useSweetistics) {
if (!sweetistics.apiKey) {
+17
View File
@@ -11,3 +11,20 @@ export function normalizeHandle(input?: string | null): string | null {
return handle;
}
export function mentionsQueryFromUserOption(userOption?: string | null): {
query: string | null;
error: string | null;
} {
if (typeof userOption === 'undefined') return { query: null, error: null };
const handle = normalizeHandle(userOption);
if (!handle) {
return {
query: null,
error: 'Invalid --user handle. Expected something like @steipete (letters, digits, underscore; max 15).',
};
}
return { query: `@${handle}`, error: null };
}
+18 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { normalizeHandle } from '../src/lib/normalize-handle.js';
import { mentionsQueryFromUserOption, normalizeHandle } from '../src/lib/normalize-handle.js';
describe('normalizeHandle', () => {
it('accepts bare handle', () => {
@@ -22,3 +22,20 @@ describe('normalizeHandle', () => {
expect(normalizeHandle('a'.repeat(16))).toBeNull();
});
});
describe('mentionsQueryFromUserOption', () => {
it('returns null query when option omitted', () => {
expect(mentionsQueryFromUserOption(undefined)).toEqual({ query: null, error: null });
});
it('returns normalized @query for valid handle', () => {
expect(mentionsQueryFromUserOption('@steipete')).toEqual({ query: '@steipete', error: null });
expect(mentionsQueryFromUserOption(' steipete ')).toEqual({ query: '@steipete', error: null });
});
it('returns error for invalid handle', () => {
const result = mentionsQueryFromUserOption('@stei-pete');
expect(result.query).toBeNull();
expect(result.error).toMatch(/Invalid --user handle/);
});
});