diff --git a/e2e/clawdhub.e2e.test.ts b/e2e/clawdhub.e2e.test.ts new file mode 100644 index 00000000..5e47cc59 --- /dev/null +++ b/e2e/clawdhub.e2e.test.ts @@ -0,0 +1,122 @@ +/* @vitest-environment node */ + +import { spawnSync } from 'node:child_process' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { + ApiCliWhoamiResponseSchema, + ApiRoutes, + ApiSearchResponseSchema, + parseArk, +} from '@clawdhub/schema' +import { describe, expect, it } from 'vitest' +import { readGlobalConfig } from '../packages/clawdhub/src/config' + +function mustGetToken() { + const fromEnv = process.env.CLAWDHUB_E2E_TOKEN?.trim() + if (fromEnv) return fromEnv + return null +} + +async function makeTempConfig(registry: string, token: string | null) { + const dir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-')) + const path = join(dir, 'config.json') + await writeFile( + path, + `${JSON.stringify({ registry, token: token || undefined }, null, 2)}\n`, + 'utf8', + ) + return { dir, path } +} + +describe('clawdhub e2e', () => { + it('search endpoint returns a results array (schema parse)', async () => { + const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com' + const url = new URL(ApiRoutes.search, registry) + url.searchParams.set('q', 'gif') + url.searchParams.set('limit', '5') + + const response = await fetch(url.toString(), { headers: { Accept: 'application/json' } }) + expect(response.ok).toBe(true) + const json = (await response.json()) as unknown + const parsed = parseArk(ApiSearchResponseSchema, json, 'API response') + expect(Array.isArray(parsed.results)).toBe(true) + }) + + it('cli search does not error on multi-result responses', async () => { + const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com' + const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com' + const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null + + const cfg = await makeTempConfig(registry, token) + try { + const workdir = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-workdir-')) + const result = spawnSync( + 'bun', + [ + 'clawdhub', + 'search', + 'gif', + '--limit', + '5', + '--site', + site, + '--registry', + registry, + '--workdir', + workdir, + ], + { + cwd: process.cwd(), + env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path }, + encoding: 'utf8', + }, + ) + await rm(workdir, { recursive: true, force: true }) + + expect(result.status).toBe(0) + expect(result.stderr).not.toMatch(/API response:/) + } finally { + await rm(cfg.dir, { recursive: true, force: true }) + } + }) + + it('assumes a logged-in user (whoami succeeds)', async () => { + const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com' + const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com' + const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null + if (!token) { + throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login') + } + + const cfg = await makeTempConfig(registry, token) + try { + const whoamiUrl = new URL(ApiRoutes.cliWhoami, registry) + const whoamiRes = await fetch(whoamiUrl.toString(), { + headers: { Accept: 'application/json', Authorization: `Bearer ${token}` }, + }) + expect(whoamiRes.ok).toBe(true) + const whoami = parseArk( + ApiCliWhoamiResponseSchema, + (await whoamiRes.json()) as unknown, + 'Whoami', + ) + expect(whoami.user).toBeTruthy() + + const result = spawnSync( + 'bun', + ['clawdhub', 'whoami', '--site', site, '--registry', registry], + { + cwd: process.cwd(), + env: { ...process.env, CLAWDHUB_CONFIG_PATH: cfg.path }, + encoding: 'utf8', + }, + ) + expect(result.status).toBe(0) + expect(result.stderr).not.toMatch(/not logged in|unauthorized|error:/i) + } finally { + await rm(cfg.dir, { recursive: true, force: true }) + } + }) +}) diff --git a/package.json b/package.json index 55e3fea4..4a40ece3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "preview": "bun --bun vite preview", "test": "vitest run", "test:watch": "vitest", + "test:e2e": "vitest run -c vitest.e2e.config.ts", "coverage": "vitest run --coverage", "convex:deploy": "bunx convex deploy --typecheck=disable --yes", "lint": "bun run lint:biome && bun run lint:oxlint", diff --git a/packages/clawdhub/src/config.ts b/packages/clawdhub/src/config.ts index b33d4c0c..950bb18d 100644 --- a/packages/clawdhub/src/config.ts +++ b/packages/clawdhub/src/config.ts @@ -1,9 +1,11 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { homedir } from 'node:os' -import { dirname, join } from 'node:path' +import { dirname, join, resolve } from 'node:path' import { type GlobalConfig, GlobalConfigSchema, parseArk } from '@clawdhub/schema' export function getGlobalConfigPath() { + const override = process.env.CLAWDHUB_CONFIG_PATH?.trim() + if (override) return resolve(override) const home = homedir() if (process.platform === 'darwin') { return join(home, 'Library', 'Application Support', 'clawdhub', 'config.json') diff --git a/vitest.config.ts b/vitest.config.ts index 8d68480e..ad67e331 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,7 +5,14 @@ export default defineConfig({ environment: 'jsdom', globals: true, setupFiles: ['./vitest.setup.ts'], - exclude: ['**/node_modules/**', '**/dist/**', '**/coverage/**', '**/convex/_generated/**'], + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/coverage/**', + '**/convex/_generated/**', + 'e2e/**', + '**/*.e2e.test.ts', + ], coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], @@ -33,6 +40,7 @@ export default defineConfig({ 'packages/clawdhub/src/config.ts', 'packages/clawdhub/src/types.ts', 'packages/schema/dist/', + 'e2e/**', ], }, }, diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts new file mode 100644 index 00000000..4989ed0c --- /dev/null +++ b/vitest.e2e.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + include: ['e2e/**/*.e2e.test.ts'], + exclude: ['**/node_modules/**', '**/dist/**', '**/coverage/**', '**/convex/_generated/**'], + testTimeout: 60_000, + hookTimeout: 60_000, + }, +})