From 39a8d54f3fad75b47919ad78b7c85aea22fc04e7 Mon Sep 17 00:00:00 2001 From: Jordan Partridge Date: Wed, 11 Feb 2026 10:49:23 -0700 Subject: [PATCH] refactor: rename Odin references to Remote for public usability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depersonalizes the codebase by replacing all "Odin" references with generic "Remote" terminology. The remote sync feature is optional infrastructure for home labs and teams sharing a centralized vector store — naming should reflect that, not a specific server. Renames: OdinSyncService → RemoteSyncService, OdinSyncCommand → RemoteSyncCommand, sync:odin → sync:remote, --odin → --remote, ODIN_* env vars → REMOTE_SYNC_*, docker-compose.odin.yml → docker-compose.remote.yml. All tests updated accordingly. --- .env.example | 6 +- CLAUDE.md | 2 +- MISSION.md | 8 +- README.md | 8 +- ROADMAP.md | 4 +- app/Commands/KnowledgeStatsCommand.php | 14 +- ...nSyncCommand.php => RemoteSyncCommand.php} | 58 +++--- app/Commands/Service/DownCommand.php | 8 +- app/Commands/Service/LogsCommand.php | 8 +- app/Commands/Service/StatusCommand.php | 8 +- app/Commands/Service/UpCommand.php | 8 +- app/Providers/AppServiceProvider.php | 6 +- ...nSyncService.php => RemoteSyncService.php} | 34 ++-- app/Services/ThemeClassifierService.php | 2 +- config/services.php | 18 +- ...pose.odin.yml => docker-compose.remote.yml | 2 +- ...mandTest.php => RemoteSyncCommandTest.php} | 174 +++++++++--------- .../Commands/Service/DownCommandTest.php | 18 +- .../Commands/Service/LogsCommandTest.php | 12 +- .../Commands/Service/StatusCommandTest.php | 14 +- .../Commands/Service/UpCommandTest.php | 18 +- tests/Feature/KnowledgeStatsCommandTest.php | 54 +++--- ...viceTest.php => RemoteSyncServiceTest.php} | 130 ++++++------- .../Services/ThemeClassifierServiceTest.php | 2 +- 24 files changed, 308 insertions(+), 308 deletions(-) rename app/Commands/{OdinSyncCommand.php => RemoteSyncCommand.php} (76%) rename app/Services/{OdinSyncService.php => RemoteSyncService.php} (94%) rename docker-compose.odin.yml => docker-compose.remote.yml (95%) rename tests/Feature/Commands/{OdinSyncCommandTest.php => RemoteSyncCommandTest.php} (59%) rename tests/Unit/Services/{OdinSyncServiceTest.php => RemoteSyncServiceTest.php} (79%) diff --git a/.env.example b/.env.example index 2fba421..e3f6b1e 100644 --- a/.env.example +++ b/.env.example @@ -16,9 +16,9 @@ OLLAMA_PORT=11434 OLLAMA_MODEL=llama3.2:3b # Centralized Sync (optional, for multi-machine knowledge sharing) -ODIN_SYNC_ENABLED=false -# ODIN_URL=http://your-server:8080 -# ODIN_API_TOKEN=your-token +REMOTE_SYNC_ENABLED=false +# REMOTE_SYNC_URL=http://your-server:8080 +# REMOTE_SYNC_TOKEN=your-token # Cloud API Sync (optional) # PREFRONTAL_API_URL=http://your-api:8080 diff --git a/CLAUDE.md b/CLAUDE.md index 340e55b..fd8c661 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ vendor/bin/pest tests/Feature/Commands/KnowledgeSearchCommandTest.php | `QdrantService` | All vector DB operations (upsert, search, delete, collections) | | `EmbeddingService` | Text-to-vector conversion | | `KnowledgeCacheService` | Redis caching for sub-200ms queries | -| `OdinSyncService` | Background sync to centralized Odin server | +| `RemoteSyncService` | Background sync to centralized remote server | | `WriteGateService` | Filters knowledge quality before persistence | | `EntryMetadataService` | Staleness detection, confidence degradation | | `CorrectionService` | Multi-tier correction propagation | diff --git a/MISSION.md b/MISSION.md index bd97a56..8254f26 100644 --- a/MISSION.md +++ b/MISSION.md @@ -16,7 +16,7 @@ Knowledge is a command-line tool that captures technical decisions, learnings, a ### 2. Offline-First, Sync-Later - **Local Qdrant**: Full functionality without network -- **Background sync**: Queue writes, sync to Odin every N minutes +- **Background sync**: Queue writes, sync to remote server every N minutes - **Read-optimized**: Instant local reads, async writes to central DB - **Graceful degradation**: Always functional, network optional @@ -82,10 +82,10 @@ Knowledge is a command-line tool that captures technical decisions, learnings, a │ Background sync ▼ ┌─────────────────────────────────────────┐ -│ Odin (Centralized) │ +│ Remote Server (Centralized) │ │ - Same stack as local │ -│ - Team knowledge repository │ -│ - Exposed via Tailscale mesh │ +│ - Team/shared knowledge repository │ +│ - Exposed via VPN, Tailscale, or LAN │ └─────────────────────────────────────────┘ ``` diff --git a/README.md b/README.md index a8c8c92..fb4fbcf 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Embedding Server (sentence-transformers) ↓ Ollama (optional - async auto-tagging via background queue) ↓ -Odin Sync (background sync to centralized server) +Remote Sync (optional - background sync to centralized server) ``` No SQLite. No schema migrations. Pure vector storage. Per-project isolation via auto-detected git namespaces. @@ -96,7 +96,7 @@ All commands support `--project=` to target a specific project namespace a | Command | Description | |---------|-------------| | `sync` | Bidirectional sync (--push / --pull) | -| `sync:odin` | Background sync to Odin central server | +| `sync:remote` | Background sync to centralized remote server | | `sync:purge` | Purge sync queue | ### Code Intelligence @@ -162,9 +162,9 @@ REDIS_PORT=6379 OLLAMA_HOST=http://localhost:11434 ``` -### Odin (Production) +### Remote Server (Production) -Uses `docker-compose.odin.yml` with Tailscale networking for centralized knowledge sync. +Uses `docker-compose.remote.yml` to bind services to a specific network interface (e.g. Tailscale, VPN, LAN) for centralized knowledge sync across multiple machines. ## Development diff --git a/ROADMAP.md b/ROADMAP.md index 6819a2b..0ef0786 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,8 +8,8 @@ Replaced SQLite entirely with Qdrant-only architecture. No schema migrations, no ### Redis Caching Layer KnowledgeCacheService provides sub-200ms query responses through aggressive caching of embeddings, search results, and collection stats. -### Odin Background Sync -OdinSyncService syncs knowledge to centralized Odin server. Includes deletion propagation, sync purge, and bidirectional push/pull. +### Remote Background Sync +RemoteSyncService syncs knowledge to a centralized remote server. Includes deletion propagation, sync purge, and bidirectional push/pull. Useful for home labs and teams sharing a knowledge base. ### Entry Metadata & Staleness Detection EntryMetadataService tracks entry freshness with confidence degradation over time. Superseded marking instead of destructive overwrites. diff --git a/app/Commands/KnowledgeStatsCommand.php b/app/Commands/KnowledgeStatsCommand.php index 93ef1c0..c49243e 100644 --- a/app/Commands/KnowledgeStatsCommand.php +++ b/app/Commands/KnowledgeStatsCommand.php @@ -6,8 +6,8 @@ use App\Commands\Concerns\ResolvesProject; use App\Services\KnowledgeCacheService; -use App\Services\OdinSyncService; use App\Services\QdrantService; +use App\Services\RemoteSyncService; use Illuminate\Support\Collection; use LaravelZero\Framework\Commands\Command; @@ -25,7 +25,7 @@ class KnowledgeStatsCommand extends Command protected $description = 'Display analytics dashboard for knowledge entries'; - public function handle(QdrantService $qdrant, OdinSyncService $odinSync): int + public function handle(QdrantService $qdrant, RemoteSyncService $remoteSync): int { $project = $this->resolveProject(); @@ -44,7 +44,7 @@ public function handle(QdrantService $qdrant, OdinSyncService $odinSync): int $this->renderCacheMetrics($cacheService); } - $this->renderSyncStatus($odinSync); + $this->renderSyncStatus($remoteSync); return self::SUCCESS; } @@ -138,13 +138,13 @@ private function renderCacheMetrics(KnowledgeCacheService $cacheService): void table(['Cache', 'Hits', 'Misses', 'Hit Rate'], $rows); } - private function renderSyncStatus(OdinSyncService $odinSync): void + private function renderSyncStatus(RemoteSyncService $remoteSync): void { - if (! $odinSync->isEnabled()) { + if (! $remoteSync->isEnabled()) { return; } - $status = $odinSync->getStatus(); + $status = $remoteSync->getStatus(); $statusColor = match ($status['status']) { 'synced' => 'green', @@ -154,7 +154,7 @@ private function renderSyncStatus(OdinSyncService $odinSync): void }; $this->newLine(); - $this->line('Odin Sync'); + $this->line('Remote Sync'); table( ['Property', 'Value'], [ diff --git a/app/Commands/OdinSyncCommand.php b/app/Commands/RemoteSyncCommand.php similarity index 76% rename from app/Commands/OdinSyncCommand.php rename to app/Commands/RemoteSyncCommand.php index 011c7a9..f40438c 100644 --- a/app/Commands/OdinSyncCommand.php +++ b/app/Commands/RemoteSyncCommand.php @@ -4,19 +4,19 @@ namespace App\Commands; -use App\Services\OdinSyncService; use App\Services\QdrantService; +use App\Services\RemoteSyncService; use Illuminate\Support\Str; use LaravelZero\Framework\Commands\Command; -class OdinSyncCommand extends Command +class RemoteSyncCommand extends Command { /** * @var string */ - protected $signature = 'sync:odin - {--push : Push queued entries to Odin} - {--pull : Pull entries from Odin} + protected $signature = 'sync:remote + {--push : Push queued entries to remote server} + {--pull : Pull entries from remote server} {--status : Show sync status only} {--clear : Clear the sync queue} {--project=default : Project namespace to sync}'; @@ -24,26 +24,26 @@ class OdinSyncCommand extends Command /** * @var string */ - protected $description = 'Synchronize knowledge with Odin centralized server'; + protected $description = 'Synchronize knowledge with remote centralized server'; - public function handle(OdinSyncService $odinSync, QdrantService $qdrant): int + public function handle(RemoteSyncService $remoteSync, QdrantService $qdrant): int { - if (! $odinSync->isEnabled()) { - $this->error('Odin sync is disabled. Set ODIN_SYNC_ENABLED=true to enable.'); + if (! $remoteSync->isEnabled()) { + $this->error('Remote sync is disabled. Set REMOTE_SYNC_ENABLED=true to enable.'); return self::FAILURE; } // Status-only mode if ((bool) $this->option('status')) { - $this->displayStatus($odinSync); + $this->displayStatus($remoteSync); return self::SUCCESS; } // Clear queue if ((bool) $this->option('clear')) { - $odinSync->clearQueue(); + $remoteSync->clearQueue(); $this->info('Sync queue cleared.'); return self::SUCCESS; @@ -61,19 +61,19 @@ public function handle(OdinSyncService $odinSync, QdrantService $qdrant): int } // Check connectivity - $this->line('Checking Odin connectivity...'); - $available = $odinSync->isAvailable(); + $this->line('Checking remote server connectivity...'); + $available = $remoteSync->isAvailable(); if (! $available) { - $this->warn('Odin server is not reachable. Operations will remain queued for later sync.'); + $this->warn('Remote server is not reachable. Operations will remain queued for later sync.'); - $status = $odinSync->getStatus(); + $status = $remoteSync->getStatus(); $this->line("Pending operations: {$status['pending']}"); return self::SUCCESS; } - $this->info('Odin server connected.'); + $this->info('Remote server connected.'); $pushResult = ['synced' => 0, 'failed' => 0, 'remaining' => 0]; $pullCount = 0; @@ -81,14 +81,14 @@ public function handle(OdinSyncService $odinSync, QdrantService $qdrant): int // Push queued items if ($push) { $this->line('Processing sync queue...'); - $pushResult = $odinSync->processQueue(); + $pushResult = $remoteSync->processQueue(); } - // Pull from Odin + // Pull from remote server if ($pull) { - $this->line("Pulling entries from Odin for project '{$project}'..."); - $entries = $odinSync->pullFromOdin($project); - $pullCount = $this->mergeEntries($entries, $qdrant, $odinSync, $project); + $this->line("Pulling entries from remote server for project '{$project}'..."); + $entries = $remoteSync->pullFromRemote($project); + $pullCount = $this->mergeEntries($entries, $qdrant, $remoteSync, $project); } $this->displaySyncSummary($pushResult, $pullCount, $pull); @@ -104,7 +104,7 @@ public function handle(OdinSyncService $odinSync, QdrantService $qdrant): int private function mergeEntries( array $remoteEntries, QdrantService $qdrant, - OdinSyncService $odinSync, + RemoteSyncService $remoteSync, string $project, ): int { $merged = 0; @@ -121,14 +121,14 @@ private function mergeEntries( if ($local !== null) { // Conflict resolution: last-write-wins - $winner = $odinSync->resolveConflict($local, $remote); + $winner = $remoteSync->resolveConflict($local, $remote); if ($winner === $remote) { $entry = $this->buildEntryFromRemote($remote, $local['id']); $qdrant->upsert($entry, $project); $merged++; } } else { - // New entry from Odin + // New entry from remote server $entry = $this->buildEntryFromRemote($remote, Str::uuid()->toString()); $qdrant->upsert($entry, $project); $merged++; @@ -173,9 +173,9 @@ private function buildEntryFromRemote(array $remote, string|int $id): array return $entry; } - private function displayStatus(OdinSyncService $odinSync): void + private function displayStatus(RemoteSyncService $remoteSync): void { - $status = $odinSync->getStatus(); + $status = $remoteSync->getStatus(); $statusColor = match ($status['status']) { 'synced' => 'green', @@ -185,7 +185,7 @@ private function displayStatus(OdinSyncService $odinSync): void }; $this->newLine(); - $this->line('Odin Sync Status'); + $this->line('Remote Sync Status'); $this->table( ['Property', 'Value'], [ @@ -193,7 +193,7 @@ private function displayStatus(OdinSyncService $odinSync): void ['Pending Operations', (string) $status['pending']], ['Last Synced', $status['last_synced'] ?? 'Never'], ['Last Error', $status['last_error'] ?? 'None'], - ['Odin URL', (string) config('services.odin.url', 'Not configured')], + ['Remote URL', (string) config('services.remote.url', 'Not configured')], ] ); } @@ -204,7 +204,7 @@ private function displayStatus(OdinSyncService $odinSync): void private function displaySyncSummary(array $pushResult, int $pullCount, bool $pulled): void { $this->newLine(); - $this->info('=== Odin Sync Summary ==='); + $this->info('=== Remote Sync Summary ==='); $rows = [ ['Pushed (synced)', (string) $pushResult['synced']], diff --git a/app/Commands/Service/DownCommand.php b/app/Commands/Service/DownCommand.php index 706afca..9728cb0 100644 --- a/app/Commands/Service/DownCommand.php +++ b/app/Commands/Service/DownCommand.php @@ -14,18 +14,18 @@ class DownCommand extends Command { protected $signature = 'service:down {--volumes : Remove volumes} - {--odin : Use Odin (remote) configuration} + {--remote : Use remote configuration} {--force : Skip confirmation prompts}'; protected $description = 'Stop knowledge services'; public function handle(): int { - $composeFile = $this->option('odin') === true - ? 'docker-compose.odin.yml' + $composeFile = $this->option('remote') === true + ? 'docker-compose.remote.yml' : 'docker-compose.yml'; - $environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local'; + $environment = $this->option('remote') === true ? 'Remote' : 'Local'; if (! file_exists(base_path($composeFile))) { render(<<option('odin') === true - ? 'docker-compose.odin.yml' + $composeFile = $this->option('remote') === true + ? 'docker-compose.remote.yml' : 'docker-compose.yml'; - $environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local'; + $environment = $this->option('remote') === true ? 'Remote' : 'Local'; if (! file_exists(base_path($composeFile))) { render(<<option('odin') === true - ? 'docker-compose.odin.yml' + $composeFile = $this->option('remote') === true + ? 'docker-compose.remote.yml' : 'docker-compose.yml'; - $environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local'; + $environment = $this->option('remote') === true ? 'Remote' : 'Local'; // Perform health checks with spinner $healthData = spin( diff --git a/app/Commands/Service/UpCommand.php b/app/Commands/Service/UpCommand.php index 61ccf5e..2b0004b 100644 --- a/app/Commands/Service/UpCommand.php +++ b/app/Commands/Service/UpCommand.php @@ -13,17 +13,17 @@ class UpCommand extends Command { protected $signature = 'service:up {--d|detach : Run in detached mode} - {--odin : Use Odin (remote) configuration}'; + {--remote : Use remote configuration}'; protected $description = 'Start knowledge services (Qdrant, Redis, Embeddings)'; public function handle(): int { - $composeFile = $this->option('odin') === true - ? 'docker-compose.odin.yml' + $composeFile = $this->option('remote') === true + ? 'docker-compose.remote.yml' : 'docker-compose.yml'; - $environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local'; + $environment = $this->option('remote') === true ? 'Remote' : 'Local'; if (! file_exists(base_path($composeFile))) { render(<<make(EntryMetadataService::class), )); - // Odin sync service - $this->app->singleton(OdinSyncService::class, fn ($app): \App\Services\OdinSyncService => new OdinSyncService( + // Remote sync service + $this->app->singleton(RemoteSyncService::class, fn ($app): \App\Services\RemoteSyncService => new RemoteSyncService( $app->make(KnowledgePathService::class) )); diff --git a/app/Services/OdinSyncService.php b/app/Services/RemoteSyncService.php similarity index 94% rename from app/Services/OdinSyncService.php rename to app/Services/RemoteSyncService.php index a318dfb..80c3877 100644 --- a/app/Services/OdinSyncService.php +++ b/app/Services/RemoteSyncService.php @@ -7,7 +7,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; -class OdinSyncService +class RemoteSyncService { private readonly string $queuePath; @@ -24,15 +24,15 @@ public function __construct( } /** - * Check if Odin sync is enabled in configuration. + * Check if remote sync is enabled. */ public function isEnabled(): bool { - return (bool) config('services.odin.enabled', true); + return (bool) config('services.remote.enabled', true); } /** - * Check connectivity to Odin server. + * Check connectivity to remote server. */ public function isAvailable(): bool { @@ -60,7 +60,7 @@ public function isAvailable(): bool } /** - * Queue an entry for sync to Odin. + * Queue an entry for sync to remote server. * * @param array $entry */ @@ -83,7 +83,7 @@ public function queueForSync(array $entry, string $operation = 'upsert', string } /** - * Process the sync queue, pushing pending items to Odin. + * Process the sync queue, pushing pending items to the remote server. * * @return array{synced: int, failed: int, remaining: int} */ @@ -105,7 +105,7 @@ public function processQueue(): array } $remaining = []; - $batchSize = max(1, (int) config('services.odin.batch_size', 50)); + $batchSize = max(1, (int) config('services.remote.batch_size', 50)); $batches = array_chunk($queue, $batchSize); foreach ($batches as $batch) { @@ -146,11 +146,11 @@ public function processQueue(): array } /** - * Pull fresh entries from Odin for a project. + * Pull fresh entries from remote server. * * @return array> */ - public function pullFromOdin(string $project = 'default'): array + public function pullFromRemote(string $project = 'default'): array { $token = $this->getToken(); if ($token === '') { @@ -183,7 +183,7 @@ public function pullFromOdin(string $project = 'default'): array } /** - * List all projects that have been synced to Odin. + * List all projects that have been synced to remote server. * * @return array */ @@ -313,7 +313,7 @@ public function clearQueue(): void } /** - * Push a batch of entries to Odin. + * Push a batch of entries to remote server. * * @param array> $items * @return array{synced: int, failed: int, failedItems: array>} @@ -376,7 +376,7 @@ private function pushBatch(string $token, array $items): array } /** - * Delete a batch of entries from Odin. + * Delete a batch of entries from remote server. * * @param array> $items * @return array{synced: int, failed: int, failedItems: array>} @@ -483,21 +483,21 @@ private function updateStatus(string $status, int $pending, ?string $lastSynced } /** - * Get the Odin API base URL. + * Get the remote API base URL. */ private function getBaseUrl(): string { - $url = config('services.odin.url', ''); + $url = config('services.remote.url', ''); return is_string($url) ? $url : ''; } /** - * Get the Odin API token. + * Get the remote API token. */ private function getToken(): string { - $token = config('services.odin.token', ''); + $token = config('services.remote.token', ''); return is_string($token) ? $token : ''; } @@ -525,7 +525,7 @@ protected function createClient(): Client { return new Client([ 'base_uri' => $this->getBaseUrl(), - 'timeout' => (int) config('services.odin.timeout', 10), + 'timeout' => (int) config('services.remote.timeout', 10), 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', diff --git a/app/Services/ThemeClassifierService.php b/app/Services/ThemeClassifierService.php index 076dfef..d52c3bc 100644 --- a/app/Services/ThemeClassifierService.php +++ b/app/Services/ThemeClassifierService.php @@ -51,7 +51,7 @@ class ThemeClassifierService ], 'integrated-infrastructure' => [ 'keywords' => [ - 'infrastructure', 'server', 'homelab', 'odin', 'docker', + 'infrastructure', 'server', 'homelab', 'docker', 'podman', 'container', 'deploy', 'deployment', 'hosting', 'property', 'physical', 'hardware', 'network', 'tailscale', 'nginx', 'redis', 'database', 'postgres', 'mysql', diff --git a/config/services.php b/config/services.php index 5ddf9b9..b870e8b 100644 --- a/config/services.php +++ b/config/services.php @@ -19,21 +19,21 @@ /* |-------------------------------------------------------------------------- - | Odin Sync Configuration + | Remote Sync Configuration |-------------------------------------------------------------------------- | - | Configuration for background sync with the Odin centralized Qdrant - | server. Operations are queued locally and synced when Odin is available. + | Configuration for background sync with a centralized Qdrant + | server. Operations are queued locally and synced when the remote server is available. | Last-write-wins conflict resolution based on updated_at timestamps. | */ - 'odin' => [ - 'enabled' => env('ODIN_SYNC_ENABLED', false), - 'url' => env('ODIN_URL'), - 'token' => env('ODIN_API_TOKEN', env('PREFRONTAL_API_TOKEN')), - 'timeout' => env('ODIN_TIMEOUT', 10), - 'batch_size' => env('ODIN_BATCH_SIZE', 50), + 'remote' => [ + 'enabled' => env('REMOTE_SYNC_ENABLED', false), + 'url' => env('REMOTE_SYNC_URL'), + 'token' => env('REMOTE_SYNC_TOKEN', env('PREFRONTAL_API_TOKEN')), + 'timeout' => env('REMOTE_SYNC_TIMEOUT', 10), + 'batch_size' => env('REMOTE_SYNC_BATCH_SIZE', 50), ], ]; diff --git a/docker-compose.odin.yml b/docker-compose.remote.yml similarity index 95% rename from docker-compose.odin.yml rename to docker-compose.remote.yml index 71d72bf..217f489 100644 --- a/docker-compose.odin.yml +++ b/docker-compose.remote.yml @@ -2,7 +2,7 @@ # Binds to a specific network interface (e.g. Tailscale, VPN, LAN) # # Usage: -# BIND_ADDR=100.68.122.24 docker compose -f docker-compose.odin.yml up -d +# BIND_ADDR=192.168.1.100 docker compose -f docker-compose.remote.yml up -d # # Set BIND_ADDR to your server's interface IP (Tailscale, LAN, etc.) # Defaults to 127.0.0.1 (localhost only) if not set. diff --git a/tests/Feature/Commands/OdinSyncCommandTest.php b/tests/Feature/Commands/RemoteSyncCommandTest.php similarity index 59% rename from tests/Feature/Commands/OdinSyncCommandTest.php rename to tests/Feature/Commands/RemoteSyncCommandTest.php index 88deded..7a96ccf 100644 --- a/tests/Feature/Commands/OdinSyncCommandTest.php +++ b/tests/Feature/Commands/RemoteSyncCommandTest.php @@ -2,90 +2,90 @@ declare(strict_types=1); -use App\Services\OdinSyncService; use App\Services\QdrantService; +use App\Services\RemoteSyncService; beforeEach(function (): void { - $this->odinMock = Mockery::mock(OdinSyncService::class); + $this->remoteMock = Mockery::mock(RemoteSyncService::class); $this->qdrantMock = Mockery::mock(QdrantService::class); - $this->app->instance(OdinSyncService::class, $this->odinMock); + $this->app->instance(RemoteSyncService::class, $this->remoteMock); $this->app->instance(QdrantService::class, $this->qdrantMock); }); -describe('OdinSyncCommand basic', function (): void { - it('fails when odin sync is disabled', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(false); +describe('RemoteSyncCommand basic', function (): void { + it('fails when remote sync is disabled', function (): void { + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(false); - $this->artisan('sync:odin') + $this->artisan('sync:remote') ->assertFailed() - ->expectsOutput('Odin sync is disabled. Set ODIN_SYNC_ENABLED=true to enable.'); + ->expectsOutput('Remote sync is disabled. Set REMOTE_SYNC_ENABLED=true to enable.'); }); it('shows status only with --status flag', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('getStatus')->once()->andReturn([ + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('getStatus')->once()->andReturn([ 'status' => 'synced', 'pending' => 0, 'last_synced' => '2025-06-01T12:00:00+00:00', 'last_error' => null, ]); - config(['services.odin.url' => 'http://odin.local']); + config(['services.remote.url' => 'http://remote.local']); - $this->artisan('sync:odin', ['--status' => true]) + $this->artisan('sync:remote', ['--status' => true]) ->assertSuccessful() - ->expectsOutputToContain('Odin Sync Status'); + ->expectsOutputToContain('Remote Sync Status'); }); it('clears queue with --clear flag', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('clearQueue')->once(); + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('clearQueue')->once(); - $this->artisan('sync:odin', ['--clear' => true]) + $this->artisan('sync:remote', ['--clear' => true]) ->assertSuccessful() ->expectsOutput('Sync queue cleared.'); }); }); -describe('OdinSyncCommand connectivity', function (): void { - it('warns when Odin is unreachable', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(false); - $this->odinMock->shouldReceive('getStatus')->once()->andReturn([ +describe('RemoteSyncCommand connectivity', function (): void { + it('warns when remote is unreachable', function (): void { + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(false); + $this->remoteMock->shouldReceive('getStatus')->once()->andReturn([ 'status' => 'pending', 'pending' => 3, 'last_synced' => null, 'last_error' => null, ]); - $this->artisan('sync:odin') + $this->artisan('sync:remote') ->assertSuccessful() - ->expectsOutputToContain('Odin server is not reachable') + ->expectsOutputToContain('Remote server is not reachable') ->expectsOutputToContain('Pending operations: 3'); }); }); -describe('OdinSyncCommand push', function (): void { +describe('RemoteSyncCommand push', function (): void { it('pushes queued items when --push is specified', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('processQueue')->once()->andReturn([ + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('processQueue')->once()->andReturn([ 'synced' => 5, 'failed' => 0, 'remaining' => 0, ]); - $this->artisan('sync:odin', ['--push' => true]) + $this->artisan('sync:remote', ['--push' => true]) ->assertSuccessful() - ->expectsOutputToContain('Odin Sync Summary'); + ->expectsOutputToContain('Remote Sync Summary'); }); }); -describe('OdinSyncCommand pull', function (): void { +describe('RemoteSyncCommand pull', function (): void { it('pulls and merges entries when --pull is specified', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->with('default') ->andReturn([ @@ -114,11 +114,11 @@ ->once() ->andReturn(true); - $this->odinMock->shouldNotReceive('resolveConflict'); + $this->remoteMock->shouldNotReceive('resolveConflict'); - $this->artisan('sync:odin', ['--pull' => true]) + $this->artisan('sync:remote', ['--pull' => true]) ->assertSuccessful() - ->expectsOutputToContain('Odin Sync Summary'); + ->expectsOutputToContain('Remote Sync Summary'); }); it('resolves conflicts with last-write-wins on pull', function (): void { @@ -142,9 +142,9 @@ 'usage_count' => 0, ]; - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->with('default') ->andReturn([$remoteEntry]); @@ -155,7 +155,7 @@ ->andReturn(collect([$localEntry])); // Remote is newer, should win - $this->odinMock->shouldReceive('resolveConflict') + $this->remoteMock->shouldReceive('resolveConflict') ->once() ->with($localEntry, $remoteEntry) ->andReturn($remoteEntry); @@ -164,14 +164,14 @@ ->once() ->andReturn(true); - $this->artisan('sync:odin', ['--pull' => true]) + $this->artisan('sync:remote', ['--pull' => true]) ->assertSuccessful(); }); it('skips entries with empty title on pull', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->andReturn([ ['title' => '', 'content' => 'No title'], @@ -180,7 +180,7 @@ $this->qdrantMock->shouldNotReceive('search'); $this->qdrantMock->shouldNotReceive('upsert'); - $this->artisan('sync:odin', ['--pull' => true]) + $this->artisan('sync:remote', ['--pull' => true]) ->assertSuccessful(); }); @@ -198,9 +198,9 @@ 'updated_at' => '2025-05-01T12:00:00+00:00', ]; - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->andReturn([$remoteEntry]); @@ -209,23 +209,23 @@ ->andReturn(collect([$localEntry])); // Local wins - $this->odinMock->shouldReceive('resolveConflict') + $this->remoteMock->shouldReceive('resolveConflict') ->once() ->andReturn($localEntry); // Should NOT upsert since local wins $this->qdrantMock->shouldNotReceive('upsert'); - $this->artisan('sync:odin', ['--pull' => true]) + $this->artisan('sync:remote', ['--pull' => true]) ->assertSuccessful(); }); }); -describe('OdinSyncCommand pull with optional fields', function (): void { +describe('RemoteSyncCommand pull with optional fields', function (): void { it('includes category and module from remote entry when set', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->with('default') ->andReturn([ @@ -256,14 +256,14 @@ && ($data['module'] ?? null) === 'core-api'), Mockery::any()) ->andReturn(true); - $this->artisan('sync:odin', ['--pull' => true]) + $this->artisan('sync:remote', ['--pull' => true]) ->assertSuccessful(); }); it('omits category and module when not set in remote entry', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->with('default') ->andReturn([ @@ -290,94 +290,94 @@ && ! array_key_exists('module', $data)), Mockery::any()) ->andReturn(true); - $this->artisan('sync:odin', ['--pull' => true]) + $this->artisan('sync:remote', ['--pull' => true]) ->assertSuccessful(); }); }); -describe('OdinSyncCommand status display', function (): void { +describe('RemoteSyncCommand status display', function (): void { it('uses red color for error sync status', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('getStatus')->once()->andReturn([ + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('getStatus')->once()->andReturn([ 'status' => 'error', 'pending' => 0, 'last_synced' => null, 'last_error' => 'Connection refused', ]); - config(['services.odin.url' => 'http://odin.local']); + config(['services.remote.url' => 'http://remote.local']); - $this->artisan('sync:odin', ['--status' => true]) + $this->artisan('sync:remote', ['--status' => true]) ->assertSuccessful() - ->expectsOutputToContain('Odin Sync Status'); + ->expectsOutputToContain('Remote Sync Status'); }); it('uses yellow color for pending sync status', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('getStatus')->once()->andReturn([ + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('getStatus')->once()->andReturn([ 'status' => 'pending', 'pending' => 3, 'last_synced' => null, 'last_error' => null, ]); - config(['services.odin.url' => 'http://odin.local']); + config(['services.remote.url' => 'http://remote.local']); - $this->artisan('sync:odin', ['--status' => true]) + $this->artisan('sync:remote', ['--status' => true]) ->assertSuccessful() - ->expectsOutputToContain('Odin Sync Status'); + ->expectsOutputToContain('Remote Sync Status'); }); it('uses gray color for unknown sync status', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('getStatus')->once()->andReturn([ + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('getStatus')->once()->andReturn([ 'status' => 'unknown-status', 'pending' => 0, 'last_synced' => null, 'last_error' => null, ]); - config(['services.odin.url' => 'http://odin.local']); + config(['services.remote.url' => 'http://remote.local']); - $this->artisan('sync:odin', ['--status' => true]) + $this->artisan('sync:remote', ['--status' => true]) ->assertSuccessful() - ->expectsOutputToContain('Odin Sync Status'); + ->expectsOutputToContain('Remote Sync Status'); }); }); -describe('OdinSyncCommand default two-way sync', function (): void { +describe('RemoteSyncCommand default two-way sync', function (): void { it('performs push then pull with no flags', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('processQueue')->once()->andReturn([ + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('processQueue')->once()->andReturn([ 'synced' => 2, 'failed' => 0, 'remaining' => 0, ]); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->with('default') ->andReturn([]); - $this->artisan('sync:odin') + $this->artisan('sync:remote') ->assertSuccessful() - ->expectsOutputToContain('Odin Sync Summary'); + ->expectsOutputToContain('Remote Sync Summary'); }); it('accepts custom project option', function (): void { - $this->odinMock->shouldReceive('isEnabled')->once()->andReturn(true); - $this->odinMock->shouldReceive('isAvailable')->once()->andReturn(true); - $this->odinMock->shouldReceive('processQueue')->once()->andReturn([ + $this->remoteMock->shouldReceive('isEnabled')->once()->andReturn(true); + $this->remoteMock->shouldReceive('isAvailable')->once()->andReturn(true); + $this->remoteMock->shouldReceive('processQueue')->once()->andReturn([ 'synced' => 0, 'failed' => 0, 'remaining' => 0, ]); - $this->odinMock->shouldReceive('pullFromOdin') + $this->remoteMock->shouldReceive('pullFromRemote') ->once() ->with('custom-project') ->andReturn([]); - $this->artisan('sync:odin', ['--project' => 'custom-project']) + $this->artisan('sync:remote', ['--project' => 'custom-project']) ->assertSuccessful(); }); }); diff --git a/tests/Feature/Commands/Service/DownCommandTest.php b/tests/Feature/Commands/Service/DownCommandTest.php index a4dc372..1b196e1 100644 --- a/tests/Feature/Commands/Service/DownCommandTest.php +++ b/tests/Feature/Commands/Service/DownCommandTest.php @@ -26,13 +26,13 @@ } }); - it('fails when odin docker-compose file does not exist', function () { + it('fails when remote docker-compose file does not exist', function () { $tempDir = sys_get_temp_dir().'/know-test-'.uniqid(); mkdir($tempDir, 0755, true); $this->app->setBasePath($tempDir); try { - $this->artisan('service:down', ['--odin' => true]) + $this->artisan('service:down', ['--remote' => true]) ->assertFailed(); } finally { rmdir($tempDir); @@ -57,11 +57,11 @@ && in_array('-v', $process->command)); }); - it('uses odin compose file when odin flag is set', function () { - $this->artisan('service:down', ['--odin' => true]) + it('uses remote compose file when remote flag is set', function () { + $this->artisan('service:down', ['--remote' => true]) ->assertSuccessful(); - Process::assertRan(fn ($process) => in_array('docker-compose.odin.yml', $process->command)); + Process::assertRan(fn ($process) => in_array('docker-compose.remote.yml', $process->command)); }); it('returns failure when docker compose fails', function () { @@ -76,11 +76,11 @@ ->assertFailed(); }); - it('combines odin and volumes flags correctly when forced', function () { - $this->artisan('service:down', ['--odin' => true, '--volumes' => true, '--force' => true]) + it('combines remote and volumes flags correctly when forced', function () { + $this->artisan('service:down', ['--remote' => true, '--volumes' => true, '--force' => true]) ->assertSuccessful(); - Process::assertRan(fn ($process) => in_array('docker-compose.odin.yml', $process->command) + Process::assertRan(fn ($process) => in_array('docker-compose.remote.yml', $process->command) && in_array('-v', $process->command)); }); }); @@ -95,7 +95,7 @@ expect($signature)->toContain('service:down'); expect($signature)->toContain('--volumes'); - expect($signature)->toContain('--odin'); + expect($signature)->toContain('--remote'); expect($signature)->toContain('--force'); }); diff --git a/tests/Feature/Commands/Service/LogsCommandTest.php b/tests/Feature/Commands/Service/LogsCommandTest.php index 43d8217..bbc56e1 100644 --- a/tests/Feature/Commands/Service/LogsCommandTest.php +++ b/tests/Feature/Commands/Service/LogsCommandTest.php @@ -26,7 +26,7 @@ } }); - it('fails when odin docker-compose file does not exist', function () { + it('fails when remote docker-compose file does not exist', function () { $tempDir = sys_get_temp_dir().'/know-test-'.uniqid(); mkdir($tempDir, 0755, true); $this->app->setBasePath($tempDir); @@ -34,7 +34,7 @@ try { $this->artisan('service:logs', [ 'service' => 'qdrant', - '--odin' => true, + '--remote' => true, ]) ->assertFailed(); } finally { @@ -69,11 +69,11 @@ Process::assertRan(fn ($process) => in_array('--tail=100', $process->command)); }); - it('uses odin compose file when odin flag is set', function () { - $this->artisan('service:logs', ['service' => 'qdrant', '--odin' => true]) + it('uses remote compose file when remote flag is set', function () { + $this->artisan('service:logs', ['service' => 'qdrant', '--remote' => true]) ->assertSuccessful(); - Process::assertRan(fn ($process) => in_array('docker-compose.odin.yml', $process->command)); + Process::assertRan(fn ($process) => in_array('docker-compose.remote.yml', $process->command)); }); it('returns exit code from docker compose', function () { @@ -122,7 +122,7 @@ expect($signature)->toContain('{service?'); expect($signature)->toContain('--f|follow'); expect($signature)->toContain('--tail=50'); - expect($signature)->toContain('--odin'); + expect($signature)->toContain('--remote'); }); it('has correct description', function () { diff --git a/tests/Feature/Commands/Service/StatusCommandTest.php b/tests/Feature/Commands/Service/StatusCommandTest.php index 87914ec..e64345a 100644 --- a/tests/Feature/Commands/Service/StatusCommandTest.php +++ b/tests/Feature/Commands/Service/StatusCommandTest.php @@ -57,11 +57,11 @@ ->assertSuccessful(); }); - it('shows odin environment with --odin flag', function () { - $this->artisan('service:status', ['--odin' => true]) + it('shows remote environment with --remote flag', function () { + $this->artisan('service:status', ['--remote' => true]) ->assertSuccessful(); - Process::assertRan(fn ($process) => in_array('docker-compose.odin.yml', $process->command)); + Process::assertRan(fn ($process) => in_array('docker-compose.remote.yml', $process->command)); }); }); @@ -143,11 +143,11 @@ && in_array('ps', $process->command)); }); - it('uses odin compose file with --odin flag', function () { - $this->artisan('service:status', ['--odin' => true]) + it('uses remote compose file with --remote flag', function () { + $this->artisan('service:status', ['--remote' => true]) ->assertSuccessful(); - Process::assertRan(fn ($process) => in_array('docker-compose.odin.yml', $process->command)); + Process::assertRan(fn ($process) => in_array('docker-compose.remote.yml', $process->command)); }); it('handles docker compose failure gracefully', function () { @@ -196,7 +196,7 @@ $signature = $signatureProperty->getValue($command); expect($signature)->toContain('service:status'); - expect($signature)->toContain('--odin'); + expect($signature)->toContain('--remote'); }); it('has correct description', function () { diff --git a/tests/Feature/Commands/Service/UpCommandTest.php b/tests/Feature/Commands/Service/UpCommandTest.php index 1751d11..e853657 100644 --- a/tests/Feature/Commands/Service/UpCommandTest.php +++ b/tests/Feature/Commands/Service/UpCommandTest.php @@ -26,13 +26,13 @@ } }); - it('fails when odin docker-compose file does not exist', function () { + it('fails when remote docker-compose file does not exist', function () { $tempDir = sys_get_temp_dir().'/know-test-'.uniqid(); mkdir($tempDir, 0755, true); $this->app->setBasePath($tempDir); try { - $this->artisan('service:up', ['--odin' => true]) + $this->artisan('service:up', ['--remote' => true]) ->assertFailed(); } finally { rmdir($tempDir); @@ -57,11 +57,11 @@ && in_array('-d', $process->command)); }); - it('uses odin compose file when odin flag is set', function () { - $this->artisan('service:up', ['--odin' => true]) + it('uses remote compose file when remote flag is set', function () { + $this->artisan('service:up', ['--remote' => true]) ->assertSuccessful(); - Process::assertRan(fn ($process) => in_array('docker-compose.odin.yml', $process->command)); + Process::assertRan(fn ($process) => in_array('docker-compose.remote.yml', $process->command)); }); it('returns failure when docker compose fails', function () { @@ -76,11 +76,11 @@ ->assertFailed(); }); - it('combines odin and detach flags correctly', function () { - $this->artisan('service:up', ['--odin' => true, '--detach' => true]) + it('combines remote and detach flags correctly', function () { + $this->artisan('service:up', ['--remote' => true, '--detach' => true]) ->assertSuccessful(); - Process::assertRan(fn ($process) => in_array('docker-compose.odin.yml', $process->command) + Process::assertRan(fn ($process) => in_array('docker-compose.remote.yml', $process->command) && in_array('-d', $process->command)); }); }); @@ -95,7 +95,7 @@ expect($signature)->toContain('service:up'); expect($signature)->toContain('--d|detach'); - expect($signature)->toContain('--odin'); + expect($signature)->toContain('--remote'); }); it('has correct description', function () { diff --git a/tests/Feature/KnowledgeStatsCommandTest.php b/tests/Feature/KnowledgeStatsCommandTest.php index caea984..af5fcaf 100644 --- a/tests/Feature/KnowledgeStatsCommandTest.php +++ b/tests/Feature/KnowledgeStatsCommandTest.php @@ -3,15 +3,15 @@ declare(strict_types=1); use App\Services\KnowledgeCacheService; -use App\Services\OdinSyncService; use App\Services\QdrantService; +use App\Services\RemoteSyncService; describe('KnowledgeStatsCommand', function (): void { it('displays comprehensive analytics dashboard covering all code paths', function (): void { $qdrant = mock(QdrantService::class); - $odinSync = mock(OdinSyncService::class); + $remoteSync = mock(RemoteSyncService::class); app()->instance(QdrantService::class, $qdrant); - app()->instance(OdinSyncService::class, $odinSync); + app()->instance(RemoteSyncService::class, $remoteSync); mockProjectDetector(); // Mixed entries testing all display logic in a single test due to static caching @@ -66,7 +66,7 @@ ->once() ->andReturnNull(); - $odinSync->shouldReceive('isEnabled') + $remoteSync->shouldReceive('isEnabled') ->once() ->andReturn(false); @@ -77,9 +77,9 @@ it('displays cache metrics when cache service is available', function (): void { $qdrant = mock(QdrantService::class); $cacheService = mock(KnowledgeCacheService::class); - $odinSync = mock(OdinSyncService::class); + $remoteSync = mock(RemoteSyncService::class); app()->instance(QdrantService::class, $qdrant); - app()->instance(OdinSyncService::class, $odinSync); + app()->instance(RemoteSyncService::class, $remoteSync); mockProjectDetector(); $entries = collect([ @@ -120,7 +120,7 @@ 'stats' => ['hits' => 8, 'misses' => 2], ]); - $odinSync->shouldReceive('isEnabled') + $remoteSync->shouldReceive('isEnabled') ->once() ->andReturn(false); @@ -128,11 +128,11 @@ ->assertSuccessful(); }); - it('displays odin sync status with unknown status using gray color', function (): void { + it('displays remote sync status with unknown status using gray color', function (): void { $qdrant = mock(QdrantService::class); - $odinSync = mock(OdinSyncService::class); + $remoteSync = mock(RemoteSyncService::class); app()->instance(QdrantService::class, $qdrant); - app()->instance(OdinSyncService::class, $odinSync); + app()->instance(RemoteSyncService::class, $remoteSync); mockProjectDetector(); $entries = collect([ @@ -165,11 +165,11 @@ ->once() ->andReturnNull(); - $odinSync->shouldReceive('isEnabled') + $remoteSync->shouldReceive('isEnabled') ->once() ->andReturn(true); - $odinSync->shouldReceive('getStatus') + $remoteSync->shouldReceive('getStatus') ->once() ->andReturn([ 'status' => 'unknown-status', @@ -182,19 +182,19 @@ ->assertSuccessful(); }); - it('displays odin sync error status in red', function (): void { + it('displays remote sync error status in red', function (): void { $qdrant = mock(QdrantService::class); - $odinSync = mock(OdinSyncService::class); + $remoteSync = mock(RemoteSyncService::class); app()->instance(QdrantService::class, $qdrant); - app()->instance(OdinSyncService::class, $odinSync); + app()->instance(RemoteSyncService::class, $remoteSync); mockProjectDetector(); $qdrant->shouldReceive('count')->once()->with('default')->andReturn(0); $qdrant->shouldReceive('scroll')->once()->with([], 0, 'default')->andReturn(collect([])); $qdrant->shouldReceive('getCollectionName')->with('default')->andReturn('knowledge_default'); $qdrant->shouldReceive('getCacheService')->once()->andReturnNull(); - $odinSync->shouldReceive('isEnabled')->once()->andReturn(true); - $odinSync->shouldReceive('getStatus')->once()->andReturn([ + $remoteSync->shouldReceive('isEnabled')->once()->andReturn(true); + $remoteSync->shouldReceive('getStatus')->once()->andReturn([ 'status' => 'error', 'pending' => 0, 'last_synced' => null, @@ -204,19 +204,19 @@ $this->artisan('stats')->assertSuccessful(); }); - it('displays odin sync pending status in yellow', function (): void { + it('displays remote sync pending status in yellow', function (): void { $qdrant = mock(QdrantService::class); - $odinSync = mock(OdinSyncService::class); + $remoteSync = mock(RemoteSyncService::class); app()->instance(QdrantService::class, $qdrant); - app()->instance(OdinSyncService::class, $odinSync); + app()->instance(RemoteSyncService::class, $remoteSync); mockProjectDetector(); $qdrant->shouldReceive('count')->once()->with('default')->andReturn(0); $qdrant->shouldReceive('scroll')->once()->with([], 0, 'default')->andReturn(collect([])); $qdrant->shouldReceive('getCollectionName')->with('default')->andReturn('knowledge_default'); $qdrant->shouldReceive('getCacheService')->once()->andReturnNull(); - $odinSync->shouldReceive('isEnabled')->once()->andReturn(true); - $odinSync->shouldReceive('getStatus')->once()->andReturn([ + $remoteSync->shouldReceive('isEnabled')->once()->andReturn(true); + $remoteSync->shouldReceive('getStatus')->once()->andReturn([ 'status' => 'pending', 'pending' => 5, 'last_synced' => null, @@ -226,11 +226,11 @@ $this->artisan('stats')->assertSuccessful(); }); - it('displays odin sync status when enabled', function (): void { + it('displays remote sync status when enabled', function (): void { $qdrant = mock(QdrantService::class); - $odinSync = mock(OdinSyncService::class); + $remoteSync = mock(RemoteSyncService::class); app()->instance(QdrantService::class, $qdrant); - app()->instance(OdinSyncService::class, $odinSync); + app()->instance(RemoteSyncService::class, $remoteSync); mockProjectDetector(); $entries = collect([ @@ -263,11 +263,11 @@ ->once() ->andReturnNull(); - $odinSync->shouldReceive('isEnabled') + $remoteSync->shouldReceive('isEnabled') ->once() ->andReturn(true); - $odinSync->shouldReceive('getStatus') + $remoteSync->shouldReceive('getStatus') ->once() ->andReturn([ 'status' => 'synced', diff --git a/tests/Unit/Services/OdinSyncServiceTest.php b/tests/Unit/Services/RemoteSyncServiceTest.php similarity index 79% rename from tests/Unit/Services/OdinSyncServiceTest.php rename to tests/Unit/Services/RemoteSyncServiceTest.php index 0b59c28..f5066af 100644 --- a/tests/Unit/Services/OdinSyncServiceTest.php +++ b/tests/Unit/Services/RemoteSyncServiceTest.php @@ -3,49 +3,49 @@ declare(strict_types=1); use App\Services\KnowledgePathService; -use App\Services\OdinSyncService; +use App\Services\RemoteSyncService; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; beforeEach(function (): void { - $this->tempDir = sys_get_temp_dir().'/odin_sync_test_'.uniqid(); + $this->tempDir = sys_get_temp_dir().'/remote_sync_test_'.uniqid(); mkdir($this->tempDir, 0755, true); $this->pathService = Mockery::mock(KnowledgePathService::class); $this->pathService->shouldReceive('getKnowledgeDirectory') ->andReturn($this->tempDir); - config(['services.odin.enabled' => true]); - config(['services.odin.url' => 'http://test-odin.local']); - config(['services.odin.token' => 'test-token']); - config(['services.odin.timeout' => 5]); - config(['services.odin.batch_size' => 50]); + config(['services.remote.enabled' => true]); + config(['services.remote.url' => 'http://test-remote.local']); + config(['services.remote.token' => 'test-token']); + config(['services.remote.timeout' => 5]); + config(['services.remote.batch_size' => 50]); }); afterEach(function (): void { removeDirectory($this->tempDir); }); -describe('OdinSyncService configuration', function (): void { +describe('RemoteSyncService configuration', function (): void { it('reports enabled when config is true', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); expect($service->isEnabled())->toBeTrue(); }); it('reports disabled when config is false', function (): void { - config(['services.odin.enabled' => false]); - $service = new OdinSyncService($this->pathService); + config(['services.remote.enabled' => false]); + $service = new RemoteSyncService($this->pathService); expect($service->isEnabled())->toBeFalse(); }); }); -describe('OdinSyncService queue operations', function (): void { +describe('RemoteSyncService queue operations', function (): void { it('queues an entry for sync', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $entry = [ 'id' => 'test-1', @@ -59,8 +59,8 @@ }); it('does not queue when disabled', function (): void { - config(['services.odin.enabled' => false]); - $service = new OdinSyncService($this->pathService); + config(['services.remote.enabled' => false]); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => 'test-1', 'title' => 'Test', 'content' => 'Content']); @@ -68,7 +68,7 @@ }); it('queues multiple entries', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'First', 'content' => 'Content 1']); $service->queueForSync(['id' => '2', 'title' => 'Second', 'content' => 'Content 2']); @@ -78,7 +78,7 @@ }); it('clears the queue', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test', 'content' => 'Content']); expect($service->getPendingCount())->toBe(1); @@ -88,22 +88,22 @@ }); it('handles empty queue file gracefully', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); expect($service->getPendingCount())->toBe(0); }); it('handles corrupted queue file gracefully', function (): void { file_put_contents($this->tempDir.'/sync_queue.json', 'not-valid-json'); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); expect($service->getPendingCount())->toBe(0); }); }); -describe('OdinSyncService processQueue', function (): void { +describe('RemoteSyncService processQueue', function (): void { it('processes empty queue', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $result = $service->processQueue(); @@ -111,8 +111,8 @@ }); it('returns remaining when no token is set', function (): void { - config(['services.odin.token' => '']); - $service = new OdinSyncService($this->pathService); + config(['services.remote.token' => '']); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test', 'content' => 'Content']); $result = $service->processQueue(); @@ -129,7 +129,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test Entry', 'content' => 'Content']); $result = $service->processQueue(); @@ -149,7 +149,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test Entry', 'content' => 'Content']); $result = $service->processQueue(); @@ -169,7 +169,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test Entry', 'content' => 'Content'], 'delete'); $result = $service->processQueue(); @@ -189,7 +189,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test', 'content' => 'Content'], 'delete'); $result = $service->processQueue(); @@ -202,9 +202,9 @@ }); }); -describe('OdinSyncService conflict resolution', function (): void { +describe('RemoteSyncService conflict resolution', function (): void { it('picks local when local is newer', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $local = ['title' => 'Local', 'updated_at' => '2025-06-01T12:00:00+00:00']; $remote = ['title' => 'Remote', 'updated_at' => '2025-05-01T12:00:00+00:00']; @@ -215,7 +215,7 @@ }); it('picks remote when remote is newer', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $local = ['title' => 'Local', 'updated_at' => '2025-05-01T12:00:00+00:00']; $remote = ['title' => 'Remote', 'updated_at' => '2025-06-01T12:00:00+00:00']; @@ -226,7 +226,7 @@ }); it('picks local when timestamps are equal', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $local = ['title' => 'Local', 'updated_at' => '2025-06-01T12:00:00+00:00']; $remote = ['title' => 'Remote', 'updated_at' => '2025-06-01T12:00:00+00:00']; @@ -237,7 +237,7 @@ }); it('picks local when both timestamps are empty', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $local = ['title' => 'Local']; $remote = ['title' => 'Remote']; @@ -248,7 +248,7 @@ }); it('picks remote when local timestamp is empty', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $local = ['title' => 'Local']; $remote = ['title' => 'Remote', 'updated_at' => '2025-06-01T12:00:00+00:00']; @@ -259,7 +259,7 @@ }); it('picks local when remote timestamp is empty', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $local = ['title' => 'Local', 'updated_at' => '2025-06-01T12:00:00+00:00']; $remote = ['title' => 'Remote']; @@ -270,9 +270,9 @@ }); }); -describe('OdinSyncService status', function (): void { +describe('RemoteSyncService status', function (): void { it('returns default status when no status file exists', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $status = $service->getStatus(); @@ -283,7 +283,7 @@ }); it('returns pending status when queue has items', function (): void { - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test', 'content' => 'Content']); $status = $service->getStatus(); @@ -301,7 +301,7 @@ 'last_error' => null, ])); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); // Add items to queue - this makes the queue non-empty $service->queueForSync(['id' => '1', 'title' => 'Test', 'content' => 'Content']); @@ -315,7 +315,7 @@ it('handles corrupted status file gracefully', function (): void { file_put_contents($this->tempDir.'/sync_status.json', 'not-valid-json'); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $status = $service->getStatus(); @@ -330,7 +330,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $service->queueForSync(['id' => '1', 'title' => 'Test', 'content' => 'Content']); $service->processQueue(); @@ -342,17 +342,17 @@ }); }); -describe('OdinSyncService connectivity', function (): void { +describe('RemoteSyncService connectivity', function (): void { it('reports unavailable when URL is empty', function (): void { - config(['services.odin.url' => '']); - $service = new OdinSyncService($this->pathService); + config(['services.remote.url' => '']); + $service = new RemoteSyncService($this->pathService); expect($service->isAvailable())->toBeFalse(); }); it('reports unavailable when token is empty', function (): void { - config(['services.odin.token' => '']); - $service = new OdinSyncService($this->pathService); + config(['services.remote.token' => '']); + $service = new RemoteSyncService($this->pathService); expect($service->isAvailable())->toBeFalse(); }); @@ -365,7 +365,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); expect($service->isAvailable())->toBeTrue(); @@ -380,7 +380,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); expect($service->isAvailable())->toBeFalse(); @@ -388,15 +388,15 @@ }); }); -describe('OdinSyncService pull', function (): void { +describe('RemoteSyncService pull', function (): void { it('returns empty when token is missing', function (): void { - config(['services.odin.token' => '']); - $service = new OdinSyncService($this->pathService); + config(['services.remote.token' => '']); + $service = new RemoteSyncService($this->pathService); - expect($service->pullFromOdin())->toBe([]); + expect($service->pullFromRemote())->toBe([]); }); - it('pulls entries from Odin', function (): void { + it('pulls entries from remote', function (): void { $entries = [ 'data' => [ ['title' => 'Entry 1', 'content' => 'Content 1'], @@ -411,8 +411,8 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); - $result = $service->pullFromOdin('myproject'); + $service = new RemoteSyncService($this->pathService); + $result = $service->pullFromRemote('myproject'); expect($result)->toHaveCount(2); expect($result[0]['title'])->toBe('Entry 1'); @@ -428,8 +428,8 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); - $result = $service->pullFromOdin(); + $service = new RemoteSyncService($this->pathService); + $result = $service->pullFromRemote(); expect($result)->toBe([]); @@ -444,8 +444,8 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); - $result = $service->pullFromOdin(); + $service = new RemoteSyncService($this->pathService); + $result = $service->pullFromRemote(); expect($result)->toBe([]); @@ -453,15 +453,15 @@ }); }); -describe('OdinSyncService listProjects', function (): void { +describe('RemoteSyncService listProjects', function (): void { it('returns empty when token is missing', function (): void { - config(['services.odin.token' => '']); - $service = new OdinSyncService($this->pathService); + config(['services.remote.token' => '']); + $service = new RemoteSyncService($this->pathService); expect($service->listProjects())->toBe([]); }); - it('returns projects list from Odin', function (): void { + it('returns projects list from remote', function (): void { $projects = [ 'data' => [ ['name' => 'project-a', 'entry_count' => 10, 'last_synced' => '2025-06-01'], @@ -476,7 +476,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $result = $service->listProjects(); expect($result)->toHaveCount(2); @@ -493,7 +493,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $result = $service->listProjects(); expect($result)->toBe([]); @@ -509,7 +509,7 @@ $mockClient = new Client(['handler' => $handlerStack]); app()->instance(Client::class, $mockClient); - $service = new OdinSyncService($this->pathService); + $service = new RemoteSyncService($this->pathService); $result = $service->listProjects(); expect($result)->toBe([]); diff --git a/tests/Unit/Services/ThemeClassifierServiceTest.php b/tests/Unit/Services/ThemeClassifierServiceTest.php index 034504b..aa9d07f 100644 --- a/tests/Unit/Services/ThemeClassifierServiceTest.php +++ b/tests/Unit/Services/ThemeClassifierServiceTest.php @@ -50,7 +50,7 @@ $entry = [ 'title' => 'Docker Deployment Configuration', 'content' => 'Configure Podman containers for homelab server with Tailscale networking', - 'tags' => ['docker', 'infrastructure', 'odin'], + 'tags' => ['docker', 'infrastructure', 'server'], ]; $result = $this->classifier->classify($entry);