import assert from 'node:assert/strict'; import {readFileSync} from 'node:fs'; import {join} from 'node:path'; import {hashAt,frozenSnapshot} from './boot-contract-report.mjs'; import {filesIn} from './entry-extension.mjs'; export const threadWaitIntegrationTests=["real_frozen_stage_early_signal_aborts_before_table_sleep", "actual_frozen_wait_and_irq_ingress_resolve_once_after_fresh_landing", "same_tick_signal_wins_once_and_removes_both_links", "actual_timeout_wakes_exact_frozen_wait_without_signal", "two_actual_waits_cancel_one_without_touching_other_sleep_episode", "later_failure_preserves_queryable_earlier_committed_winner", "full_ingress_retries_count_each_distinct_observed_sequence_once", "producer_release_requires_owning_cpu_masked_task_and_retains_exact_ingress", "backend_release_requires_actual_owning_cpu_masked_task", "producer_clock_regression_is_rejected_even_without_new_signal", "backend_clock_behind_actual_producer_refuses_before_retain", "departure_and_landing_clock_regression_preserve_actual_handoff_custody", "before_stage_failure_returns_all_new_pins_and_keeps_real_thread_running", "failed_stage_rollback_keeps_actual_pins_and_private_lookup_in_quarantine", "before_depart_failure_retains_prepared_and_retry_sleeps_exact_thread", "uncertain_departure_keeps_actual_frozen_wait_and_pins", "source_epoch_change_cannot_relabel_or_resolve_existing_wait", "failed_terminal_release_retains_exact_owned_pins_without_second_wake", "dead_thread_refuses_wake_callback_and_retains_every_wait_pin", "earlier_queued_signal_does_not_wake_new_same_channel_generation", "actual_irq_or_wrong_cpu_arm_is_before_any_source_retain", "exhausted_wait_slot_refuses_before_new_real_pin_or_prepare", "rejected_configuration_returns_actual_exclusive_backend_and_stack_owners", "failed_pin_validation_rolls_back_before_context_preparation"]; export const threadWaitPrivateTests=["thread_wait::private_tests::stale_wait_lookup_cannot_cancel_new_same_channel_generation", "thread_wait::private_tests::same_desired_source_in_independent_sets_rejects_foreign_wait", "thread_wait::private_tests::generation_max_retires_slot_before_any_source_acquire", "thread_wait::private_tests::loss_counter_overflow_retains_exact_events_and_producer_progression", "thread_wait::private_tests::winner_counter_overflow_keeps_actual_staged_prepared_and_both_links"]; export const threadWaitBoundary='Host-only bounded thread wait custody and independent IRQ ingress. Twenty-four integration and five private tests execute the frozen Threads Table/Wait and AsyncBank prepare/depart/land/abort APIs through a source-bound host fixture, preserving nonclone source pins and exact sleep episodes. Register observations, condition progression, CPU/DAIF and clock in this fixture are model values, not actual CPU, assembly, physical pins, timer IRQ or guest measurements. Early source progression aborts Prepared while Table remains Running; no mutable set/bank loan spans the modeled continuation boundary. Actual-observer API gates, checked unique identity/generations, independent signal/deadline links, same-tick single winner, cancellation, partial queryable winners, bounded deduplicated ingress losses, release failure and uncertain handoff custody are covered. K1065/K1070/K1075/K1080 require canonical K1060 and later source/ELF/raw QMP guest proof; physical M1, SMP and general preemption are not accepted.'; export const threadWaitMutants=[ {id:'cpu-gate-bypassed',needle:'if execution.cpu != cpu {',replacement:'if false {',test:'actual_irq_or_wrong_cpu_arm_is_before_any_source_retain'}, {id:'irq-gate-bypassed',needle:'if execution.in_irq && operation != Operation::Produce {',replacement:'if false {',test:'actual_irq_or_wrong_cpu_arm_is_before_any_source_retain'}, {id:'mask-gate-bypassed',needle:'if !execution.irq_masked {',replacement:'if false {',test:'actual_irq_or_wrong_cpu_arm_is_before_any_source_retain'}, {id:'producer-release-gate-bypassed',needle:'if context(self.cpu, self.producer.execution(), Operation::Retire).is_ok()\n && self.count == 0',replacement:'if self.count == 0',test:'producer_release_requires_owning_cpu_masked_task_and_retains_exact_ingress'}, {id:'backend-release-gate-bypassed',needle:'if self.gate(Operation::Retire).is_ok() && self.slots.iter().all(|s| s.owned.is_none()) {',replacement:'if self.slots.iter().all(|s| s.owned.is_none()) {',test:'backend_release_requires_actual_owning_cpu_masked_task'}, {id:'full-retry-loss-duplicated',needle:'if self.last_lost_sequence != Some(o.sequence) {',replacement:'if true {',test:'full_ingress_retries_count_each_distinct_observed_sequence_once'}, {id:'producer-clock-regression',needle:'|| o.now < self.clock',replacement:'',test:'producer_clock_regression_is_rejected_even_without_new_signal'}, {id:'backend-clock-regression',needle:'|| initial.now < producer.now',replacement:'',test:'backend_clock_behind_actual_producer_refuses_before_retain'}, {id:'retained-clock-regression',needle:'|| o.now < r.clock',replacement:'',test:'departure_and_landing_clock_regression_preserve_actual_handoff_custody'}, {id:'identity-repeated',needle:'Ok(v) => return Ok(v),',replacement:'Ok(_) => return Ok(7),',test:'thread_wait::private_tests::same_desired_source_in_independent_sets_rejects_foreign_wait',unit:true}, {id:'foreign-set-bypassed',needle:'if token.set != self.identity {',replacement:'if false {',test:'thread_wait::private_tests::same_desired_source_in_independent_sets_rejects_foreign_wait',unit:true}, {id:'stale-generation-bypassed',needle:'slot.record.generation != token.generation || slot.owned.is_none()',replacement:'slot.owned.is_none()',test:'thread_wait::private_tests::stale_wait_lookup_cannot_cancel_new_same_channel_generation',unit:true}, {id:'generation-wrap',needle:'self.slots[i].record.generation.checked_add(1)',replacement:'Some(self.slots[i].record.generation.wrapping_add(1))',test:'thread_wait::private_tests::generation_max_retires_slot_before_any_source_acquire',unit:true}, {id:'early-signal-lost',needle:'let early = if o.ready || o.sequence > r.sequence {',replacement:'let early = if o.ready {',test:'real_frozen_stage_early_signal_aborts_before_table_sleep'}, {id:'same-tick-wrong-winner',needle:'let outcome = if signal {\n Some(Outcome::Signal)\n } else if r.deadline.is_some_and(|d| observed.now >= d) {\n Some(Outcome::Timeout)',replacement:'let outcome = if r.deadline.is_some_and(|d| observed.now >= d) {\n Some(Outcome::Timeout)\n } else if signal {\n Some(Outcome::Signal)',test:'same_tick_signal_wins_once_and_removes_both_links'}, {id:'signal-link-retained',needle:'r.signal_link = false;',replacement:'r.signal_link = true;',test:'same_tick_signal_wins_once_and_removes_both_links'}, {id:'deadline-link-retained',needle:'r.deadline_link = false;',replacement:'r.deadline_link = true;',test:'same_tick_signal_wins_once_and_removes_both_links'}, {id:'uncertain-handoff-not-quarantined',needle:'if f.phase == FailurePhase::After {',replacement:'if false {',all:3,test:'uncertain_departure_keeps_actual_frozen_wait_and_pins'}, {id:'failed-return-custody-discarded',needle:'self.slots[i].owned = Some(owned);',replacement:'let _discarded = owned;',all:2,test:'failed_stage_rollback_keeps_actual_pins_and_private_lookup_in_quarantine'}, {id:'live-slot-overwritten',needle:'.position(|s| s.record.phase == Phase::Empty)',replacement:'.position(|_| true)',test:'exhausted_wait_slot_refuses_before_new_real_pin_or_prepare'}, {id:'loss-overflow-wrap',needle:'self.lost.checked_add(1).ok_or(Error::Exhausted)?',replacement:'self.lost.wrapping_add(1)',test:'thread_wait::private_tests::loss_counter_overflow_retains_exact_events_and_producer_progression',unit:true}, {id:'winner-overflow-wrap',needle:'self.winners.checked_add(1).ok_or(Error::Exhausted)?',replacement:'self.winners.wrapping_add(1)',test:'thread_wait::private_tests::winner_counter_overflow_keeps_actual_staged_prepared_and_both_links',unit:true}, ]; const directories=['os-thread-wait','os-threads','os-exceptions','os-context-switch','os-context-handoff']; const scripts=['thread-wait-host.mjs','thread-wait-host-report.mjs','thread-wait-test-host-report.mjs']; const extra=['.cargo/config.toml','scripts/os-acceptance/k1040-context-handoff.rs']; export const threadWaitInputs=root=>[...directories.flatMap(d=>filesIn(root,d)),...extra,...scripts.map(p=>'scripts/os-acceptance/'+p)].sort().map(path=>({path,sha256:hashAt(root,path)})); export function threadWaitFixture(root){let s=readFileSync(join(root,'os-context-handoff/tests/handoff.rs'),'utf8');s=s.slice(0,s.indexOf('#[test]'));const replacements=[[' cpu: Rc>,\n}', ' cpu: Rc>,\n waits: Rc; 4]>>,\n}'],['self.inner.sleep_wait = Some(', 'self.waits.borrow_mut()[source.id.slot()] = Some('],['[Handle; 2]', '[Handle; 3]'],['saved::backend(2)', 'saved::backend(3)'],[' cpu: cpu.clone(),\n })',' cpu: cpu.clone(),\n waits: Rc::new(RefCell::new(core::array::from_fn(|_| None))),\n })'],[' let b = handles.next().unwrap();\n (AsyncBank::new(bank).ok().unwrap(), [a, b], h, cpu)', ' let b = handles.next().unwrap();\n let c = handles.next().unwrap();\n (AsyncBank::new(bank).ok().unwrap(), [a, b, c], h, cpu)']];for(const[n,r]of replacements){assert.equal(s.split(n).length,2,n);s=s.replace(n,r);}return'// Host-only source-bound copy of the frozen asynchronous handoff fixture prefix.\n// Real frozen Table/AsyncBank APIs execute; no assembly or actual CPU is claimed.\n'+s;} export function threadWaitSources(root,mutant=null){assert.equal(readFileSync(join(root,'os-thread-wait/tests/handoff_fixture.rs'),'utf8'),threadWaitFixture(root));const map=new Map([...directories.flatMap(d=>filesIn(root,d)),...extra].sort().map(path=>[path,readFileSync(join(root,path))]));if(mutant){const p='os-thread-wait/src/thread_wait.rs',s=map.get(p).toString();assert.equal(s.split(mutant.needle).length,(mutant.all??1)+1,mutant.id);map.set(p,Buffer.from(mutant.all?s.replaceAll(mutant.needle,mutant.replacement):s.replace(mutant.needle,mutant.replacement)));}return map;} export function inspectThreadWaitHost(report,root){ assert.equal(report.schema,'turkos-thread-wait-host/v1');assert.equal(report.status,'HOST_PREPARED');assert.equal(report.guestAccepted,false);assert.equal(report.boundary,threadWaitBoundary);assert.equal(report.hostTests,29);assert.equal(report.arm64Status,'CHECK_ONLY');assert.deepEqual(report.pendingGuests,[1065,1070,1075,1080]);assert.deepEqual(report.localMoves,Array.from({length:20},(_,i)=>1061+i).filter(n=>n%5));assert.match(report.runDirectory,/^reports\/os-implementation\/K1061-K1079-preparation-[A-Za-z0-9_-]+$/);assert.deepEqual(report.inputsBefore,threadWaitInputs(root));assert.deepEqual(report.inputsAfter,report.inputsBefore);assert.deepEqual(report.protectedBefore,frozenSnapshot(root));assert.deepEqual(report.protectedAfter,report.protectedBefore);assert.match(report.hostTarget,/^aarch64-apple-darwin$/); const read=a=>{assert.ok(a.path.startsWith(report.runDirectory+'/')&&!a.path.split('/').some(p=>p==='.'||p==='..'||p===''));assert.equal(hashAt(root,a.path),a.sha256);return readFileSync(join(root,a.path));}; function sources(artifacts,expected,prefix){assert.deepEqual(artifacts.map(a=>a.originalPath),[...expected.keys()]);assert.deepEqual(filesIn(root,prefix).sort(),artifacts.map(a=>a.path).sort());for(const a of artifacts){assert.equal(a.path,prefix+'/'+a.originalPath);assert.deepEqual(read(a),expected.get(a.originalPath));}} sources(report.sources,threadWaitSources(root),report.runDirectory+'/sources');assert.deepEqual(report.commands.map(c=>c.id),['integration','private','arm64','format','clippy']); const args=(prefix,target,unit=false)=>['--manifest-path',`/${prefix}/os-thread-wait/Cargo.toml`,'--locked','--offline',...(unit?['--lib']:['--test','wait']),'--target',target]; for(const[i,c]of report.commands.entries()){ assert.equal(c.executable,i===3?'rustfmt':'cargo');assert.equal(c.exitCode,0);assert.equal(c.targetDirectory,`/${report.runDirectory}/target`); const prefix=`/${report.runDirectory}/sources`;const expected=i===0||i===1?['test',...args(report.runDirectory+'/sources',report.hostTarget,i===1),'--','--test-threads=1']:i===2?['check','--manifest-path',prefix+'/os-thread-wait/Cargo.toml','--locked','--offline','--lib','--target','aarch64-unknown-none']:i===3?['--config','skip_children=true','--edition','2024','--check',...[...threadWaitSources(root).keys()].filter(p=>p.startsWith('os-thread-wait/')&&p.endsWith('.rs')).map(p=>prefix+'/'+p)]:['clippy','--manifest-path',prefix+'/os-thread-wait/Cargo.toml','--locked','--offline','--all-targets','--target',report.hostTarget,'--','-D','warnings'];assert.deepEqual(c.argv,expected);const log=read(c.log).toString();assert.doesNotMatch(log,/could not compile|error\[E\d+\]/); if(i<2){const tests=i?threadWaitPrivateTests:threadWaitIntegrationTests;assert.match(log,new RegExp(`running ${tests.length} tests`));assert.ok(log.includes(`test result: ok. ${tests.length} passed; 0 failed`));for(const n of tests)assert.ok(log.includes(`test ${n} ... ok`),n);}else if(i!==3)assert.match(log,/Finished `dev` profile/); } assert.equal(report.mutants.length,threadWaitMutants.length);for(const[i,m]of report.mutants.entries()){const spec=threadWaitMutants[i];for(const[k,v]of Object.entries(spec))assert.deepEqual(m[k],v);assert.equal(m.executable,'cargo');assert.equal(m.exitCode,101);assert.equal(m.targetDirectory,`/${report.runDirectory}/target`);const prefix=`${report.runDirectory}/mutant-${spec.id}/sources`;sources(m.sources,threadWaitSources(root,spec),prefix);assert.equal(m.source.path,prefix+'/os-thread-wait/src/thread_wait.rs');assert.deepEqual(read(m.source),threadWaitSources(root,spec).get('os-thread-wait/src/thread_wait.rs'));assert.deepEqual(m.argv,['test',...args(prefix,report.hostTarget,spec.unit),spec.test,'--','--exact','--test-threads=1']);const log=read(m.log).toString();assert.doesNotMatch(log,/could not compile|error\[E\d+\]|signal: \d/);assert.match(log,/running 1 test/);assert.match(log,/test result: FAILED\. 0 passed; 1 failed/);assert.ok(log.includes(`test ${spec.test} ... FAILED`));assert.ok(log.includes('panicked at'));} return{hostTests:29,compiledKilledMutants:threadWaitMutants.length,guestAccepted:false,pendingGuests:report.pendingGuests,realFrozenThreadsAndAsyncBankApis:true,actualCpuOrAssemblyMeasured:false}; }