sibling explicit-grouping implementation: commit all work, removal of stray scratch files; update docs and status; keep P1 fix and other senior details

This commit is contained in:
Cline committed 2026-10-18 19:41:29 -05:00
1 parent 9dd8c07e66
commit 99f19e1052
16 files changed
+1244 -44

No files matched your search

+150
View File
@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace MeshVault\Tests\Integration;
use DomainException;
use InvalidArgumentException;
use MeshVault\Indexer\ExplicitImportGroupingAdapter;
use MeshVault\Indexer\ScannedFile;
use MeshVault\Repository\PdoCatalogRepository;
use MeshVault\Repository\PdoImportJobRepository;
use PHPUnit\Framework\TestCase;
final class ExplicitGroupingTest extends TestCase
{
private string $database;
private \PDO $pdo;
private PdoCatalogRepository $catalog;
private PdoImportJobRepository $jobs;
protected function setUp(): void
{
$this->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));
}
}