write('targets.json', [ 'first' => ['id'=>'first','domain'=>'first.example','type'=>'sftp','host'=>'unreachable.invalid','root'=>'/not-mounted','secret'=>'fixture-only-secret'], 'second' => ['id'=>'second','domain'=>'second.example','type'=>'local','root'=>'/not-mounted'], ]); $runtimeFile = $testRoot . '/runtime-fixture.json'; file_put_contents($runtimeFile, '{"host":"unchanged.example","utm":{"indpic":{"default":"1"}}}'); $runtimeBefore = hash_file('sha256', $runtimeFile); $builder = new LinkBuilder($linkStore); $empty = ['selection'=>null, 'url'=>null, 'saved_at'=>null]; linkCheck($builder->get('first') === $empty, 'first opening has no implicit saved choice or generated URL'); $selection = ['indpic'=>'9','theme'=>'2','degr'=>'3','reg'=>'1','banch'=>'rnd','tp'=>'4','cons'=>'5','focus'=>'rnd','part'=>'rnd']; $saved = $builder->save('first', $selection); linkCheck($saved['selection'] === $selection && is_string($saved['saved_at']), 'explicit save returns complete independent selection and timestamp'); linkCheck(str_starts_with($saved['url'], 'https://first.example/?'), 'URL uses saved target HTTPS origin'); parse_str(parse_url($saved['url'], PHP_URL_QUERY), $parameters); linkCheck(array_keys($parameters) === array_keys($selection), 'URL contains all nine canonical UTM keys'); foreach ($selection as $key=>$value) { $digit = $value === 'rnd' ? '0' : $value; linkCheck((bool)preg_match('/^[a-zA-Z0-9]{4}' . $digit . '[a-zA-Z0-9]+$/D', $parameters[$key]), "{$key} has four ASCII prefix characters, selected digit and noise suffix"); } linkCheck((new LinkBuilder(new Store($testRoot)))->get('first') === $saved, 'reload preserves exact saved URL and selection without regenerating noise'); $regenerated = $builder->save('first', $selection); linkCheck($regenerated['selection']===$saved['selection']&&$regenerated['url']!==$saved['url'],'only explicit generation refreshes random noise for unchanged selection'); $saved = $regenerated; linkCheck($builder->get('second') === $empty, 'other target does not inherit saved selection'); $modified = $saved['selection']; $modified['theme'] = '1'; $updated = $builder->save('first', $modified); linkCheck($updated['selection']['theme']==='1' && array_diff_assoc($updated['selection'], $selection)===['theme'=>'1'], 'changing one selector preserves all other saved choices'); $builder->save('second', array_fill_keys(array_keys($selection), 'rnd')); linkCheck($builder->get('first') === $updated, 'saving another target does not overwrite first target state'); $targets = $linkStore->read('targets.json');$targets['first']['domain']='renamed.example';$linkStore->write('targets.json',$targets); $renamed = $builder->get('first'); linkCheck(str_starts_with($renamed['url'],'https://renamed.example/?') && !str_contains($renamed['url'],'first.example'), 'domain rename rebases saved URL to current origin'); linkCheck($renamed['selection']===$modified && $renamed['saved_at']===$updated['saved_at'] && parse_url($renamed['url'],PHP_URL_QUERY)===parse_url($updated['url'],PHP_URL_QUERY), 'domain rename preserves selection, timestamp and existing query noise'); foreach (array_diff(array_keys($selection),['part']) as $key) { $manualSelection=array_fill_keys(array_keys($selection),'rnd');$manualSelection[$key]='9'; $manual=$builder->save('second',$manualSelection);parse_str(parse_url($manual['url'],PHP_URL_QUERY),$manualParameters); linkCheck($builder->get('second')===$manual&&$manual['selection'][$key]==='9'&&$manualParameters[$key][4]==='9','manual digit 9 encodes and persists for '.$key); } foreach ([[], array_diff_key($selection,['focus'=>true]), $selection+['unexpected'=>'1'], array_replace($selection,['theme'=>'10']), array_replace($selection,['indpic'=>'0']), array_replace($selection,['cons'=>1]), array_replace($selection,['tp'=>'rnd&host=evil']), array_replace($selection,['reg'=>['1']])] as $invalid) { linkReject(fn()=>$builder->save('first',$invalid),'strict selection rejects missing, unknown, wrong-type or injected value'); } linkReject(fn()=>$builder->get('../targets'),'target traversal rejected'); linkReject(fn()=>$builder->get('missing'),'unknown target rejected'); foreach (['evil.example/path','user@evil.example','https://evil.example','evil.example:443','bad..example','-bad.example',"evil.example\nX: bad"] as $domain) { $targets['first']['domain']=$domain;$linkStore->write('targets.json',$targets); linkReject(fn()=>$builder->get('first'),'unsafe registry domain rejected instead of composing executable URL'); } linkCheck(hash_file('sha256',$runtimeFile)===$runtimeBefore && glob($testRoot.'/jobs/*.json')===[], 'link builder leaves runtime bytes unchanged and creates no jobs'); linkCheck((fileperms($testRoot.'/link-builder-first.json') & 0777)===0600,'saved selection and URL remain in private 0600 state'); echo "Link builder contract complete\n";