/**
 * Phase 4 shell tools tests — Tests #103-131
 *
 * Tests ShellEffectBackend registration, individual tool effects,
 * security, composition, and shell prelude sugar functions.
 */
import { describe, it, before } from 'node:test';
import assert from 'node:assert/strict';
import { MembraneStepper } from '../MembraneStepper.js';
import { ShellEffectBackend, type ShellDelegate } from '../ShellEffectBackend.js';
import type { Expr, Val } from '@open-matrix/omega-core';

// ─── Mock delegate ───────────────────────────────────────────────────────────

function createMockDelegate(overrides: Partial<ShellDelegate> = {}): ShellDelegate & { calls: Array<{ method: string; args: unknown[] }> } {
  const calls: Array<{ method: string; args: unknown[] }> = [];
  return {
    calls,
    isInSubtree: overrides.isInSubtree ?? ((target) => {
      calls.push({ method: 'isInSubtree', args: [target] });
      return true; // allow all by default
    }),
    executeInvoke: overrides.executeInvoke ?? (async (target, op, payload) => {
      calls.push({ method: 'executeInvoke', args: [target, op, payload] });
      return { ok: true, data: { invoked: true, target, op } };
    }),
    executeEmit: overrides.executeEmit ?? (async (event, payload) => {
      calls.push({ method: 'executeEmit', args: [event, payload] });
      return { ok: true, data: { emitted: event } };
    }),
    executeRespond: overrides.executeRespond ?? (async (text) => {
      calls.push({ method: 'executeRespond', args: [text] });
      return { ok: true, data: { responded: text } };
    }),
    executeDefineComponent: overrides.executeDefineComponent ?? (async (name, code) => {
      calls.push({ method: 'executeDefineComponent', args: [name, code] });
      return { ok: true, data: { defined: name } };
    }),
    executeLoadComponent: overrides.executeLoadComponent ?? (async (name) => {
      calls.push({ method: 'executeLoadComponent', args: [name] });
      return { ok: true, data: { loaded: name } };
    }),
    executeSaveComponent: overrides.executeSaveComponent ?? (async (name, code) => {
      calls.push({ method: 'executeSaveComponent', args: [name, code] });
      return { ok: true, data: { saved: name } };
    }),
    executeListComponents: overrides.executeListComponents ?? (async () => {
      calls.push({ method: 'executeListComponents', args: [] });
      return { ok: true, data: { components: ['comp1', 'comp2'] } };
    }),
    executeEvalLisp: overrides.executeEvalLisp ?? (async (code) => {
      calls.push({ method: 'executeEvalLisp', args: [code] });
      return { ok: true, data: { result: 'eval-ok' } };
    }),
    executeFindComponents: overrides.executeFindComponents ?? (async (query) => {
      calls.push({ method: 'executeFindComponents', args: [query] });
      return { ok: true, data: [] };
    }),
    executeGetComponentSource: overrides.executeGetComponentSource ?? (async (name) => {
      calls.push({ method: 'executeGetComponentSource', args: [name] });
      return { ok: true, data: 'source' };
    }),
    executePublishComponent: overrides.executePublishComponent ?? (async (name) => {
      calls.push({ method: 'executePublishComponent', args: [name] });
      return { ok: true, data: {} };
    }),
    executeInstallComponent: overrides.executeInstallComponent ?? (async (name) => {
      calls.push({ method: 'executeInstallComponent', args: [name] });
      return { ok: true, data: {} };
    }),
    executeResolveIntent: overrides.executeResolveIntent ?? (async (text) => {
      calls.push({ method: 'executeResolveIntent', args: [text] });
      return { ok: true, data: {} };
    }),
    executeTraceQuery: overrides.executeTraceQuery ?? (async (query) => {
      calls.push({ method: 'executeTraceQuery', args: [query] });
      return { ok: true, data: [] };
    }),
    executeReplPackages: overrides.executeReplPackages ?? (async () => {
      calls.push({ method: 'executeReplPackages', args: [] });
      return { ok: true, data: [] };
    }),
    executeReplDoc: overrides.executeReplDoc ?? (async (symbol) => {
      calls.push({ method: 'executeReplDoc', args: [symbol] });
      return { ok: true, data: '' };
    }),
    executeForkSession: overrides.executeForkSession ?? (async (newSessionId, allowedEffects) => {
      calls.push({ method: 'executeForkSession', args: [newSessionId, allowedEffects] });
      return { ok: true, data: { sessionId: newSessionId } };
    }),
    isEffectAllowed: overrides.isEffectAllowed,
  };
}

/** Create a stepper with ShellEffectBackend */
function createShellStepper(delegate: ShellDelegate) {
  const backend = new ShellEffectBackend(delegate);
  return MembraneStepper.create([backend]);
}

// ═══════════════════════════════════════════════════════════════════════════
// ShellEffectBackend registration (#103-104)
// ═══════════════════════════════════════════════════════════════════════════

describe('ShellEffectBackend registration', () => {
  it('#103 registers all shell.* effects', () => {
    const delegate = createMockDelegate();
    const backend = new ShellEffectBackend(delegate);
    const expectedOps: Array<(typeof backend.ops)[number]> = [
      'shell.invoke', 'shell.emit', 'shell.respond',
      'shell.define-component', 'shell.load-component',
      'shell.save-component', 'shell.list-components',
      'shell.eval', 'shell.introspect',
    ];
    for (const op of expectedOps) {
      assert.ok(backend.ops.includes(op), `Missing op: ${op}`);
    }
  });

  it('#104 each effect name maps to correct handler', () => {
    const delegate = createMockDelegate();
    const backend = new ShellEffectBackend(delegate);
    assert.strictEqual(backend.ops.length, 21, 'Should have exactly 21 ops (17 tools + shell.ls + shell.spawn + shell.kill + shell.fork)');
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Individual tool effects (#105-115)
// ═══════════════════════════════════════════════════════════════════════════

describe('Shell tool effects', () => {
  let delegate: ReturnType<typeof createMockDelegate>;
  let stepper: MembraneStepper;

  before(() => {
    delegate = createMockDelegate();
    stepper = createShellStepper(delegate);
  });

  it('#105 shell.invoke dispatches via delegate → returns result', async () => {
    const program = stepper.compile('(effect shell.invoke "chat.messages" "messages.add" "{\\"text\\":\\"hello\\"}")');
    // shell.invoke calls delegate.executeInvoke
    // But MembraneStepper.run expects membrane result from collector...
    // Actually, shell effects are NOT membrane effects. They return vals to the Lisp program.
    // We need to test via a program that also calls membrane.allow after the shell effect.
    const prog = stepper.compile(
      '(begin (effect shell.invoke "target" "$ping" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(invokeCall, 'executeInvoke should have been called');
    assert.deepStrictEqual(invokeCall!.args, ['target', '$ping', {}]);
  });

  it('#106 shell.emit publishes fire-and-forget', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.emit "tick" "{\\"count\\":42}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const emitCall = delegate.calls.find(c => c.method === 'executeEmit');
    assert.ok(emitCall, 'executeEmit should have been called');
  });

  it('#107 shell.respond sends response to caller', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.respond "hello world") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const respondCall = delegate.calls.find(c => c.method === 'executeRespond');
    assert.ok(respondCall);
    assert.deepStrictEqual(respondCall!.args, ['hello world']);
  });

  it('#108 shell.eval evaluates Lisp code', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.eval "(+ 1 2)") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const evalCall = delegate.calls.find(c => c.method === 'executeEvalLisp');
    assert.ok(evalCall);
    assert.deepStrictEqual(evalCall!.args, ['(+ 1 2)']);
  });

  it('#109 shell.define-component registers component', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.define-component "widget" "class {}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const defCall = delegate.calls.find(c => c.method === 'executeDefineComponent');
    assert.ok(defCall);
    assert.deepStrictEqual(defCall!.args, ['widget', 'class {}']);
  });

  it('#110 shell.load-component loads component', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.load-component "widget") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const loadCall = delegate.calls.find(c => c.method === 'executeLoadComponent');
    assert.ok(loadCall);
    assert.deepStrictEqual(loadCall!.args, ['widget']);
  });

  it('#111 shell.save-component saves to store', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.save-component "widget" "class {}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const saveCall = delegate.calls.find(c => c.method === 'executeSaveComponent');
    assert.ok(saveCall);
  });

  it('#112 shell.list-components returns component list', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.list-components) (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const listCall = delegate.calls.find(c => c.method === 'executeListComponents');
    assert.ok(listCall);
  });

  it('#113 shell.introspect calls $introspect on mount', async () => {
    delegate.calls.length = 0;
    const prog = stepper.compile(
      '(begin (effect shell.introspect "daemon.system") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c =>
      c.method === 'executeInvoke' && (c.args as string[])[1] === '$introspect',
    );
    assert.ok(invokeCall, 'Should have called executeInvoke with $introspect');
    assert.strictEqual((invokeCall!.args as string[])[0], 'daemon.system');
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Security (#116-119)
// ═══════════════════════════════════════════════════════════════════════════

describe('Shell security', () => {
  it('#116 shell.invoke with target outside allowed subtree → denied', async () => {
    const delegate = createMockDelegate({
      isInSubtree: (target) => target.startsWith('allowed.'),
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(effect shell.invoke "forbidden.actor" "$ping" "{}")',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // The effect should return an error map, not crash
    // Since the shell effect returns an error Val (not membrane.deny),
    // the program completes normally but the result is an error map
    assert.strictEqual(result.action, 'allow'); // membrane default
  });

  it('#117 shell.invoke with disallowed command → error returned', async () => {
    const delegate = createMockDelegate({
      executeInvoke: async () => ({ ok: false, data: { error: 'FORBIDDEN' } }),
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.invoke "target" "forbidden-op" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
  });

  it('#118 shell.emit with target outside subtree → denied', async () => {
    const delegate = createMockDelegate({
      isInSubtree: () => false,
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.emit "forbidden.event" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // Effect returns error map, program continues
    assert.strictEqual(result.action, 'allow');
  });

  it('#119 isInSubtree check uses delegate pattern', () => {
    let checked = false;
    const delegate = createMockDelegate({
      isInSubtree: (target) => {
        checked = true;
        return target === 'allowed';
      },
    });
    const backend = new ShellEffectBackend(delegate);
    assert.ok(backend); // Backend created
    assert.ok(!checked); // Not checked yet (lazy)
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Composition (#120-125)
// ═══════════════════════════════════════════════════════════════════════════

describe('Shell composition', () => {
  it('#120 invoke then use result — sequential effects', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    // Two sequential shell effects + membrane allow
    const prog = stepper.compile(
      '(begin (effect shell.invoke "a" "$introspect" "{}") (effect shell.invoke "b" "$ping" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCalls = delegate.calls.filter(c => c.method === 'executeInvoke');
    assert.strictEqual(invokeCalls.length, 2);
  });

  it('#121 sequential invokes execute in order', async () => {
    const order: string[] = [];
    const delegate = createMockDelegate({
      executeInvoke: async (target, op) => {
        order.push(`${target}.${op}`);
        return { ok: true, data: {} };
      },
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.invoke "a" "op1" "{}") (effect shell.invoke "b" "op2" "{}") (effect membrane.allow op payload))',
    );
    await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.deepStrictEqual(order, ['a.op1', 'b.op2']);
  });

  it('#124 error in middle of composition → program still completes', async () => {
    const delegate = createMockDelegate({
      executeInvoke: async (target) => {
        if (target === 'error-target') {
          return { ok: false, data: { error: 'FAILED' } };
        }
        return { ok: true, data: {} };
      },
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.invoke "error-target" "op" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // Program completes despite error in invoke (error is returned as Val, not thrown)
    assert.strictEqual(result.action, 'allow');
  });

  it('#125 nested composition: invoke result used in next invoke', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    // Use let to capture result of first invoke and pass to second
    const prog = stepper.compile(
      '(begin (let ((r (effect shell.invoke "a" "$introspect" "{}"))) (effect shell.invoke "b" "process" "{}")) (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCalls = delegate.calls.filter(c => c.method === 'executeInvoke');
    assert.strictEqual(invokeCalls.length, 2);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Shell prelude sugar functions (#126-131) — tested as source compilation
// ═══════════════════════════════════════════════════════════════════════════

describe('Shell prelude concepts', () => {
  // Note: Shell prelude functions like (invoke), (emit), (ls), (prompt), (pipe)
  // are defined in shell-prelude.lisp and loaded into OmegaSandbox.
  // Here we test the underlying effects they would call.

  it('#126 invoke sugar pattern works', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    // Simulates what (invoke "target" "op") would expand to
    const prog = stepper.compile(
      '(begin (effect shell.invoke "target" "op" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    assert.ok(delegate.calls.some(c => c.method === 'executeInvoke'));
  });

  it('#127 invoke sugar with payload works', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.invoke "target" "op" "{\\"key\\":\\"val\\"}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const call = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.deepStrictEqual(call!.args[2], { key: 'val' });
  });

  it('#128 emit sugar pattern works', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.emit "event" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    assert.ok(delegate.calls.some(c => c.method === 'executeEmit'));
  });

  it('#129 introspect sugar (ls) pattern works', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.introspect "daemon") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const call = delegate.calls.find(c =>
      c.method === 'executeInvoke' && (c.args as string[])[1] === '$introspect',
    );
    assert.ok(call);
  });

  it('#130 prompt sugar pattern works', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    // Simulates (prompt mount text) → (invoke mount "$prompt" '{"text":"hello"}')
    const prog = stepper.compile(
      '(begin (effect shell.invoke "chat" "$prompt" "{\\"text\\":\\"hello\\"}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const call = delegate.calls.find(c =>
      c.method === 'executeInvoke' && (c.args as string[])[1] === '$prompt',
    );
    assert.ok(call);
  });

  it('#131 sequential effects (pipe-like) work correctly', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    // Three sequential operations
    const prog = stepper.compile(
      '(begin (effect shell.invoke "a" "step1" "{}") (effect shell.invoke "b" "step2" "{}") (effect shell.invoke "c" "step3" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCalls = delegate.calls.filter(c => c.method === 'executeInvoke');
    assert.strictEqual(invokeCalls.length, 3);
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Round 3 audit fixes (#209-215)
// ═══════════════════════════════════════════════════════════════════════════

describe('Shell delegate completeness (Round 3 audit)', () => {
  it('#209 ShellDelegate has all 16 required methods', () => {
    const delegate = createMockDelegate();
    const requiredMethods: (keyof ShellDelegate)[] = [
      'isInSubtree',
      'executeInvoke',
      'executeEmit',
      'executeRespond',
      'executeDefineComponent',
      'executeLoadComponent',
      'executeSaveComponent',
      'executeListComponents',
      'executeEvalLisp',
      'executeFindComponents',
      'executeGetComponentSource',
      'executePublishComponent',
      'executeInstallComponent',
      'executeResolveIntent',
      'executeTraceQuery',
      'executeReplPackages',
      'executeReplDoc',
    ];
    for (const method of requiredMethods) {
      assert.strictEqual(typeof delegate[method], 'function', `Missing method: ${method}`);
    }
  });

  it('#210 all 17 shell effects dispatch without crashing', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    // All 17 effects with correct arities
    const effects = [
      '(effect shell.invoke "t" "op" "{}")',
      '(effect shell.emit "e" "{}")',
      '(effect shell.respond "hello")',
      '(effect shell.define-component "w" "class {}")',
      '(effect shell.load-component "w")',
      '(effect shell.save-component "w" "class {}")',
      '(effect shell.list-components)',
      '(effect shell.eval "(+ 1 2)")',
      '(effect shell.introspect "daemon")',
      '(effect shell.find-components "query")',
      '(effect shell.get-component-source "widget")',
      '(effect shell.publish-component "widget")',
      '(effect shell.install-component "widget")',
      '(effect shell.resolve-intent "find a timer")',
      '(effect shell.trace-query "errors")',
      '(effect shell.repl-packages)',
      '(effect shell.repl-doc "map")',
    ];

    for (const effectExpr of effects) {
      const prog = stepper.compile(
        `(begin ${effectExpr} (effect membrane.allow op payload))`,
      );
      const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
      assert.strictEqual(result.action, 'allow', `Effect crashed: ${effectExpr}`);
    }
  });

  it('#211 shell.find-components delegates to executeFindComponents', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.find-components "timer") (effect membrane.allow op payload))',
    );
    await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    const call = delegate.calls.find(c => c.method === 'executeFindComponents');
    assert.ok(call, 'executeFindComponents should have been called');
    assert.deepStrictEqual(call!.args, ['timer']);
  });

  it('#212 shell.publish-component delegates to executePublishComponent', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.publish-component "widget") (effect membrane.allow op payload))',
    );
    await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    const call = delegate.calls.find(c => c.method === 'executePublishComponent');
    assert.ok(call, 'executePublishComponent should have been called');
    assert.deepStrictEqual(call!.args, ['widget']);
  });

  it('#213 shell.repl-packages delegates to executeReplPackages', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.repl-packages) (effect membrane.allow op payload))',
    );
    await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    const call = delegate.calls.find(c => c.method === 'executeReplPackages');
    assert.ok(call, 'executeReplPackages should have been called');
  });

  it('#214 shell.repl-doc delegates to executeReplDoc', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.repl-doc "map") (effect membrane.allow op payload))',
    );
    await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    const call = delegate.calls.find(c => c.method === 'executeReplDoc');
    assert.ok(call, 'executeReplDoc should have been called');
    assert.deepStrictEqual(call!.args, ['map']);
  });

  it('#215 shell.trace-query delegates to executeTraceQuery', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.trace-query "errors") (effect membrane.allow op payload))',
    );
    await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    const call = delegate.calls.find(c => c.method === 'executeTraceQuery');
    assert.ok(call, 'executeTraceQuery should have been called');
    assert.deepStrictEqual(call!.args, ['errors']);
  });
});

describe('OmegaSandbox hydrate with additionalBackends (Round 3 audit)', () => {
  it('#216 hydrate passes additionalBackends to buildRuntime', () => {
    // This is a structural test — verify the hydrate path in OmegaSandbox
    // passes config.additionalBackends through to buildRuntime.
    // We test by importing OmegaSandbox and checking the code was fixed.
    // (The real test is that hydrated sandboxes have shell effects available,
    // which requires a full OmegaSandbox — tested indirectly via integration.)
    //
    // For unit-level: verify ShellEffectBackend can be instantiated and has ops
    const delegate = createMockDelegate();
    const backend = new ShellEffectBackend(delegate);
    assert.strictEqual(backend.ops.length, 21);
    // Verify all extended ops are present
    const extendedOps: Array<(typeof backend.ops)[number]> = [
      'shell.find-components', 'shell.get-component-source',
      'shell.publish-component', 'shell.install-component',
      'shell.resolve-intent', 'shell.trace-query',
      'shell.repl-packages', 'shell.repl-doc', 'shell.ls',
      'shell.spawn', 'shell.kill',
    ];
    for (const op of extendedOps) {
      assert.ok(backend.ops.includes(op), `Missing extended op: ${op}`);
    }
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// shell.ls — list children as proper Scheme list (C3 composition enabler)
// ═══════════════════════════════════════════════════════════════════════════

describe('shell.ls effect', () => {
  it('#209a shell.ls returns list of child mount strings from $introspect', async () => {
    const introspectCalls: Array<{ target: string; op: string }> = [];
    const delegate = createMockDelegate({
      executeInvoke: async (target: string, op: string, _payload: Record<string, unknown>) => {
        introspectCalls.push({ target, op });
        if (op === '$introspect') {
          return {
            ok: true,
            data: {
              mount: target,
              componentClass: 'Root',
              children: [
                { id: 'system.llm', mount: 'system.llm', componentClass: 'LlmActor' },
                { id: 'code.files', mount: 'code.files', componentClass: 'FileActor' },
              ],
            },
          };
        }
        return { ok: true, data: { target, op } };
      },
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.ls "") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    // The $introspect call should have been made
    const call = introspectCalls.find(c => c.op === '$introspect');
    assert.ok(call, 'shell.ls should call $introspect');
  });

  it('#209b shell.ls respects subtree guard', async () => {
    const delegate = createMockDelegate({
      isInSubtree: (target: string) => target.startsWith('users.alice.'),
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.ls "users.bob") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    // $introspect should NOT have been called (subtree blocked)
    const call = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(!call, 'shell.ls should not call $introspect when outside subtree');
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Subtree capability enforcement (C9) — #208a-c
// ═══════════════════════════════════════════════════════════════════════════

describe('Shell subtree capability enforcement', () => {
  it('#208a subtree guard blocks out-of-scope invoke via ShellEffectBackend', async () => {
    const delegate = createMockDelegate({
      isInSubtree: (target: string) => target.startsWith('users.alice.'),
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.invoke "users.bob.data" "$ping" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // The invoke should have failed due to subtree guard
    assert.strictEqual(result.action, 'allow');
    // executeInvoke should NOT have been called (subtree blocked it)
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(!invokeCall, 'executeInvoke should not be called when target is outside subtree');
  });

  it('#208b subtree guard allows in-scope invoke', async () => {
    const delegate = createMockDelegate({
      isInSubtree: (target: string) => target.startsWith('users.alice.'),
    });
    const stepper = createShellStepper(delegate);

    const prog = stepper.compile(
      '(begin (effect shell.invoke "users.alice.workspace" "$ping" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(invokeCall, 'executeInvoke should be called when target is in subtree');
  });

  it('#208c check-capability: system commands always allowed via isInSubtree', async () => {
    const delegate = createMockDelegate({
      // Strict subtree: only allow users.alice.*
      isInSubtree: (target: string) => {
        if (target === '' || target.startsWith('system.')) return true;
        return target.startsWith('users.alice.');
      },
    });
    const stepper = createShellStepper(delegate);

    // Invoke root actor (system command — should pass)
    const prog = stepper.compile(
      '(begin (effect shell.invoke "" "$ping" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(invokeCall, 'System command invoke should be allowed');
  });

  // ─── Round 12: shell.spawn and shell.kill effects ──────────────────────

  it('#210a shell.spawn sends factory.create to parent', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile(
      '(begin (effect shell.spawn "system" "my-child" "DynamicActor") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c =>
      c.method === 'executeInvoke' && c.args[1] === 'factory.create',
    );
    assert.ok(invokeCall, 'shell.spawn should invoke factory.create');
    assert.deepStrictEqual(invokeCall!.args[0], 'system');
    const spawnPayload = invokeCall!.args[2] as { id?: string; componentClass?: string };
    assert.deepStrictEqual(spawnPayload.id, 'my-child');
    assert.deepStrictEqual(spawnPayload.componentClass, 'DynamicActor');
  });

  it('#210b shell.spawn rejects target outside subtree', async () => {
    const delegate = createMockDelegate({
      isInSubtree: (target: string) => target.startsWith('system.'),
    });
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile(
      '(begin (effect shell.spawn "users.evil" "backdoor" "DynamicActor") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    // Should NOT have called executeInvoke — subtree guard blocked it
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(!invokeCall, 'shell.spawn should be blocked outside subtree');
  });

  it('#210c shell.spawn rejects empty childId', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile(
      '(begin (effect shell.spawn "system" "" "DynamicActor") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(!invokeCall, 'shell.spawn should reject empty childId');
  });

  it('#211a shell.kill sends $dispose to target', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile(
      '(begin (effect shell.kill "system.temp-actor") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c =>
      c.method === 'executeInvoke' && c.args[1] === '$dispose',
    );
    assert.ok(invokeCall, 'shell.kill should invoke $dispose');
    assert.strictEqual(invokeCall!.args[0], 'system.temp-actor');
  });

  it('#211b shell.kill rejects target outside subtree', async () => {
    const delegate = createMockDelegate({
      isInSubtree: (target: string) => target.startsWith('system.'),
    });
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile(
      '(begin (effect shell.kill "users.alice.session") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(!invokeCall, 'shell.kill should be blocked outside subtree');
  });

  it('#211c shell.kill rejects empty target', async () => {
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile(
      '(begin (effect shell.kill "") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const invokeCall = delegate.calls.find(c => c.method === 'executeInvoke');
    assert.ok(!invokeCall, 'shell.kill should reject empty target');
  });

  it('#211d shell.fork returns structured error when delegate lacks fork support', async () => {
    const delegate = createMockDelegate();
    delete (delegate as Partial<ShellDelegate>).executeForkSession;
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile('(effect shell.fork "session-2" "")');
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    // MembraneStepper returns MembraneResult with action field, not ok/display.
    // The shell.fork error is embedded in the CESK return value, not the membrane action.
    // The membrane defaults to 'allow' since no membrane effect (allow/deny/mutate) was called.
    assert.strictEqual(result.action, 'allow');
  });
});

// ═══════════════════════════════════════════════════════════════════════════
// Per-operation capability enforcement — isEffectAllowed (#212a-d)
// ═══════════════════════════════════════════════════════════════════════════

describe('isEffectAllowed gate (#212a-d)', () => {
  it('#212a isEffectAllowed blocks disallowed effect before dispatch', async () => {
    const delegate = createMockDelegate({
      isEffectAllowed: (op: string) => op === 'shell.invoke',
    });
    const stepper = createShellStepper(delegate);
    // shell.emit is NOT in allowedEffects — should be blocked
    const prog = stepper.compile(
      '(begin (effect shell.emit "evil-event" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    // The emit should NOT have been dispatched
    const emitCall = delegate.calls.find(c => c.method === 'executeEmit');
    assert.ok(!emitCall, 'shell.emit should NOT be dispatched when isEffectAllowed returns false');
  });

  it('#212b isEffectAllowed allows permitted effect to dispatch', async () => {
    const delegate = createMockDelegate({
      isEffectAllowed: (op: string) => op === 'shell.invoke' || op === 'shell.emit',
    });
    const stepper = createShellStepper(delegate);
    // shell.emit IS allowed — should pass through
    const prog = stepper.compile(
      '(begin (effect shell.emit "good-event" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const emitCall = delegate.calls.find(c => c.method === 'executeEmit');
    assert.ok(emitCall, 'shell.emit SHOULD be dispatched when isEffectAllowed returns true');
  });

  it('#212c isEffectAllowed undefined means all effects allowed (backward compat)', async () => {
    // No isEffectAllowed on delegate — all effects should work
    const delegate = createMockDelegate();
    const stepper = createShellStepper(delegate);
    const prog = stepper.compile(
      '(begin (effect shell.emit "any-event" "{}") (effect membrane.allow op payload))',
    );
    const result = await stepper.run(prog, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(result.action, 'allow');
    const emitCall = delegate.calls.find(c => c.method === 'executeEmit');
    assert.ok(emitCall, 'With no isEffectAllowed, all effects should dispatch');
  });

  it('#212d isEffectAllowed blocks spawn but allows invoke (separate steppers)', async () => {
    // Test spawn is blocked
    const delegate1 = createMockDelegate({
      isEffectAllowed: (op: string) => op === 'shell.invoke' || op === 'shell.introspect',
    });
    const stepper1 = createShellStepper(delegate1);
    const spawnProg = stepper1.compile(
      '(begin (effect shell.spawn "parent" "child" "DynamicActor") (effect membrane.allow op payload))',
    );
    const spawnResult = await stepper1.run(spawnProg, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(spawnResult.action, 'allow');
    const spawnCall = delegate1.calls.find(c => c.method === 'executeInvoke' && c.args[1] === 'factory.create');
    assert.ok(!spawnCall, 'shell.spawn should be blocked');

    // Test invoke is allowed (fresh stepper)
    const delegate2 = createMockDelegate({
      isEffectAllowed: (op: string) => op === 'shell.invoke' || op === 'shell.introspect',
    });
    const stepper2 = createShellStepper(delegate2);
    const invokeProg = stepper2.compile(
      '(begin (effect shell.invoke "system.llm" "$ping" "{}") (effect membrane.allow op payload))',
    );
    const invokeResult = await stepper2.run(invokeProg, { op: 'test', payload: {}, sender: 'a', meta: {} });
    assert.strictEqual(invokeResult.action, 'allow');
    const invokeCall = delegate2.calls.find(c => c.method === 'executeInvoke');
    assert.ok(invokeCall, 'shell.invoke should be allowed');
  });
});
