Experimental Bluesky client for agents
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210import { afterEach, describe, expect, test } from 'bun:test';import { mkdtempSync, rmSync } from 'node:fs';import { tmpdir } from 'node:os';import { dirname, join } from 'node:path';import type { Target, ThreadSnapshot } from '../src/model.ts';import { ReadingStore } from '../src/store.ts';
const temporaryDirectories: string[] = [];const cliPath = join(import.meta.dir, '..', 'src', 'cli.ts');const projectRoot = dirname(dirname(cliPath));
interface CliResult { exitCode: number; stdout: string; stderr: string;}
function temporaryStorePath(): string { const directory = mkdtempSync(join(tmpdir(), 'perch-state-cli-')); temporaryDirectories.push(directory); return join(directory, 'reader.sqlite');}
function cliSnapshot(marker: string, target?: Target): ThreadSnapshot { const uri = `at://did:plc:cli/app.bsky.feed.post/${marker}`; return { kind: 'thread', requested: uri, focusUri: uri, fetchedAt: '2026-09-05T15:00:00.000Z', source: { service: 'https://offline.invalid', mode: 'public' }, nodes: [{ uri, availability: 'available', author: { did: 'did:plc:cli', handle: 'cli.test', displayName: 'CLI Fixture' }, text: `CLI snapshot ${marker}`, createdAt: '2026-09-05T14:59:00.000Z', replies: [], repliesFetched: true, facets: target ? [{ byteStart: 0, byteEnd: 3, text: 'CLI', targets: [target], unsupported: [] }] : [], media: [], quotes: [], cards: [], labels: [], unsupported: [], counts: {}, }], ancestors: [], replyOrder: 'source', limits: { depth: 3, parentHeight: 40, maxNodes: 60 }, locallyTruncated: false, warnings: [], };}
async function runPerch(databasePath: string, arguments_: string[]): Promise<CliResult> { const child = Bun.spawn([process.execPath, cliPath, ...arguments_], { cwd: projectRoot, env: { ...Bun.env, PERCH_DB: databasePath, HTTP_PROXY: 'http://127.0.0.1:1', HTTPS_PROXY: 'http://127.0.0.1:1', NO_PROXY: '', }, stdout: 'pipe', stderr: 'pipe', }); const [exitCode, stdout, stderr] = await Promise.all([ child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), ]); return { exitCode, stdout, stderr };}
afterEach(() => { for (const directory of temporaryDirectories.splice(0)) rmSync(directory, { recursive: true, force: true });});
describe('local CLI state commands', () => { test('show, peek, and sessions return persisted state without moving either session', async () => { const path = temporaryStorePath(); const store = new ReadingStore(path); const linkTarget: Target = { kind: 'link', value: 'https://example.invalid/article?exact=yes', label: 'Exact article target', ownerUri: 'at://did:plc:cli/app.bsky.feed.post/local', index: 0, }; const view = store.commit('main', cliSnapshot('local', linkTarget), store.position('main').revision); store.fork('main', 'branch'); const reference = Object.entries(view.refs).find(([, target]) => target.value === linkTarget.value); expect(reference).toBeDefined(); const before = store.sessions(); store.close();
const shown = await runPerch(path, ['show', '--session', 'main', '--json']); expect(shown.exitCode).toBe(0); expect(shown.stderr).toBe(''); expect(JSON.parse(shown.stdout)).toEqual(view);
const peeked = await runPerch(path, ['peek', reference![0]]); expect(peeked.exitCode).toBe(0); expect(peeked.stderr).toBe(''); expect(JSON.parse(peeked.stdout)).toEqual(linkTarget);
const listed = await runPerch(path, ['sessions', '--json']); expect(listed.exitCode).toBe(0); expect(listed.stderr).toBe(''); expect(JSON.parse(listed.stdout)).toEqual(before);
const reopened = new ReadingStore(path); expect(reopened.sessions()).toEqual(before); expect(reopened.position('main').revision).toBe(1); expect(reopened.position('branch').revision).toBe(0); reopened.close(); });
test('opens a link reference as an offline inspection view and keeps the old reference valid', async () => { const path = temporaryStorePath(); const store = new ReadingStore(path); const linkTarget: Target = { kind: 'link', value: 'https://example.invalid/offline-target', label: 'Offline target', ownerUri: 'at://did:plc:cli/app.bsky.feed.post/inspect', index: 0, }; const original = store.commit('main', cliSnapshot('inspect', linkTarget), store.position('main').revision); const reference = Object.entries(original.refs).find(([, target]) => target.value === linkTarget.value); expect(reference).toBeDefined(); store.close();
const opened = await runPerch(path, ['open', reference![0], '--json']); expect(opened.exitCode).toBe(0); expect(opened.stderr).toBe(''); const inspection = JSON.parse(opened.stdout); expect(inspection.id).toBe('v2'); expect(inspection.snapshot.kind).toBe('target'); expect(inspection.snapshot.source).toEqual(original.snapshot.source); expect(inspection.snapshot.target).toEqual(linkTarget); expect(inspection.snapshot.note).toContain('no article content was fetched');
const reopened = new ReadingStore(path); expect(reopened.position('main').session).toEqual({ name: 'main', viewId: 'v2', history: [original.id] }); expect(reopened.resolveRef(reference![0])).toEqual({ view: original, ref: reference![0], target: linkTarget }); expect(reopened.back('main', reopened.position('main').revision)).toEqual(original); reopened.close(); });
test('unknown references and invalid command input fail without changing position', async () => { const path = temporaryStorePath(); const store = new ReadingStore(path); const first = store.commit('main', cliSnapshot('first'), store.position('main').revision); const second = store.commit('main', cliSnapshot('second'), store.position('main').revision); const baseline = store.position('main'); store.close();
const unknownPeek = await runPerch(path, ['peek', 'v999-p1']); expect(unknownPeek.exitCode).toBe(1); expect(unknownPeek.stdout).toBe(''); expect(unknownPeek.stderr).toContain('perch: Unknown reference: v999-p1');
const unknownOpen = await runPerch(path, ['open', 'v999-p1', '--json']); expect(unknownOpen.exitCode).toBe(1); expect(unknownOpen.stdout).toBe(''); expect(unknownOpen.stderr).toContain('perch: Unknown reference: v999-p1');
const invalidShow = await runPerch(path, ['show', 'unexpected']); expect(invalidShow.exitCode).toBe(1); expect(invalidShow.stderr).toContain('Usage: perch show [options]');
const reopened = new ReadingStore(path); expect(reopened.position('main')).toEqual(baseline); expect(reopened.position('main').session).toEqual({ name: 'main', viewId: second.id, history: [first.id] }); expect(reopened.getView('v3')).toBeNull(); reopened.close(); });
test('malformed flags report the offending option and never begin a refresh', async () => { const path = temporaryStorePath(); const store = new ReadingStore(path); const view = store.commit('main', cliSnapshot('flags'), store.position('main').revision); const baseline = store.position('main'); store.close();
const cases: Array<{ arguments_: string[]; message: string }> = [ { arguments_: ['show', '--unknown'], message: 'Unknown option: --unknown' }, { arguments_: ['show', '--session'], message: 'Option --session requires a value.' }, { arguments_: ['refresh', '--depth', 'many'], message: '--depth must be an integer from 0 to 100.' }, { arguments_: ['refresh', '--max-nodes', '0'], message: '--max-nodes must be an integer from 1 to 200.' }, { arguments_: ['fork'], message: 'Usage: perch fork <name> [options]' }, ]; for (const item of cases) { const result = await runPerch(path, item.arguments_); expect(result.exitCode).toBe(1); expect(result.stdout).toBe(''); expect(result.stderr).toContain(`perch: ${item.message}`); }
const reopened = new ReadingStore(path); expect(reopened.position('main')).toEqual(baseline); expect(reopened.current('main')).toEqual(view); expect(reopened.getView('v2')).toBeNull(); reopened.close(); });});