database = tempnam(sys_get_temp_dir(), 'grouping-'); $this->pdo = RepositoryDatabase::migrate($this->database); $this->catalog = new PdoCatalogRepository($this->pdo); $this->jobs = new PdoImportJobRepository($this->pdo); self::assertTrue(class_exists(ExplicitImportGroupingAdapter::class), 'Explicit adapter must exist'); } protected function tearDown(): void { RepositoryDatabase::cleanup($this->database); } public function testServicePersistsPerFileAndWildcardDecisionsAtomically(): void { $source = RepositoryDatabase::directory('explicit-source'); $library = RepositoryDatabase::directory('explicit-library'); try { file_put_contents($source . '/a.stl', 'solid a'); file_put_contents($source . '/b.stl', 'solid b'); $adapter = new ExplicitImportGroupingAdapter($this->catalog, $this->jobs); $service = new \MeshVault\Indexer\ImportService( new \MeshVault\Indexer\FileEnumerator( new \MeshVault\Indexer\Filesystem\LocalFilesystem(), new \MeshVault\Indexer\Filesystem\PathSafety($source) ), $this->jobs, $adapter, new \MeshVault\Indexer\SpaceChecker(), $library ); $context = ['*' => ['confirmed' => true, 'folder_path' => '/parts', 'model_name' => 'Part']]; $job = $service->import($source, ['stl'], $context); $map = $adapter->recoverDecisions($job); self::assertCount(2, $map); self::assertSame($map, $adapter->recoverDecisions($service->import($source, ['stl'], $context))); $before = (int) $this->pdo->query('SELECT count(*) FROM import_jobs')->fetchColumn(); try { $service->import($source, ['stl'], ['a.stl' => $context['*']]); self::fail('Incomplete grouping accepted'); } catch (\MeshVault\Indexer\ImportException) { self::assertSame($before, (int) $this->pdo->query('SELECT count(*) FROM import_jobs')->fetchColumn()); } } finally { RepositoryDatabase::remove($source); RepositoryDatabase::remove($library); } } private function resolve(array $context): int { return (new ExplicitImportGroupingAdapter($this->catalog, $this->jobs))->modelFor( '/source', new ScannedFile('/source/a.stl', 'a.stl', 1, 'model/stl'), $context ); } public function testConfirmationAndCompleteContextAreRequired(): void { foreach ( [[], ['confirmed' => true], ['confirmed' => 1, 'model_id' => 1], ['confirmed' => true, 'folder_path' => '/x'], ['confirmed' => true, 'model_id' => '1']] as $context ) { try { $this->resolve($context); self::fail('Malformed or unconfirmed context accepted'); } catch (InvalidArgumentException) { self::assertSame(0, (int) $this->pdo->query('SELECT count(*) FROM models')->fetchColumn()); } } } public function testStableIdentityNewIdentityAndAmbiguity(): void { $context = ['confirmed' => true, 'folder_path' => '/parts', 'model_name' => 'Part']; $id = $this->resolve($context); self::assertSame($id, $this->resolve($context)); self::assertSame($id, $this->resolve(['model_id' => $id] + $context)); self::assertNotSame($id, $this->resolve(['create_new' => true] + $context)); $this->expectException(DomainException::class); $this->resolve($context); } public function testIdentityMismatchAndMissingModelAreRejected(): void { $id = $this->catalog->createModel('/parts', 'Part'); foreach ( [['model_id' => $id, 'model_name' => 'Other'], ['model_id' => $id, 'folder_path' => '/other'], ['model_id' => 999999]] as $context ) { try { $this->resolve(['confirmed' => true] + $context); self::fail('Conflicting identity accepted'); } catch (DomainException) { self::assertTrue(true); } } } public function testDurableImmutableIdempotentDecisionsAndCallerRollback(): void { $id = $this->resolve(['confirmed' => true, 'folder_path' => '/parts', 'model_name' => 'Part']); $job = $this->jobs->create('/source', [['source_path' => '/source/a.stl', 'size' => 1, 'model_id' => $id]]); $adapter = new ExplicitImportGroupingAdapter($this->catalog, $this->jobs); $decision = ['source_path' => '/source/a.stl', 'model_id' => $id, 'context' => ['confirmed' => true]]; $adapter->persistDecisions($job, [$decision]); $adapter->persistDecisions($job, [$decision]); $fresh = new ExplicitImportGroupingAdapter( $this->catalog, new PdoImportJobRepository(RepositoryDatabase::connect($this->database)) ); self::assertSame(['/source/a.stl' => $id], $fresh->recoverDecisions($job)); try { $changed = array_replace($decision, ['context' => ['confirmed' => true, 'extra' => 1]]); $adapter->persistDecisions($job, [$changed]); self::fail('Decision changed'); } catch (DomainException) { self::assertSame(['/source/a.stl' => $id], $adapter->recoverDecisions($job)); } $this->pdo->beginTransaction(); $other = $this->jobs->create('/other', []); $adapter->persistDecisions($other, [$decision]); self::assertTrue($this->pdo->inTransaction()); $this->pdo->rollBack(); self::assertSame([], $adapter->recoverDecisions($other)); } }