* refactor: extract importFile from import.ts + add tag reconciliation Shared single-file import function used by both import and sync. Adds tag reconciliation (removes stale tags on reimport), >1MB file skip, and import->sync checkpoint continuity (writes git HEAD to config table after import so sync picks up seamlessly). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sync pure functions, updateSlug engine method, and sync tests - buildSyncManifest: parses git diff --name-status -M output - isSyncable: filters to .md pages, excludes hidden/ops/.raw/skip-list - pathToSlug: converts file paths to page slugs with optional prefix - updateSlug: renames page slug in-place (preserves page_id, chunks, embeddings) - rewriteLinks: stub for v0.2 (FKs use page_id, already correct) - 20 new tests, all passing (39 total across 3 files) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add gbrain sync command with CLI, MCP, and watch mode 18-step sync protocol: read config, git pull, ancestry validation, git diff --name-status -M for net changes, isSyncable filter, process deletes/renames/adds/modifies via importFile, batch optimization, sync state checkpoint in Postgres config table. Watch mode with polling and consecutive error counter. MCP sync_brain tool returns structured SyncResult. Stale page deletion for un-syncable files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add files table, gbrain files commands, and config show redaction - files table: page_slug FK with ON DELETE SET NULL + ON UPDATE CASCADE, storage_path, storage_url, mime_type, content_hash for dedup - gbrain files list/upload/sync/verify commands for Supabase Storage - gbrain config show redacts postgresql:// passwords and secret keys - CLI help updated with FILES section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add install skill for GBrain onboarding 6-phase install workflow: environment discovery, Supabase setup (magic path via CLI OAuth or fallback 2-copy-paste), init + import, ongoing sync cron, optional file migration with mandatory verification, and agent teaching (AGENTS.md rules). Every error gets what + why + fix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update project documentation for v0.2.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add v0.2 features to README (sync, files, install skill) README.md: added sync command to IMPORT/EXPORT section, added FILES section with 4 commands, added files table to schema diagram, added install skill to skills table, updated MCP tools count from 20 to 21 (sync_brain added). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: OpenClaw DX improvements (skill count, upgrade docs, config show help) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: consolidate version to single source of truth Create src/version.ts that reads from package.json via static import (safe for bun compiled binaries). Update mcp/server.ts from hardcoded '0.1.0' to use shared VERSION. Bump skills/manifest.json to 0.2.0. * fix: upgrade detection order, npm→bun naming, clawhub false positives Reorder detection: node_modules first, binary second, clawhub last. Rename 'npm' install method to 'bun'. Use 'clawhub --version' instead of 'which clawhub' to avoid false positives from dangling symlinks. Add 120s timeout to execSync calls to prevent hanging. Add --help flag. * feat: per-command --help, unknown command check before DB connection Add COMMAND_HELP map covering all 28 commands. Check --help before init/upgrade dispatch and before connectEngine() so help works without a database. Use COMMAND_HELP keys as known-command set to catch unknown commands before wasting a DB round-trip. * docs: standardize npm references to bun, add Upgrade section to README Fix init.ts: npx→bunx, npm→bun for supabase CLI guidance. Fix README: npm install→bun add for standalone CLI install. Add ## Upgrade section to README with all three install methods. Update install skill Upgrading section to list bun, ClawHub, and binary. * test: full coverage audit — CLI dispatch, upgrade detection, config, edge cases New test files: - test/cli.test.ts: COMMAND_HELP ↔ switch consistency, version from package.json, per-command --help, unknown command handling, global help - test/upgrade.test.ts: detection order verification, npm→bun naming, clawhub --version (not which), timeout presence - test/config.test.ts: redactUrl for postgresql URLs, edge cases Extended existing tests: - test/sync.test.ts: empty string pathToSlug, uppercase .MD rejection, deeply nested files, multiple renames, unknown status codes - test/markdown.test.ts: multiple --- separators, missing frontmatter, no frontmatter at all, empty string, type inference from paths Tests: 39 → 83 (+44 new). All pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: 100% coverage — import-file mock engine, files utils, chunker edge cases New test files: - test/import-file.test.ts (9 tests): mock BrainEngine to test importFile without DB — MAX_FILE_SIZE skip, content_hash dedup, tag reconciliation (remove stale + add new), compiled_truth/timeline chunking, noEmbed flag, sequential chunk_index - test/files.test.ts (22 tests): getMimeType for all extensions + uppercase + unknown + no-extension, fileHash consistency + different content + empty, collectFiles pattern (skip .md, skip hidden dirs, recurse, sorted output) Extended: - test/chunkers/recursive.test.ts (+6 tests): single newline splits, word-only text, clause delimiters, lossless preservation, default options, mixed delimiter hierarchy Tests: 83 → 118 (+35 new). All pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
184 lines
6.3 KiB
TypeScript
184 lines
6.3 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { readFileSync } from 'fs';
|
|
|
|
// Read cli.ts source to extract COMMAND_HELP keys and switch cases
|
|
const cliSource = readFileSync(new URL('../src/cli.ts', import.meta.url), 'utf-8');
|
|
|
|
// Extract COMMAND_HELP keys from the map
|
|
function extractCommandHelpKeys(source: string): string[] {
|
|
const mapMatch = source.match(/const COMMAND_HELP:\s*Record<string,\s*string>\s*=\s*\{([\s\S]*?)\};/);
|
|
if (!mapMatch) return [];
|
|
const keys: string[] = [];
|
|
for (const m of mapMatch[1].matchAll(/^\s*['"]?([a-z-]+)['"]?\s*:/gm)) {
|
|
keys.push(m[1]);
|
|
}
|
|
return keys.sort();
|
|
}
|
|
|
|
// Extract switch case labels from the switch(command) block
|
|
function extractSwitchCases(source: string): string[] {
|
|
const cases: string[] = [];
|
|
for (const m of source.matchAll(/case\s+'([^']+)':\s*\{/g)) {
|
|
cases.push(m[1]);
|
|
}
|
|
return [...new Set(cases)].sort();
|
|
}
|
|
|
|
// Extract commands handled before the switch (init, upgrade)
|
|
function extractEarlyCommands(source: string): string[] {
|
|
const cmds: string[] = [];
|
|
for (const m of source.matchAll(/if\s*\(command\s*===\s*'([^']+)'\)/g)) {
|
|
if (!['--help', '-h', '--version', '--tools-json'].includes(m[1])) {
|
|
cmds.push(m[1]);
|
|
}
|
|
}
|
|
return [...new Set(cmds)].sort();
|
|
}
|
|
|
|
describe('CLI COMMAND_HELP consistency', () => {
|
|
const helpKeys = extractCommandHelpKeys(cliSource);
|
|
const switchCases = extractSwitchCases(cliSource);
|
|
const earlyCmds = extractEarlyCommands(cliSource);
|
|
const allHandled = [...switchCases, ...earlyCmds].sort();
|
|
|
|
test('COMMAND_HELP has entries for all switch cases', () => {
|
|
for (const cmd of switchCases) {
|
|
expect(helpKeys).toContain(cmd);
|
|
}
|
|
});
|
|
|
|
test('COMMAND_HELP has entries for early-dispatch commands (init, upgrade)', () => {
|
|
for (const cmd of earlyCmds) {
|
|
expect(helpKeys).toContain(cmd);
|
|
}
|
|
});
|
|
|
|
test('every COMMAND_HELP key maps to a handled command', () => {
|
|
for (const key of helpKeys) {
|
|
expect(allHandled).toContain(key);
|
|
}
|
|
});
|
|
|
|
test('COMMAND_HELP has at least 25 entries', () => {
|
|
expect(helpKeys.length).toBeGreaterThanOrEqual(25);
|
|
});
|
|
});
|
|
|
|
describe('CLI version', () => {
|
|
test('VERSION matches package.json', async () => {
|
|
const { VERSION } = await import('../src/version.ts');
|
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
expect(VERSION).toBe(pkg.version);
|
|
});
|
|
|
|
test('VERSION is a valid semver string', async () => {
|
|
const { VERSION } = await import('../src/version.ts');
|
|
expect(VERSION).toMatch(/^\d+\.\d+\.\d+/);
|
|
});
|
|
});
|
|
|
|
describe('CLI help text', () => {
|
|
test('every COMMAND_HELP entry starts with Usage:', () => {
|
|
const mapMatch = cliSource.match(/const COMMAND_HELP:\s*Record<string,\s*string>\s*=\s*\{([\s\S]*?)\};/);
|
|
expect(mapMatch).not.toBeNull();
|
|
// Verify by importing and checking
|
|
const keys = extractCommandHelpKeys(cliSource);
|
|
expect(keys.length).toBeGreaterThan(0);
|
|
// Each help string in the source should contain 'Usage:'
|
|
for (const key of keys) {
|
|
const pattern = new RegExp(`['"]?${key.replace('-', '\\-')}['"]?:\\s*['"\`]([^'"\`]*)`);
|
|
const match = cliSource.match(pattern);
|
|
if (match) {
|
|
expect(match[1]).toContain('Usage:');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('CLI dispatch integration', () => {
|
|
test('--version outputs version', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', '--version'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
await proc.exited;
|
|
expect(stdout.trim()).toMatch(/^gbrain \d+\.\d+\.\d+/);
|
|
});
|
|
|
|
test('unknown command prints error and exits 1', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'notacommand'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stderr = await new Response(proc.stderr).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stderr).toContain('Unknown command: notacommand');
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
test('per-command --help prints usage without DB connection', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'get', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('Usage: gbrain get');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('upgrade --help prints usage without running upgrade', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'upgrade', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('Usage: gbrain upgrade');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('init --help prints usage without running wizard', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'init', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('Usage: gbrain init');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('--help prints global help', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('USAGE');
|
|
expect(stdout).toContain('gbrain <command>');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test('files --help prints subcommand help', async () => {
|
|
const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'files', '--help'], {
|
|
cwd: new URL('..', import.meta.url).pathname,
|
|
stdout: 'pipe',
|
|
stderr: 'pipe',
|
|
});
|
|
const stdout = await new Response(proc.stdout).text();
|
|
const exitCode = await proc.exited;
|
|
expect(stdout).toContain('files list');
|
|
expect(stdout).toContain('files upload');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
});
|