← Files HA Interaction AuditARCHIVED FILE

skills/ha-interaction-audit/assets/harness/fault-controller.mjs

3.92 KB · Oct 3, 2026 · 06:34 UTC

↓ Download file

// Fixture-only response scheduling. No networking and no wall-clock sleeps.
export function createFaultController() {
  const rules = [], pending = new Map(), events = [];
  const modes = new Set(['hold-before','hold-after','reject-before','commit-then-reject']);
  let serial = 0, disposed = false;
  const failure = code => Object.assign(new Error('Injected fixture failure: '+code), {code});
  const emit = (entry, phase) => events.push({id:entry.id, surface:entry.surface,
    key:entry.key, phase, committed:entry.committed});
  const commitOnce = async entry => {
    if (entry.started) throw new Error('Contract execution already started');
    if (disposed) throw failure('fixture_disposed');
    entry.started = true;
    emit(entry, 'commit-start');
    try {
      entry.value = await entry.commit();
      entry.committed = true;
      emit(entry, 'commit-complete');
    } catch (error) {
      // A throwing handler might already have made a partial change.
      entry.committed = 'unknown';
      emit(entry, 'commit-error');
      throw error;
    }
  };
  const settle = async (entry, outcome, code) => {
    try {
      if (outcome !== 'reject' && !entry.started) await commitOnce(entry);
      if (disposed) throw failure('fixture_disposed');
      if (outcome !== 'fulfill') throw failure(code);
      emit(entry, 'response-fulfilled');
      entry.resolve(entry.value);
    } catch (error) {
      emit(entry, 'response-rejected');
      entry.reject(error);
    } finally { pending.delete(entry.id); }
  };
  return {
    arm({surface, key, mode, code = 'simulated_failure'}) {
      if (disposed) throw new Error('Fault controller disposed');
      if (!surface || !key || !modes.has(mode) || typeof code !== 'string' || !code)
        throw new Error('Fault requires an exact surface/key, supported mode and code');
      rules.push({surface,key,mode,code});
    },
    async execute({surface,key}, commit) {
      if (disposed) throw new Error('Fault controller disposed');
      if (typeof commit !== 'function') throw new Error('Explicit contract callback required');
      const entry = {id:++serial,surface,key,commit,started:false,committed:false};
      const index = rules.findIndex(r=>r.surface===surface && r.key===key);
      const rule = index < 0 ? null : rules.splice(index,1)[0];
      emit(entry, 'request');
      if (!rule) { await commitOnce(entry); emit(entry,'response-fulfilled'); return entry.value; }
      if (rule.mode==='reject-before') { emit(entry,'response-rejected'); throw failure(rule.code); }
      if (rule.mode==='commit-then-reject') {
        await commitOnce(entry); emit(entry,'response-rejected'); throw failure(rule.code);
      }
      return new Promise((resolve,reject) => {
        Object.assign(entry,{resolve,reject,ready:rule.mode==='hold-before',code:rule.code});
        pending.set(entry.id,entry);
        if (rule.mode==='hold-before') emit(entry,'held-before');
        else commitOnce(entry).then(()=>{
          if (disposed) return;
          entry.ready=true; emit(entry,'held-after');
        }, error=>{
          emit(entry,'response-rejected'); pending.delete(entry.id); reject(error);
        });
      });
    },
    async release(id, {outcome='fulfill',code}={}) {
      if (!['fulfill','reject','commit-then-reject'].includes(outcome)) throw new Error('Invalid release outcome');
      const entry = pending.get(id);
      if (!entry || !entry.ready) throw new Error('Request is absent, already released, or not ready');
      entry.ready=false;
      await settle(entry,outcome,code || entry.code);
    },
    snapshot() {
      return {pending:[...pending.values()].map(({id,surface,key,ready,committed})=>({id,surface,key,ready,committed})),
        unconsumedRules:rules.length, events:structuredClone(events)};
    },
    dispose() {
      disposed=true; rules.length=0;
      for (const entry of pending.values()) {emit(entry,'disposed'); entry.reject(failure('fixture_disposed'));}
      pending.clear();
    }
  };
}

SHA-256: 75277c3d350682371036feddb0766fb4cb8af13515919e3563ef3728d44a871a