test: enforce coverage thresholds
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { createDecipheriv, pbkdf2Sync } from 'node:crypto';
|
||||
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync, readFileSync } from 'node:fs';
|
||||
import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
|
||||
+44
-1
@@ -1,7 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { labelPrefix, resolveOutputConfigFromArgv, statusPrefix } from '../src/lib/output.js';
|
||||
import {
|
||||
formatStatsLine,
|
||||
labelPrefix,
|
||||
resolveOutputConfigFromArgv,
|
||||
resolveOutputConfigFromCommander,
|
||||
statusPrefix,
|
||||
} from '../src/lib/output.js';
|
||||
|
||||
describe('output', () => {
|
||||
it('defaults to emoji + color on TTY', () => {
|
||||
const cfg = resolveOutputConfigFromArgv([], {}, true);
|
||||
expect(cfg).toEqual({ plain: false, emoji: true, color: true });
|
||||
});
|
||||
|
||||
it('plain disables emoji + color', () => {
|
||||
const cfg = resolveOutputConfigFromArgv(['--plain'], {}, true);
|
||||
expect(cfg).toEqual({ plain: true, emoji: false, color: false });
|
||||
@@ -14,9 +25,41 @@ describe('output', () => {
|
||||
expect(cfg.color).toBe(false);
|
||||
});
|
||||
|
||||
it('TERM=dumb disables colors by default', () => {
|
||||
const cfg = resolveOutputConfigFromArgv([], { TERM: 'dumb' }, true);
|
||||
expect(cfg.color).toBe(false);
|
||||
});
|
||||
|
||||
it('--no-color disables colors', () => {
|
||||
const cfg = resolveOutputConfigFromArgv(['--no-color'], {}, true);
|
||||
expect(cfg).toEqual({ plain: false, emoji: true, color: false });
|
||||
});
|
||||
|
||||
it('--no-emoji switches to text prefixes', () => {
|
||||
const cfg = resolveOutputConfigFromArgv(['--no-emoji'], {}, true);
|
||||
expect(cfg.emoji).toBe(false);
|
||||
expect(statusPrefix('warn', cfg)).toBe('Warning: ');
|
||||
});
|
||||
|
||||
it('commander opts override defaults', () => {
|
||||
const cfg = resolveOutputConfigFromCommander({ emoji: false, color: false }, {}, true);
|
||||
expect(cfg).toEqual({ plain: false, emoji: false, color: false });
|
||||
expect(statusPrefix('info', cfg)).toBe('Info: ');
|
||||
expect(labelPrefix('date', cfg)).toBe('Date: ');
|
||||
});
|
||||
|
||||
it('commander plain wins over emoji/color', () => {
|
||||
const cfg = resolveOutputConfigFromCommander({ plain: true, emoji: true, color: true }, {}, true);
|
||||
expect(cfg).toEqual({ plain: true, emoji: false, color: false });
|
||||
});
|
||||
|
||||
it('formats stats line for all modes', () => {
|
||||
const stats = { likeCount: null, retweetCount: undefined, replyCount: 2 };
|
||||
|
||||
expect(formatStatsLine(stats, { plain: true, emoji: false, color: false })).toBe(
|
||||
'likes: 0 retweets: 0 replies: 2',
|
||||
);
|
||||
expect(formatStatsLine(stats, { plain: false, emoji: false, color: false })).toBe('Likes 0 Retweets 0 Replies 2');
|
||||
expect(formatStatsLine(stats, { plain: false, emoji: true, color: false })).toBe('❤️ 0 🔁 0 💬 2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -753,6 +753,54 @@ describe('TwitterClient', () => {
|
||||
expect(result.user?.id).toBe('999');
|
||||
expect(mockFetch).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('skips an endpoint when JSON parsing fails', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => {
|
||||
throw new Error('bad json');
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
user_id: '12345',
|
||||
screen_name: 'tester',
|
||||
name: 'Test User',
|
||||
}),
|
||||
});
|
||||
|
||||
const client = new TwitterClient({ cookies: validCookies });
|
||||
const result = await client.getCurrentUser();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.user).toEqual({ id: '12345', username: 'tester', name: 'Test User' });
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('continues on fetch errors and still succeeds via HTML fallback', async () => {
|
||||
mockFetch
|
||||
.mockRejectedValueOnce(new Error('boom'))
|
||||
.mockResolvedValueOnce({ ok: false, status: 404, text: async () => 'not found' })
|
||||
.mockResolvedValueOnce({ ok: false, status: 404, text: async () => 'not found' })
|
||||
.mockResolvedValueOnce({ ok: false, status: 404, text: async () => 'not found' })
|
||||
.mockRejectedValueOnce(new Error('settings boom'))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => '<html>"screen_name":"fallback","user_id":"999"</html>',
|
||||
});
|
||||
|
||||
const client = new TwitterClient({ cookies: validCookies });
|
||||
const result = await client.getCurrentUser();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.user?.username).toBe('fallback');
|
||||
expect(result.user?.id).toBe('999');
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
@@ -832,6 +880,47 @@ describe('TwitterClient', () => {
|
||||
const parsed = JSON.parse(urlVars as string) as { rawQuery?: string };
|
||||
expect(parsed.rawQuery).toBe('needle');
|
||||
});
|
||||
|
||||
it('refreshes query IDs when all search endpoints 404', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' })
|
||||
.mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' })
|
||||
.mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' })
|
||||
.mockResolvedValueOnce({ ok: false, status: 404, text: async () => '' })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
data: {
|
||||
search_by_raw_query: {
|
||||
search_timeline: {
|
||||
timeline: {
|
||||
instructions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const client = new TwitterClient({ cookies: validCookies });
|
||||
const result = await client.search('hello', 5);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('returns an unknown error when no query IDs are available', async () => {
|
||||
const client = new TwitterClient({ cookies: validCookies });
|
||||
(client as unknown as { getSearchTimelineQueryIds: () => Promise<string[]> }).getSearchTimelineQueryIds =
|
||||
async () => [];
|
||||
|
||||
const result = await client.search('hello', 5);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Unknown error fetching search results');
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('conversation helpers', () => {
|
||||
|
||||
+108
-9
@@ -1,7 +1,18 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { formatVersionLine, resolveGitSha, resolvePackageVersion } from '../src/lib/version.js';
|
||||
import { FALLBACK_VERSION, formatVersionLine, resolveGitSha, resolvePackageVersion } from '../src/lib/version.js';
|
||||
|
||||
function withTempDir<T>(fn: (dir: string) => T): T {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bird-version-test-'));
|
||||
try {
|
||||
return fn(dir);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe('getCliVersion', () => {
|
||||
afterEach(() => {
|
||||
@@ -10,7 +21,7 @@ describe('getCliVersion', () => {
|
||||
});
|
||||
|
||||
it('reads package.json version when available', () => {
|
||||
const raw = readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8');
|
||||
const raw = fs.readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8');
|
||||
const pkg = JSON.parse(raw) as { version?: unknown };
|
||||
expect(resolvePackageVersion()).toBe(pkg.version);
|
||||
});
|
||||
@@ -21,10 +32,98 @@ describe('getCliVersion', () => {
|
||||
expect(formatVersionLine('not a url')).toBe('9.9.9 (abcdef12)');
|
||||
});
|
||||
|
||||
it('adds git sha when available', () => {
|
||||
const version = resolvePackageVersion();
|
||||
const sha = resolveGitSha();
|
||||
expect(sha).toMatch(/^[0-9a-f]{8}$/);
|
||||
expect(formatVersionLine()).toBe(`${version} (${sha})`);
|
||||
it('resolves package.json version from importMetaUrl root', () => {
|
||||
withTempDir((dir) => {
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ version: '1.2.3' }));
|
||||
const entry = path.join(dir, 'src', 'entry.ts');
|
||||
fs.mkdirSync(path.dirname(entry), { recursive: true });
|
||||
fs.writeFileSync(entry, '// noop');
|
||||
|
||||
expect(resolvePackageVersion(pathToFileURL(entry).href)).toBe('1.2.3');
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to VERSION file when package.json missing', () => {
|
||||
withTempDir((dir) => {
|
||||
fs.writeFileSync(path.join(dir, 'VERSION'), '2.0.0\n');
|
||||
const entry = path.join(dir, 'entry.ts');
|
||||
fs.writeFileSync(entry, '// noop');
|
||||
|
||||
expect(resolvePackageVersion(pathToFileURL(entry).href)).toBe('2.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to unknown when no version files exist', () => {
|
||||
withTempDir((dir) => {
|
||||
const entry = path.join(dir, 'a', 'b', 'c', 'entry.ts');
|
||||
fs.mkdirSync(path.dirname(entry), { recursive: true });
|
||||
fs.writeFileSync(entry, '// noop');
|
||||
|
||||
expect(resolvePackageVersion(pathToFileURL(entry).href)).toBe(FALLBACK_VERSION);
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves git sha from .git directory HEAD', () => {
|
||||
withTempDir((dir) => {
|
||||
const gitDir = path.join(dir, '.git');
|
||||
fs.mkdirSync(gitDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(gitDir, 'HEAD'), '0123456789abcdef0123456789abcdef01234567\n');
|
||||
|
||||
const entry = path.join(dir, 'entry.ts');
|
||||
fs.writeFileSync(entry, '// noop');
|
||||
|
||||
expect(resolveGitSha(pathToFileURL(entry).href)).toBe('01234567');
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves git sha from ref file and packed-refs', () => {
|
||||
withTempDir((dir) => {
|
||||
const gitDir = path.join(dir, '.git');
|
||||
fs.mkdirSync(path.join(gitDir, 'refs', 'heads'), { recursive: true });
|
||||
fs.writeFileSync(path.join(gitDir, 'HEAD'), 'ref: refs/heads/main\n');
|
||||
fs.writeFileSync(path.join(gitDir, 'refs', 'heads', 'main'), 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n');
|
||||
|
||||
const entry = path.join(dir, 'entry.ts');
|
||||
fs.writeFileSync(entry, '// noop');
|
||||
|
||||
expect(resolveGitSha(pathToFileURL(entry).href)).toBe('aaaaaaaa');
|
||||
|
||||
fs.rmSync(path.join(gitDir, 'refs', 'heads', 'main'), { force: true });
|
||||
fs.writeFileSync(
|
||||
path.join(gitDir, 'packed-refs'),
|
||||
['# pack-refs with: peeled fully-peeled', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb refs/heads/main', ''].join(
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
expect(resolveGitSha(pathToFileURL(entry).href)).toBe('bbbbbbbb');
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves git sha from .git file gitdir pointer', () => {
|
||||
withTempDir((dir) => {
|
||||
const realGitDir = path.join(dir, '.realgit');
|
||||
fs.mkdirSync(realGitDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(realGitDir, 'HEAD'), 'cccccccccccccccccccccccccccccccccccccccc\n');
|
||||
fs.writeFileSync(path.join(dir, '.git'), 'gitdir: .realgit\n');
|
||||
|
||||
const entry = path.join(dir, 'entry.ts');
|
||||
fs.writeFileSync(entry, '// noop');
|
||||
|
||||
expect(resolveGitSha(pathToFileURL(entry).href)).toBe('cccccccc');
|
||||
});
|
||||
});
|
||||
|
||||
it('formats version line with version + sha when both available', () => {
|
||||
withTempDir((dir) => {
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ version: '3.3.3' }));
|
||||
const gitDir = path.join(dir, '.git');
|
||||
fs.mkdirSync(gitDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(gitDir, 'HEAD'), 'dddddddddddddddddddddddddddddddddddddddd\n');
|
||||
|
||||
const entry = path.join(dir, 'entry.ts');
|
||||
fs.writeFileSync(entry, '// noop');
|
||||
|
||||
expect(formatVersionLine(pathToFileURL(entry).href)).toBe('3.3.3 (dddddddd)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,12 @@ export default defineConfig({
|
||||
reporter: ['text', 'html'],
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: ['src/index.ts'],
|
||||
thresholds: {
|
||||
statements: 70,
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user