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

+148 -20
View File
@@ -63,7 +63,7 @@ final class ImportPipelineTest extends TestCase
);
}
private function worker(?callable $shouldStop = null): ImportWorker
private function worker(?callable $shouldStop = null, array $operations = []): ImportWorker
{
$safety = new PathSafety($this->library);
$sourceSafety = new PathSafety($this->source);
@@ -71,7 +71,7 @@ final class ImportPipelineTest extends TestCase
$this->jobs,
new \MeshVault\Catalog\CatalogService($this->catalog),
new StreamingHasher($sourceSafety),
new VerifiedCopier($safety),
new VerifiedCopier($safety, $operations),
new StlValidator($sourceSafety),
$safety,
new SpaceChecker(),
@@ -121,10 +121,7 @@ final class ImportPipelineTest extends TestCase
self::assertStringStartsWith('CATALOG_CONFLICT:', $item['failure_reason']);
self::assertSame($other, (int) $this->catalog->file($id)['model_id']);
self::assertSame($sourceBefore, PhysicalFileSnapshot::capture($this->source));
self::assertSame(
$sourceBefore['planes/wing.stl']['bytes'],
file_get_contents($this->library . '/planes/wing.stl')
);
self::assertFileDoesNotExist($this->library . '/planes/wing.stl');
}
public function testCompletionRegistersBeforeUpdatingItemPath(): void
@@ -159,10 +156,97 @@ final class ImportPipelineTest extends TestCase
self::assertStringStartsWith('CATALOG_CONFLICT:', $item['failure_reason']);
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM files')->fetchColumn());
self::assertSame($sourceBefore, PhysicalFileSnapshot::capture($this->source));
self::assertSame($sourceBefore['loose.stl']['bytes'], file_get_contents($this->library . '/unfiled/loose.stl'));
self::assertFileDoesNotExist($this->library . '/unfiled/loose.stl');
}
public function testCompletionSqlFailureRollsBackCatalogPathAndProgressInsideCallerTransaction(): void
public static function invalidExplicitOwners(): array
{
return [
'deleted before execution' => [true, false, false],
'deleted before recovery' => [true, true, false],
'different manifest owner' => [false, false, true],
'missing historical owner with replacement' => [true, false, true],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('invalidExplicitOwners')]
public function testExplicitOwnershipFailsClosed(bool $delete, bool $recover, bool $replace): void
{
$source = $this->writeStl('part.stl');
$adapter = new \MeshVault\Indexer\ExplicitImportGroupingAdapter($this->catalog, $this->jobs);
$service = new ImportService(
new FileEnumerator(new LocalFilesystem(), new PathSafety($this->source)),
$this->jobs,
$adapter,
new SpaceChecker(),
$this->library,
);
$model = $this->catalog->createModel('/confirmed', 'Part');
$job = $service->import($this->source, ['stl'], [
'*' => ['confirmed' => true, 'model_id' => $model],
]);
$item = $this->jobs->items($job)[0];
$destination = $this->library . '/confirmed/part.stl';
if ($recover) {
$this->jobs->claimNext();
$this->jobs->planItem(
(int) $item['id'],
'confirmed/part.stl',
hash_file('sha256', $source),
filesize($source)
);
mkdir(dirname($destination));
copy($source, $destination);
unlink($source); // Recovery must validate ownership even without the source.
}
if ($delete) {
$this->pdo->exec('DELETE FROM models WHERE id = ' . $model);
self::assertNull($this->jobs->items($job)[0]['model_id']);
}
if ($replace) {
$other = $this->catalog->createModel('/replacement', 'Other');
$this->pdo->exec('UPDATE import_items SET model_id = ' . $other . ' WHERE id = ' . (int) $item['id']);
}
self::assertSame([$source => $model], $adapter->recoverDecisions($job));
$sourceBefore = PhysicalFileSnapshot::capture($this->source);
$this->worker()->runOnce();
$item = $this->jobs->items($job)[0];
self::assertSame('Failed', $item['status']);
self::assertStringStartsWith('CATALOG_CONFLICT:', $item['failure_reason']);
self::assertSame('Failed', $this->jobs->get($job)['status']);
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM files')->fetchColumn());
self::assertSame([$source => $model], $adapter->recoverDecisions($job));
self::assertSame($sourceBefore, PhysicalFileSnapshot::capture($this->source));
self::assertFileDoesNotExist($this->library . '/unfiled/part.stl');
self::assertFileDoesNotExist($this->library . '/replacement/part.stl');
if ($recover) {
self::assertSame($item['hash'], hash_file('sha256', $destination));
self::assertSame('confirmed/part.stl', $item['managed_path']);
} else {
self::assertNull($item['managed_path']);
self::assertFileDoesNotExist($destination);
}
}
public function testCallerTransactionIsRejectedBeforePublication(): void
{
$this->writeStl('planes/wing.stl');
$job = $this->service()->import($this->source);
$this->pdo->beginTransaction();
try {
$this->worker()->runOnce();
self::fail('Uncommitted publication intent accepted');
} catch (\LogicException $error) {
self::assertStringContainsString('transaction', $error->getMessage());
} finally {
self::assertTrue($this->pdo->inTransaction());
$this->pdo->rollBack();
}
self::assertSame('Pending', $this->jobs->items($job)[0]['status']);
self::assertFileDoesNotExist($this->library . '/planes/wing.stl');
}
public function testCompletionSqlFailureCompensatesOwnedSuffix(): void
{
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'sqlite') {
self::markTestSkipped('SQLite failure-injection trigger.');
@@ -176,27 +260,17 @@ final class ImportPipelineTest extends TestCase
// Fail after registration and item-path update, during progress refresh.
$this->pdo->exec("CREATE TRIGGER reject_completion BEFORE UPDATE ON import_jobs"
. " WHEN NEW.status = 'Completed' BEGIN SELECT RAISE(ABORT, 'injected completion failure'); END");
$this->pdo->beginTransaction();
$sentinel = $this->catalog->createModel('/sentinel', 'sentinel');
$this->worker()->runOnce();
self::assertTrue($this->pdo->inTransaction());
self::assertNotNull($this->catalog->aggregate($sentinel));
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM files')->fetchColumn());
$item = $this->jobs->items($job)[0];
self::assertSame('planes/wing.stl', $item['managed_path']);
self::assertSame('planes/wing_001.stl', $item['managed_path']);
self::assertSame('Failed', $item['status']);
self::assertStringStartsWith('COPY_FAILED:', $item['failure_reason']);
self::assertSame('Failed', $this->jobs->get($job)['status']);
self::assertSame($sourceBefore, PhysicalFileSnapshot::capture($this->source));
$published = PhysicalFileSnapshot::capture($this->library);
self::assertSame($libraryBefore['planes/wing.stl'], $published['planes/wing.stl']);
self::assertSame($sourceBefore['planes/wing.stl']['bytes'], $published['planes/wing_001.stl']['bytes']);
self::assertSame($sourceBefore['planes/wing.stl']['size'], $published['planes/wing_001.stl']['size']);
$this->pdo->rollBack();
self::assertSame($sourceBefore, PhysicalFileSnapshot::capture($this->source));
self::assertSame($published, PhysicalFileSnapshot::capture($this->library));
self::assertNull($this->catalog->aggregate($sentinel));
self::assertSame('Pending', $this->jobs->items($job)[0]['status']);
self::assertArrayNotHasKey('planes/wing_001.stl', $published);
}
public function testImportCreatesJobWithLegacyGuessedSubfolderGrouping(): void
@@ -300,6 +374,60 @@ final class ImportPipelineTest extends TestCase
self::assertCount(1, $this->catalog->aggregate((int) $item['model_id'])['files']);
}
public function testSuffixIntentIsCommittedBeforeLinkAndRecoveryNeedsNoSource(): void
{
$source = $this->writeStl('planes/wing.stl');
$job = $this->service()->import($this->source);
mkdir($this->library . '/planes');
file_put_contents($this->library . '/planes/wing.stl', 'occupied');
$observed = null;
$this->worker(null, ['link' => function (string $temp, string $dest) use ($job, &$observed): bool {
$reader = RepositoryDatabase::connect($this->database);
$observed = $reader->query('SELECT managed_path FROM import_items WHERE job_id = ' . $job)->fetchColumn();
return link($temp, $dest);
}])->runOnce();
self::assertSame('planes/wing_001.stl', $observed);
$this->pdo->exec("UPDATE import_items SET status = 'Copying', completed_at = NULL");
$this->pdo->exec("UPDATE import_jobs SET status = 'Copying', processed_files = 0");
$this->pdo->exec('DELETE FROM files');
unlink($source);
$this->worker()->runOnce();
self::assertSame('Completed', $this->jobs->items($job)[0]['status']);
self::assertFileDoesNotExist($this->library . '/planes/wing_002.stl');
}
public function testPublicationRaceFailsWithoutSilentSuffix(): void
{
$this->writeStl('planes/wing.stl');
$job = $this->service()->import($this->source);
$this->worker(null, ['link' => static function (string $temp, string $dest): bool {
file_put_contents($dest, 'race winner');
return false;
}])->runOnce();
self::assertSame('Failed', $this->jobs->items($job)[0]['status']);
self::assertSame('race winner', file_get_contents($this->library . '/planes/wing.stl'));
self::assertFileDoesNotExist($this->library . '/planes/wing_001.stl');
}
public function testAdoptedBytesSurviveCompletionFailure(): void
{
if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'sqlite') {
self::markTestSkipped('SQLite failure-injection trigger.');
}
$source = $this->writeStl('planes/wing.stl');
$job = $this->service()->import($this->source);
$item = $this->jobs->items($job)[0];
$this->jobs->planItem((int) $item['id'], 'planes/wing.stl', hash_file('sha256', $source), filesize($source));
mkdir($this->library . '/planes');
copy($source, $this->library . '/planes/wing.stl');
$this->pdo->exec("CREATE TRIGGER reject_adoption BEFORE UPDATE ON import_jobs"
. " WHEN NEW.status = 'Completed' BEGIN SELECT RAISE(ABORT, 'completion failure'); END");
$this->worker()->runOnce();
self::assertSame('Failed', $this->jobs->items($job)[0]['status']);
self::assertSame(file_get_contents($source), file_get_contents($this->library . '/planes/wing.stl'));
self::assertSame(0, (int) $this->pdo->query('SELECT COUNT(*) FROM files')->fetchColumn());
}
public function testGracefulStopLeavesJobResumable(): void
{
$this->writeStl('planes/wing.stl');