setConstraint('http:aria2', '^1.2'); $m->coreConstraint = '>=1.1'; $m->save($dir); $loaded = ProjectManifest::load($dir); eq('^1.2', $loaded->constraint('http:aria2')); eq('>=1.1', $loaded->coreConstraint); rrmdir($dir); }); Harness::it('lock guarda core y componentes con schema esperado', function () { $dir = tmpdir(); $file = $dir . '/src/Libs/Aria2.php'; write_file($file, "setCore(['version' => '1.5.0', 'commit' => 'abc', 'files' => ['src/Libs/Router.php' => 'sha256:x']]); $lock->setComponent('http:aria2', [ 'package' => 'http', 'version' => '1.2.0', 'toolbox_commit' => 'D', 'mode' => 'copy', 'files' => ['src/Libs/Aria2.php' => Drift::hash($file)], ]); $lock->save($dir); $loaded = Lock::load($dir); eq('1.5.0', $loaded->core()['version']); eq('http', $loaded->component('http:aria2')['package']); eq('copy', $loaded->component('http:aria2')['mode']); rrmdir($dir); }); Harness::it('ownership detecta colisión de rutas', function () { $lock = Lock::empty(); $lock->setCore(['files' => ['src/Libs/X.php' => 'sha256:a']]); $lock->setComponent('a:b', ['package' => 'a', 'version' => '1', 'toolbox_commit' => null, 'mode' => 'copy', 'files' => ['src/Libs/X.php' => 'sha256:b']]); $ownership = Ownership::fromLock($lock); ok($ownership->collisions() !== [], 'debía detectar colisión'); throws(fn () => $ownership->assertNoCollisions()); }); Harness::it('ownership separa core y componentes sin colisión', function () { $lock = Lock::empty(); $lock->setCore(['files' => ['src/Libs/Router.php' => 'sha256:a']]); $lock->setComponent('a:b', ['package' => 'a', 'version' => '1', 'toolbox_commit' => null, 'mode' => 'copy', 'files' => ['src/Libs/B.php' => 'sha256:b']]); $ownership = Ownership::fromLock($lock); eq('core', $ownership->ownerOf('src/Libs/Router.php')); eq('a:b', $ownership->ownerOf('src/Libs/B.php')); $ownership->assertNoCollisions(); }); Harness::it('drift distingue match, drifted y missing', function () { $dir = tmpdir(); $file = $dir . '/f.php'; write_file($file, 'original'); $hash = Drift::hash($file); eq(Drift::MATCH, Drift::status($file, $hash)); write_file($file, 'editado'); eq(Drift::DRIFTED, Drift::status($file, $hash)); eq(Drift::MISSING, Drift::status($dir . '/nope.php', $hash)); rrmdir($dir); }); Harness::it('destino aplica la regla src y fuera de src', function () { $rootDir = '/proj'; $rootCore = '/proj/src'; eq('src/Libs/Crypto.php', Destination::relative('src/Libs/Crypto.php', $rootDir, $rootCore)); eq('duckbrain', Destination::relative('duckbrain', $rootDir, $rootCore)); eq('/proj/src/Libs/Crypto.php', Destination::resolve('src/Libs/Crypto.php', $rootDir, $rootCore)); eq('/proj/duckbrain', Destination::resolve('duckbrain', $rootDir, $rootCore)); eq('custom/Libs/Crypto.php', Destination::relative('src/Libs/Crypto.php', $rootDir, '/proj/custom')); }); Harness::it('destino explícito sobreescribe la regla', function () { $dest = Destination::relative('src/Libs/Crypto.php', '/proj', '/proj/src', ['src/Libs/Crypto.php' => 'elsewhere/Crypto.php']); eq('elsewhere/Crypto.php', $dest); });