import { spawnSync } from 'node:child_process';
import {
  existsSync,
  mkdirSync,
  mkdtempSync,
  readdirSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from 'node:fs';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { load } from 'js-yaml';
import { describe, expect, it, vi } from 'vitest';

const WORKFLOW_PATH = path.resolve(
  __dirname,
  '../../../.github/workflows/gitnexus-review-agent.yml',
);
const RUNTIME_PACKAGE_PATH = path.resolve(
  __dirname,
  '../../../.github/gitnexus-review-runtime/package.json',
);
const RUNTIME_LOCK_PATH = path.resolve(
  __dirname,
  '../../../.github/gitnexus-review-runtime/package-lock.json',
);
const CLAUDE_RUNTIME_PACKAGE_PATH = path.resolve(
  __dirname,
  '../../../.github/claude-canary-runtime/package.json',
);
const CLAUDE_RUNTIME_LOCK_PATH = path.resolve(
  __dirname,
  '../../../.github/claude-canary-runtime/package-lock.json',
);
const workflow = readFileSync(WORKFLOW_PATH, 'utf8');
// Both pinned installs run through this helper, so the flags that keep them
// inert and lock-bound are asserted against it rather than the step bodies.
const npmCiHelper = readFileSync(
  path.resolve(__dirname, '../../../.github/scripts/npm-ci-retry.sh'),
  'utf8',
);
const runtimePackage = JSON.parse(readFileSync(RUNTIME_PACKAGE_PATH, 'utf8')) as {
  dependencies?: Record<string, string>;
  engines?: Record<string, string>;
};
const runtimeLock = JSON.parse(readFileSync(RUNTIME_LOCK_PATH, 'utf8')) as {
  packages?: Record<string, { version?: string; integrity?: string }>;
};
const claudeRuntimePackage = JSON.parse(readFileSync(CLAUDE_RUNTIME_PACKAGE_PATH, 'utf8')) as {
  dependencies?: Record<string, string>;
  engines?: Record<string, string>;
};
const claudeRuntimeLock = JSON.parse(readFileSync(CLAUDE_RUNTIME_LOCK_PATH, 'utf8')) as {
  lockfileVersion?: number;
  packages?: Record<
    string,
    {
      dependencies?: Record<string, string>;
      engines?: Record<string, string>;
      version?: string;
      integrity?: string;
    }
  >;
};
const requireCjs = createRequire(import.meta.url);
const workflowDocument = load(workflow) as {
  jobs?: Record<
    string,
    {
      steps?: Array<{
        name?: string;
        env?: Record<string, string>;
        run?: unknown;
        with?: Record<string, unknown> & { script?: unknown };
      }>;
    }
  >;
};

const PR_NUMBER = 2431;
const CONTROL_SHA = '1'.repeat(40);
const HEAD_SHA = '2'.repeat(40);
const BASE_SHA = '3'.repeat(40);
const CHANGED_PATH = 'gitnexus/src/cli/status.ts';
// Long enough to clear the assembler's MIN_BODY_CHARS floor, which exists so a
// stub like the literal string "placeholder" can never be published.
const ACCEPTED_BODY = `**APPROVE.** ${'Accepted graph-backed review of the changed surface. '.repeat(5)}`;

function jobBlock(name: string): string {
  const match = workflow.match(
    new RegExp(`\\n  ${name}:\\n[\\s\\S]*?(?=\\n  [a-zA-Z0-9_-]+:\\n|$)`),
  );
  return match?.[0] ?? '';
}

function jobScript(job: string, stepName: string): string {
  const step = workflowDocument.jobs?.[job]?.steps?.find(({ name }) => name === stepName);
  return typeof step?.with?.script === 'string' ? step.with.script : '';
}

function jobRun(job: string, stepName: string): string {
  const step = workflowDocument.jobs?.[job]?.steps?.find(({ name }) => name === stepName);
  return typeof step?.run === 'string' ? step.run : '';
}

function embeddedNodeScript(job: string, stepName: string): string {
  const run = jobRun(job, stepName);
  const marker = "node <<'NODE'\n";
  const start = run.indexOf(marker);
  const end = run.lastIndexOf('\nNODE');
  if (start < 0 || end <= start) throw new Error(`${stepName} Node heredoc not found`);
  return run.slice(start + marker.length, end);
}

function runGit(cwd: string, arguments_: string[]): void {
  const result = spawnSync('git', arguments_, { cwd, encoding: 'utf8' });
  if (result.status !== 0) {
    throw new Error(`git ${arguments_.join(' ')} failed: ${result.stderr}`);
  }
}

type ContextScenario = {
  controlSha?: string;
  dispatchPr?: string;
  eventName?: 'issue_comment' | 'workflow_dispatch';
  eventPr?: string;
  permission?: string;
  permissionError?: Error;
  pull?: Record<string, unknown>;
  pullError?: Error;
  existingComments?: Array<{ user?: { login: string }; body?: string }>;
};

async function runContextScenario({
  controlSha = CONTROL_SHA,
  dispatchPr = String(PR_NUMBER),
  eventName = 'workflow_dispatch',
  eventPr = String(PR_NUMBER),
  permission = 'write',
  permissionError,
  pull,
  pullError,
  existingComments = [],
}: ContextScenario = {}) {
  const contextScript = jobScript('analyze', 'Normalize and authorize the request');
  if (!contextScript) throw new Error('context github-script block not found');

  const resolvedPull =
    pull ??
    ({
      state: 'open',
      head: { sha: HEAD_SHA, repo: { full_name: 'fork/repo' } },
      base: { sha: BASE_SHA, repo: { full_name: 'owner/repo' } },
    } as Record<string, unknown>);
  const getPermission = permissionError
    ? vi.fn().mockRejectedValue(permissionError)
    : vi.fn().mockResolvedValue({ data: { permission } });
  const getPull = pullError
    ? vi.fn().mockRejectedValue(pullError)
    : vi.fn().mockResolvedValue({ data: resolvedPull });
  const listComments = vi.fn();
  const github = {
    rest: {
      repos: { getCollaboratorPermissionLevel: getPermission },
      pulls: { get: getPull },
      issues: { listComments },
    },
    paginate: {
      iterator: vi.fn(function* iterate() {
        yield { data: existingComments };
      }),
    },
  };
  const outputs = new Map<string, string>();
  const core = {
    debug: vi.fn(),
    notice: vi.fn(),
    setOutput: vi.fn((name: string, value: string) => outputs.set(name, value)),
  };
  const context = {
    actor: 'trusted-maintainer',
    eventName,
    repo: { owner: 'owner', repo: 'repo' },
  };

  try {
    vi.stubEnv('CONTROL_SHA', controlSha);
    vi.stubEnv('DISPATCH_PR', dispatchPr);
    vi.stubEnv('EVENT_PR', eventPr);
    const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor as new (
      ...arguments_: string[]
    ) => (...arguments_: unknown[]) => Promise<void>;
    const execute = new AsyncFunction('github', 'context', 'core', contextScript);
    await execute(github, context, core);
  } finally {
    vi.unstubAllEnvs();
  }

  return { core, getPermission, getPull, outputs };
}

function runChangedPathManifest(rawNameStatus: string | Uint8Array) {
  const script = embeddedNodeScript('analyze', 'Prepare exact merge-base review inputs');
  const inputDirectory = mkdtempSync(path.join(tmpdir(), 'gitnexus-review-name-status-'));
  writeFileSync(path.join(inputDirectory, 'changed-name-status.bin'), rawNameStatus);
  try {
    const result = spawnSync(process.execPath, ['-'], {
      encoding: 'utf8',
      env: {
        ...process.env,
        PR_NUMBER: String(PR_NUMBER),
        HEAD_SHA,
        BASE_SHA,
        MERGE_BASE: CONTROL_SHA,
        INPUT_DIR: inputDirectory,
      },
      input: script,
    });
    const manifestPath = path.join(inputDirectory, 'changed-paths.json');
    return {
      result,
      manifest: existsSync(manifestPath)
        ? (JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>)
        : undefined,
    };
  } finally {
    rmSync(inputDirectory, { recursive: true, force: true });
  }
}

type PublisherScenario = {
  artifactStatus: 'success' | 'failure';
  artifactOverrides?: Record<string, unknown>;
  rawArtifact?: string | Uint8Array;
  comments?: Array<{
    id: number;
    body: string;
    user: { login: string };
  }>;
  commentPages?: Array<
    Array<{
      id: number;
      body: string;
      user: { login: string };
    }>
  >;
  currentBase?: string;
  currentHead?: string;
  finalBase?: string;
  finalHead?: string;
  finalState?: string;
};

type ArtifactScenario = {
  basePaths?: string[];
  basePrescanPaths?: string[];
  changedPaths?: string[];
  entries?: Array<Record<string, string>>;
  executionFileOutput?: string;
  repairStructuredOutput?: string;
  repairOutcome?: string;
  noIndexableChangedSymbols?: boolean;
  rawTranscript?: string | Uint8Array | ((runnerTemp: string) => string | Uint8Array);
  structuredOutput?: string;
};

function contextResultContent(filePath = CHANGED_PATH): string {
  return `${JSON.stringify({
    status: 'found',
    symbol: {
      uid: 'Function:gitnexus/src/cli/status.ts:statusCommand',
      name: 'statusCommand',
      kind: 'Function',
      filePath,
      startLine: 1,
      endLine: 20,
    },
  })}\n\n---\n**Next:** use impact() for blast radius.`;
}

function reviewTranscript({
  toolName = 'mcp__gitnexus__context',
  // No file_path: the gate is call-argument-agnostic, and a default that
  // carried one would imply the opposite.
  toolInput = { name: 'statusCommand' },
  toolResultContent = contextResultContent(),
  resultIsError = false,
  toolUseId = 'tool-1',
  parentToolUseId = null,
}: {
  toolName?: string;
  toolInput?: Record<string, unknown>;
  toolResultContent?: unknown;
  resultIsError?: boolean | undefined;
  toolUseId?: string;
  parentToolUseId?: string | null;
} = {}): Array<Record<string, unknown>> {
  const toolResult: Record<string, unknown> = {
    type: 'tool_result',
    tool_use_id: toolUseId,
    content: toolResultContent,
  };
  if (resultIsError !== undefined) toolResult.is_error = resultIsError;

  return [
    {
      type: 'system',
      subtype: 'init',
      session_id: 'session-1',
      uuid: '11111111-1111-4111-8111-111111111111',
    },
    {
      type: 'assistant',
      parent_tool_use_id: parentToolUseId,
      session_id: 'session-1',
      uuid: '22222222-2222-4222-8222-222222222222',
      message: {
        role: 'assistant',
        content: [
          {
            type: 'tool_use',
            id: toolUseId,
            name: toolName,
            input: toolInput,
          },
        ],
      },
    },
    {
      type: 'user',
      parent_tool_use_id: parentToolUseId,
      session_id: 'session-1',
      uuid: '33333333-3333-4333-8333-333333333333',
      message: {
        role: 'user',
        content: [toolResult],
      },
    },
    {
      type: 'result',
      subtype: 'success',
      is_error: false,
      num_turns: 25,
      total_cost_usd: 3.8797,
      session_id: 'session-1',
      uuid: '44444444-4444-4444-8444-444444444444',
    },
  ];
}

// Two orchestrator context calls in one turn: the first proves the evidence,
// the second is an ordinary exploratory call whose result may be junk.
function twoCallTranscript({
  firstResult,
  secondResult,
}: {
  firstResult: string;
  secondResult: string;
}): Array<Record<string, unknown>> {
  return [
    {
      type: 'system',
      subtype: 'init',
      session_id: 'session-1',
      uuid: '11111111-1111-4111-8111-111111111111',
    },
    {
      type: 'assistant',
      parent_tool_use_id: null,
      session_id: 'session-1',
      uuid: '22222222-2222-4222-8222-222222222222',
      message: {
        role: 'assistant',
        content: [
          {
            type: 'tool_use',
            id: 'tool-1',
            name: 'mcp__gitnexus__context',
            input: { name: 'statusCommand' },
          },
          {
            type: 'tool_use',
            id: 'tool-2',
            name: 'mcp__gitnexus__context',
            input: { name: 'bigHotSymbol' },
          },
        ],
      },
    },
    {
      type: 'user',
      parent_tool_use_id: null,
      session_id: 'session-1',
      uuid: '33333333-3333-4333-8333-333333333333',
      message: {
        role: 'user',
        content: [
          { type: 'tool_result', tool_use_id: 'tool-1', is_error: false, content: firstResult },
          { type: 'tool_result', tool_use_id: 'tool-2', is_error: false, content: secondResult },
        ],
      },
    },
    {
      type: 'result',
      subtype: 'success',
      is_error: false,
      session_id: 'session-1',
      uuid: '44444444-4444-4444-8444-444444444444',
    },
  ];
}

function reviewTranscriptWithoutTools(): Array<Record<string, unknown>> {
  return [
    {
      type: 'system',
      subtype: 'init',
      session_id: 'session-1',
      uuid: '11111111-1111-4111-8111-111111111111',
    },
    {
      type: 'assistant',
      parent_tool_use_id: null,
      session_id: 'session-1',
      uuid: '22222222-2222-4222-8222-222222222222',
      message: { role: 'assistant', content: [] },
    },
    {
      type: 'result',
      subtype: 'success',
      is_error: false,
      session_id: 'session-1',
      uuid: '44444444-4444-4444-8444-444444444444',
    },
  ];
}

function runArtifactScenario({
  basePaths = [],
  basePrescanPaths = basePaths,
  changedPaths = [CHANGED_PATH],
  entries = [
    ...changedPaths.map((headPath) => ({ status: 'A', head_path: headPath })),
    ...basePaths.map((basePath) => ({ status: 'D', base_path: basePath })),
  ],
  executionFileOutput,
  repairStructuredOutput,
  repairOutcome = repairStructuredOutput ? 'success' : 'skipped',
  noIndexableChangedSymbols = false,
  rawTranscript = JSON.stringify(reviewTranscript()),
  structuredOutput = JSON.stringify({ body: ACCEPTED_BODY, complete: true }),
}: ArtifactScenario = {}) {
  const script = embeddedNodeScript('analyze', 'Assemble bounded review artifact');
  const runnerTemp = mkdtempSync(path.join(tmpdir(), 'gitnexus-review-artifact-'));
  const inputDirectory = path.join(runnerTemp, 'gitnexus-review-control', 'review-input');
  const transcriptPath = path.join(runnerTemp, 'claude-execution-output.json');
  const githubOutput = path.join(runnerTemp, 'github-output');
  mkdirSync(inputDirectory, { recursive: true });
  writeFileSync(
    path.join(inputDirectory, 'changed-paths.json'),
    `${JSON.stringify({
      schema: 'gitnexus.changed-paths/v2',
      entries,
      head_paths: changedPaths,
      base_paths: basePaths,
      base_prescan_paths: basePrescanPaths,
      prescan: {
        head_has_indexable_symbol: !noIndexableChangedSymbols && changedPaths.length > 0,
        base_has_indexable_symbol: !noIndexableChangedSymbols && basePrescanPaths.length > 0,
        no_indexable_changed_symbols: noIndexableChangedSymbols,
      },
    })}\n`,
  );
  writeFileSync(
    transcriptPath,
    typeof rawTranscript === 'function' ? rawTranscript(runnerTemp) : rawTranscript,
  );
  writeFileSync(githubOutput, '');

  const workspace = path.join(runnerTemp, 'workspace');
  const headCheckout = path.join(workspace, 'pr-target');
  const baseCheckout = path.join(runnerTemp, 'gitnexus-review-merge-base');
  mkdirSync(path.join(workspace, '.github', 'scripts'), { recursive: true });
  writeFileSync(
    path.join(workspace, '.github', 'scripts', 'review-citations.cjs'),
    readFileSync(path.resolve(__dirname, '../../../.github/scripts/review-citations.cjs'), 'utf8'),
  );
  // 40 real lines per changed file so a citation can resolve or overrun.
  for (const [checkout, files] of [
    [headCheckout, changedPaths],
    [baseCheckout, basePaths],
  ] as const) {
    for (const filePath of files) {
      const absolute = path.join(checkout, filePath);
      mkdirSync(path.dirname(absolute), { recursive: true });
      writeFileSync(
        absolute,
        Array.from({ length: 40 }, (_unused, i) => `line ${i + 1}`).join('\n'),
      );
    }
  }

  const environment = {
    ...process.env,
    RUNNER_TEMP: runnerTemp,
    GITHUB_REPOSITORY: 'owner/repo',
    MERGE_BASE_SHA: BASE_SHA,
    GITHUB_WORKSPACE: workspace,
    GITHUB_OUTPUT: githubOutput,
    PR_NUMBER: String(PR_NUMBER),
    CONTROL_SHA,
    HEAD_SHA,
    BASE_SHA,
    CONTEXT_READY: 'true',
    FAILURE_CODE: 'none',
    CONTROL_OUTCOME: 'success',
    HEAD_OUTCOME: 'success',
    VALIDATE_OUTCOME: 'success',
    SETUP_NODE_OUTCOME: 'success',
    ISOLATION_OUTCOME: 'success',
    CLAUDE_RUNTIME_OUTCOME: 'success',
    RUNTIME_OUTCOME: 'success',
    INDEX_OUTCOME: 'success',
    INPUTS_OUTCOME: 'success',
    MERGE_BASE_SOURCE_OUTCOME: 'success',
    GRAPH_PRESCAN_OUTCOME: 'success',
    CLAUDE_RECHECK_OUTCOME: 'success',
    CLAUDE_OUTCOME: 'success',
    EXECUTION_FILE: executionFileOutput ?? transcriptPath,
    STRUCTURED_OUTPUT: structuredOutput,
    REPAIR_OUTCOME: repairOutcome,
    REPAIR_STRUCTURED_OUTPUT: repairStructuredOutput ?? '',
    REPAIR_EXECUTION_FILE: repairStructuredOutput ? transcriptPath : '',
  };

  try {
    const result = spawnSync(process.execPath, ['-'], {
      encoding: 'utf8',
      env: environment,
      input: script,
      maxBuffer: 20_000_000,
    });
    if (result.status !== 0) {
      throw new Error(`artifact assembler failed: ${result.stderr || result.stdout}`);
    }
    const artifact = JSON.parse(
      readFileSync(path.join(runnerTemp, 'gitnexus-review-artifact', 'review.json'), 'utf8'),
    ) as {
      body: string;
      failure_code: string | null;
      graph_evidence: {
        base_has_indexable_symbol: boolean;
        head_has_indexable_symbol: boolean;
        mode: 'context' | 'no_indexable_changed_symbols';
      } | null;
      status: 'success' | 'failure';
    };
    return { artifact, stderr: result.stderr, stdout: result.stdout };
  } finally {
    rmSync(runnerTemp, { recursive: true, force: true });
  }
}

async function runPublisherScenario({
  artifactStatus,
  artifactOverrides = {},
  rawArtifact,
  comments = [],
  commentPages,
  currentBase = BASE_SHA,
  currentHead = HEAD_SHA,
  finalBase = currentBase,
  finalHead = currentHead,
  finalState = 'open',
}: PublisherScenario) {
  const publisherScript = jobScript(
    'publish',
    'Validate freshness and upsert an accepted same-SHA comment',
  );
  if (!publisherScript) throw new Error('publisher github-script block not found');

  const tempDir = mkdtempSync(path.join(tmpdir(), 'gitnexus-review-publisher-'));
  const artifactPath = path.join(tempDir, 'review.json');
  const artifact = {
    schema: 'gitnexus.review/v2',
    pr_number: PR_NUMBER,
    control_sha: CONTROL_SHA,
    head_sha: HEAD_SHA,
    base_sha: BASE_SHA,
    status: artifactStatus,
    body: artifactStatus === 'success' ? 'Accepted review body' : 'Model failed safely.',
    failure_code: artifactStatus === 'success' ? null : 'model_failed',
    graph_evidence:
      artifactStatus === 'success'
        ? {
            mode: 'context',
            head_has_indexable_symbol: true,
            base_has_indexable_symbol: false,
          }
        : null,
    ...artifactOverrides,
  };
  writeFileSync(artifactPath, rawArtifact ?? `${JSON.stringify(artifact)}\n`);

  const updateComment = vi.fn().mockResolvedValue({ data: {} });
  const createComment = vi.fn().mockResolvedValue({ data: { id: 99 } });
  const paginateIterator = vi.fn(() => {
    const pages = commentPages ?? [comments];
    return (async function* () {
      for (const page of pages) yield { data: page };
    })();
  });
  let pullRead = 0;
  const getPull = vi.fn().mockImplementation(async () => {
    const initial = pullRead++ === 0;
    return {
      data: {
        state: initial ? 'open' : finalState,
        head: {
          sha: initial ? currentHead : finalHead,
          repo: { full_name: 'fork/repo' },
        },
        base: {
          sha: initial ? currentBase : finalBase,
          repo: { full_name: 'owner/repo' },
        },
      },
    };
  });
  const github = {
    paginate: Object.assign(vi.fn(), { iterator: paginateIterator }),
    rest: {
      pulls: {
        get: getPull,
      },
      issues: {
        listComments: vi.fn(),
        updateComment,
        createComment,
      },
    },
  };
  const core = {
    info: vi.fn(),
    notice: vi.fn(),
    setFailed: vi.fn(),
    warning: vi.fn(),
  };
  const context = { repo: { owner: 'owner', repo: 'repo' } };
  const environment = {
    ARTIFACT_PATH: artifactPath,
    DOWNLOAD_OUTCOME: 'success',
    PR_NUMBER: String(PR_NUMBER),
    CONTROL_SHA,
    HEAD_SHA,
    BASE_SHA,
  };

  try {
    for (const [key, value] of Object.entries(environment)) vi.stubEnv(key, value);
    const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor as new (
      ...arguments_: string[]
    ) => (...arguments_: unknown[]) => Promise<void>;
    const execute = new AsyncFunction('github', 'context', 'core', 'require', publisherScript);
    await execute(github, context, core, requireCjs);
  } finally {
    vi.unstubAllEnvs();
    rmSync(tempDir, { recursive: true, force: true });
  }

  return { core, createComment, getPull, paginate: paginateIterator, updateComment };
}

describe('gitnexus review-agent workflow security contract', () => {
  it('ships default-off activation and rollback instructions with the workflow', () => {
    expect(workflow).toContain('Activation checklist (the comment-trigger lane is OFF by default)');
    expect(workflow).toContain('Configure the repository secret CLAUDE_CODE_OAUTH_TOKEN');
    expect(workflow).toContain(
      'Run workflow_dispatch against a disposable same-repo PR and a fork PR',
    );
    expect(workflow).toContain('GITNEXUS_REVIEW_COMMENT_ENABLED=true');
    expect(workflow).toContain('Roll back immediately by setting that variable to false');
  });

  it('pins every third-party action and the GitNexus analyzer exactly', () => {
    const expectedPins = [
      'actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0',
      'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3',
      'actions/setup-node@820762786026740c76f36085b0efc47a31fe5020',
      'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a',
      'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c',
      'anthropics/claude-code-action/base-action@3553f84341b92da26052e28acf1aa898f9511f32',
    ];

    for (const pin of expectedPins) {
      expect(workflow).toContain(pin);
    }

    const uses = [...workflow.matchAll(/^\s*-?\s*uses:\s*([^\s#]+)/gm)].map((match) => match[1]);
    expect(uses.length).toBeGreaterThan(0);
    for (const action of uses) {
      expect(action, `${action} must use a full immutable SHA`).toMatch(/@[0-9a-f]{40}$/);
    }

    expect(runtimePackage.dependencies?.gitnexus).toBe('1.6.9');
    expect(runtimePackage.engines?.node).toBe('22.18.0');
    expect(runtimeLock.packages?.['node_modules/gitnexus']?.version).toBe('1.6.9');
    expect(runtimeLock.packages?.['node_modules/gitnexus']?.integrity).toMatch(/^sha512-/);
    expect(workflow).not.toMatch(/gitnexus@(latest|next|beta)/);
    expect(workflow).toContain("node-version: '22.18.0'");
    expect(workflow).toContain('test "$(node --version)" = \'v22.18.0\'');
    expect(workflow).toContain('.github/scripts/npm-ci-retry.sh');
    expect(workflow).not.toContain('--package-lock=false');
    expect(workflow).toContain(
      'actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0',
    );
    expect(workflow).toContain(
      'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0',
    );
  });

  it('installs inert lock payloads, then activates and preflights them offline', () => {
    const runtimeStep = workflowDocument.jobs?.analyze?.steps?.find(
      ({ name }) => name === 'Prepare exact GitNexus runtime and strict MCP config',
    );
    expect(runtimeStep?.env).toEqual({
      NPM_CONFIG_IGNORE_SCRIPTS: 'true',
      ONNXRUNTIME_NODE_INSTALL: 'skip',
      SCARF_ANALYTICS: 'false',
      DO_NOT_TRACK: '1',
    });
    const script = typeof runtimeStep?.run === 'string' ? runtimeStep.run : '';
    expect(script).toContain('.github/scripts/npm-ci-retry.sh');
    expect(npmCiHelper).toContain('--ignore-scripts=true');
    expect(script).toContain('"${npm_path}" rebuild');
    expect(script).toContain('NPM_CONFIG_OFFLINE=true');
    expect(script).toContain('--offline');
    expect(script).toContain('--unshare-net');
    expect(script).toContain('NPM_CONFIG_IGNORE_SCRIPTS=false');
    expect(script).toContain('"${runtime_dir}/node_modules/.bin/gitnexus" analyze');
    expect(script).toContain('"${runtime_dir}/node_modules/.bin/gitnexus" status');
    // A registry ECONNRESET during this install burned a whole review run; the
    // lock is pinned, so a bounded retry can only refetch the identical tree.
    // Both installs call one shared helper, exercised behaviourally below.
    expect(script).toContain('/.github/scripts/npm-ci-retry.sh');
    expect(script).toContain("'analyzer runtime'");
    expect(script.indexOf('npm ci')).toBeLessThan(script.indexOf('"${npm_path}" rebuild'));
    expect(script.indexOf('"${npm_path}" rebuild')).toBeLessThan(
      script.indexOf('"${runtime_dir}/node_modules/.bin/gitnexus" analyze'),
    );
  });

  it('runs the hostile-derived MCP database reader in a separate bounded sandbox', () => {
    const script = jobRun('analyze', 'Prepare exact GitNexus runtime and strict MCP config');
    const start = script.indexOf('# The index is derived from hostile parser input.');
    const end = script.indexOf('chmod 0755 "${mcp_wrapper}"', start);
    expect(start).toBeGreaterThan(-1);
    expect(end).toBeGreaterThan(start);
    const mcpSandbox = script.slice(start, end);

    expect(script).toContain('mcp_wrapper="${RUNNER_TEMP}/gitnexus-review-mcp"');
    expect(script).toContain('MCP_WRAPPER="${mcp_wrapper}" MCP_CONFIG="${mcp_config}"');
    expect(script).toContain('command: process.env.MCP_WRAPPER');
    expect(script).toContain('args: []');
    expect(script).not.toContain('command: process.env.WRAPPER');
    expect(mcpSandbox).toContain('--unshare-user');
    expect(mcpSandbox).toContain('--unshare-pid');
    expect(mcpSandbox).toContain('--unshare-net');
    expect(mcpSandbox).toContain('--die-with-parent');
    expect(mcpSandbox).toContain('--new-session');
    expect(mcpSandbox).toContain('--ro-bind / /');
    expect(mcpSandbox).toContain('--ro-bind "${source_dir}" "${source_dir}"');
    expect(mcpSandbox).toContain('--ro-bind "${base_source_dir}" "${base_source_dir}"');
    expect(mcpSandbox).toContain('--ro-bind "${runtime_dir}" "${runtime_dir}"');
    expect(mcpSandbox).toContain('--ro-bind "${claude_runtime_dir}" "${claude_runtime_dir}"');
    expect(mcpSandbox).toContain('--bind "${storage_dir}" "${storage_dir}"');
    expect(mcpSandbox).toContain('--bind "${base_storage_dir}" "${base_storage_dir}"');
    expect(mcpSandbox).toContain('--bind "${index_home}" "${index_home}"');
    expect(mcpSandbox).toContain('--bind "${mcp_home}" "${mcp_home}"');
    expect(mcpSandbox).toContain('--bind "${mcp_tmp}" "${mcp_tmp}"');
    expect(mcpSandbox).toContain('/usr/bin/env -i');
    expect(mcpSandbox).toContain('GITNEXUS_MCP_READ_ONLY=1');
    expect(mcpSandbox).toContain('GITNEXUS_MCP_ALLOWED_REPOS=${source_dir},${base_source_dir}');
    expect(mcpSandbox).toContain('requested_command=(mcp)');
    expect(mcpSandbox).toContain(
      '"${runtime_dir}/node_modules/.bin/gitnexus" "${requested_command[@]}"',
    );
    expect(mcpSandbox).not.toContain('--bind "${source_dir}" "${source_dir}"');
    expect(mcpSandbox).not.toContain('CLAUDE_CODE_OAUTH_TOKEN');
  });

  it('installs the exact secret-consuming Claude executable before the action runs', () => {
    const runtimeStep = workflowDocument.jobs?.analyze?.steps?.find(
      ({ name }) => name === 'Prepare exact Claude Code executable',
    );
    const claudeStep = workflowDocument.jobs?.analyze?.steps?.find(
      ({ name }) => name === 'Run read-only graph-backed review',
    );
    const script = typeof runtimeStep?.run === 'string' ? runtimeStep.run : '';
    const expectedExecutable =
      '${{ runner.temp }}/gitnexus-review-claude-runtime/node_modules/@anthropic-ai/claude-code/bin/claude.exe';

    expect(claudeRuntimePackage.dependencies?.['@anthropic-ai/claude-code']).toBe('2.1.214');
    expect(claudeRuntimePackage.engines?.node).toBe('22.18.0');
    expect(claudeRuntimeLock.lockfileVersion).toBe(3);
    expect(claudeRuntimeLock.packages?.['node_modules/@anthropic-ai/claude-code']).toMatchObject({
      version: '2.1.214',
      integrity:
        'sha512-Gf8XbPHBacVqBlxx8sMnKWPEU6AvRNUcjD0FS6zhD44fCgCHcpbpxwSoTbHlLTqKsr/0S7wdfhjjOIq8WlYbng==',
    });
    expect(
      claudeRuntimeLock.packages?.['node_modules/@anthropic-ai/claude-code-linux-x64'],
    ).toMatchObject({
      version: '2.1.214',
      integrity:
        'sha512-NSQjXX8QjjjYdDlYbPvlse5yQ3UwsmV2vuPNR3eFaXnGVv7ymFHvDSMIkTFRLXQlmPjp+tvAN5fbH3e1C38SOw==',
    });
    expect(runtimeStep?.env).toEqual({
      NPM_CONFIG_IGNORE_SCRIPTS: 'true',
      DO_NOT_TRACK: '1',
    });
    expect(script).toContain('.github/claude-canary-runtime/package-lock.json');
    expect(script).toContain('.github/scripts/npm-ci-retry.sh');
    expect(script).toContain('/.github/scripts/npm-ci-retry.sh');
    expect(script).toContain("'Claude runtime'");
    expect(npmCiHelper).toContain('--ignore-scripts=true');
    expect(script).toContain('--unshare-net');
    expect(script).toContain('NPM_CONFIG_OFFLINE=true');
    expect(script).toContain('@anthropic-ai/claude-code/install.cjs');
    expect(script).toContain('cmp --silent');
    expect(script).toContain('3c029136f7c81f54ed4a38e9d52e655aad536433dbbde50519c8c31bb646ad14');
    expect(script).toContain("'2.1.214 (Claude Code)'");
    expect(claudeStep?.with?.path_to_claude_code_executable).toBe(expectedExecutable);
    expect(workflow).not.toContain('https://claude.ai/install.sh');
    expect(workflow.indexOf('id: claude-runtime')).toBeLessThan(
      workflow.indexOf('claude_code_oauth_token:'),
    );
    expect(workflow).toContain('CLAUDE_RUNTIME_OUTCOME: ${{ steps.claude-runtime.outcome }}');
  });

  it('gates comment triggers exactly and checks API write authority before model spend', () => {
    expect(workflow).toContain("github.event.comment.body == '@gitnexus review'");
    expect(workflow).not.toContain("contains(github.event.comment.body, '@gitnexus review')");
    expect(workflow).toContain("vars.GITNEXUS_REVIEW_COMMENT_ENABLED == 'true'");
    expect(workflow).toContain("github.event.comment.author_association == 'OWNER'");
    expect(workflow).toContain("github.event.comment.author_association == 'MEMBER'");
    expect(workflow).toContain("github.event.comment.author_association == 'COLLABORATOR'");
    expect(workflow).not.toContain("github.event.comment.author_association == 'CONTRIBUTOR'");

    const permissionCheck = workflow.indexOf('getCollaboratorPermissionLevel');
    const modelInvocation = workflow.indexOf('anthropics/claude-code-action/base-action@');
    expect(permissionCheck).toBeGreaterThan(-1);
    expect(modelInvocation).toBeGreaterThan(permissionCheck);
    expect(workflow).toContain("['admin', 'maintain', 'write'].includes(permission)");
    expect(workflow).toContain("steps.context.outputs.authorized == 'true'");
  });

  it('normalizes both events through the API and rejects unsafe PR metadata', () => {
    expect(workflow).toContain("github.event_name == 'workflow_dispatch'");
    expect(workflow).toContain('github.rest.pulls.get');
    expect(workflow).toContain("pull.state !== 'open'");
    expect(workflow).toContain('pull.base.repo.full_name');
    expect(workflow).toContain('pull.head.repo');
    expect(workflow).toContain('pull.head.sha');
    expect(workflow).toContain('pull.base.sha');
    expect(workflow).toMatch(/\^\\d\+\$|\^\[1-9\]\\d\*\$/);
    expect(workflow).toContain('/^[0-9a-f]{40}$/');
    expect(workflow).toContain('context.repo.owner');
    expect(workflow).toContain('context.repo.repo');
    expect(workflow).toContain('Number.isSafeInteger(prNumber)');
  });

  it('executes normalization for dispatch and comment events before enabling the model path', async () => {
    const dispatch = await runContextScenario({
      eventName: 'workflow_dispatch',
      dispatchPr: String(PR_NUMBER),
      eventPr: 'not-used',
      permission: 'admin',
    });
    expect(dispatch.getPermission).toHaveBeenCalledWith({
      owner: 'owner',
      repo: 'repo',
      username: 'trusted-maintainer',
    });
    expect(dispatch.getPull).toHaveBeenCalledWith({
      owner: 'owner',
      repo: 'repo',
      pull_number: PR_NUMBER,
    });
    expect(Object.fromEntries(dispatch.outputs)).toMatchObject({
      authorized: 'true',
      ready: 'true',
      pr_number: String(PR_NUMBER),
      control_sha: CONTROL_SHA,
      head_repo: 'fork/repo',
      head_sha: HEAD_SHA,
      base_sha: BASE_SHA,
      failure_code: 'none',
    });

    const comment = await runContextScenario({
      eventName: 'issue_comment',
      dispatchPr: 'hostile-not-used',
      eventPr: String(PR_NUMBER),
      permission: 'maintain',
    });
    expect(comment.outputs.get('ready')).toBe('true');
    expect(comment.outputs.get('failure_code')).toBe('none');
  });

  it('executes the authorization boundary and fails hostile request metadata closed', async () => {
    for (const rawPr of ['0', '01', '-1', '1e3', '2431 trailing', '9007199254740992']) {
      const invalid = await runContextScenario({ dispatchPr: rawPr });
      expect(invalid.outputs.get('authorized')).toBe('false');
      expect(invalid.outputs.get('ready')).toBe('false');
      expect(invalid.outputs.get('failure_code')).toBe('invalid_pr_number');
      expect(invalid.getPermission).not.toHaveBeenCalled();
      expect(invalid.getPull).not.toHaveBeenCalled();
    }

    const invalidControl = await runContextScenario({ controlSha: `g${CONTROL_SHA.slice(1)}` });
    expect(invalidControl.outputs.get('failure_code')).toBe('invalid_control_sha');
    expect(invalidControl.getPermission).not.toHaveBeenCalled();

    const reader = await runContextScenario({ permission: 'read' });
    expect(reader.outputs.get('authorized')).toBe('false');
    expect(reader.outputs.get('ready')).toBe('false');
    expect(reader.outputs.get('failure_code')).toBe('actor_not_authorized');
    expect(reader.getPull).not.toHaveBeenCalled();
  });

  it('executes PR tuple validation and rejects hostile API responses', async () => {
    const validHead = { sha: HEAD_SHA, repo: { full_name: 'fork/repo' } };
    const validBase = { sha: BASE_SHA, repo: { full_name: 'owner/repo' } };
    const cases: Array<{ failure: string; pull: Record<string, unknown> }> = [
      {
        failure: 'invalid_pr_sha',
        pull: { state: 'open', head: { ...validHead, sha: 'not-a-sha' }, base: validBase },
      },
      {
        failure: 'pr_not_open',
        pull: { state: 'closed', head: validHead, base: validBase },
      },
      {
        failure: 'wrong_base_repository',
        pull: {
          state: 'open',
          head: validHead,
          base: { ...validBase, repo: { full_name: 'attacker/repo' } },
        },
      },
      {
        failure: 'head_repository_deleted',
        pull: { state: 'open', head: { sha: HEAD_SHA, repo: null }, base: validBase },
      },
      {
        failure: 'invalid_head_repository',
        pull: {
          state: 'open',
          head: { sha: HEAD_SHA, repo: { full_name: 'attacker/repo/extra' } },
          base: validBase,
        },
      },
    ];

    for (const scenario of cases) {
      const result = await runContextScenario({ pull: scenario.pull });
      expect(result.outputs.get('ready')).toBe('false');
      expect(result.outputs.get('failure_code')).toBe(scenario.failure);
    }

    const unavailable = await runContextScenario({
      permissionError: new Error('API unavailable'),
    });
    expect(unavailable.outputs.get('authorized')).toBe('false');
    expect(unavailable.outputs.get('ready')).toBe('false');
    expect(unavailable.outputs.get('failure_code')).toBe('metadata_unavailable');
    expect(unavailable.core.debug).toHaveBeenCalled();
  });

  it('checks out trusted control code at the workflow SHA and treats fork code as passive data', () => {
    const analyze = jobBlock('analyze');
    expect(analyze).not.toBe('');
    expect(analyze).toContain('repository: ${{ github.repository }}');
    expect(analyze).toContain('ref: ${{ steps.context.outputs.control_sha }}');
    expect(analyze).toContain('repository: ${{ steps.context.outputs.head_repo }}');
    expect(analyze).toContain('ref: ${{ steps.context.outputs.head_sha }}');
    expect(analyze).toContain('path: pr-target');
    expect(analyze.match(/fetch-depth: 0/g)?.length).toBeGreaterThanOrEqual(2);
    expect(analyze.match(/persist-credentials: false/g)?.length).toBeGreaterThanOrEqual(2);
    expect(analyze.match(/submodules: false/g)?.length).toBeGreaterThanOrEqual(2);
    expect(analyze.match(/lfs: false/g)?.length).toBeGreaterThanOrEqual(2);

    // Fetch the base commit as an object without checking it out. A non-shallow
    // fetch is intentional because the trusted diff step must compute the true
    // merge-base even when the base branch advanced after the fork point.
    expect(analyze).toContain('git fetch --no-tags origin "${BASE_SHA}"');
    expect(analyze).toContain('git cat-file -e "${BASE_SHA}^{commit}"');
    expect(analyze).toContain('git rev-parse HEAD');
    expect(analyze).toContain('git -C pr-target rev-parse HEAD');
    expect(analyze).toContain('find pr-target -path pr-target/.gitnexus -prune -o -type l -print0');
    expect(analyze).not.toContain('find pr-target -type l -print0');
    expect(analyze).toContain('realpath -m');
    expect(analyze).toContain('Escaping symlink');
    expect(analyze).toContain('gitnexus-review-hostile-dot-gitnexus');

    // The trusted job installs the lock-resolved analyzer into RUNNER_TEMP,
    // but it must never invoke package scripts from the PR checkout.
    expect(analyze).not.toMatch(/cd[^\n]*pr-target[\s\S]{0,200}npm\s+(ci|install|run)\b/);
    expect(analyze).not.toContain('npm install');
    expect(analyze).toContain('.github/scripts/npm-ci-retry.sh');
    expect(npmCiHelper).toContain('--prefix "${runtime_dir}"');
    expect(analyze).not.toContain('pr-target/.mcp.json');
    expect(analyze).not.toContain('pr-target/.claude');
    expect(analyze).not.toContain('node pr-target/.gitnexus/run.cjs');
    expect(analyze).toContain("GITNEXUS_NO_GITIGNORE: '1'");
    expect(analyze).toContain('for config in .gitnexusrc .gitnexusignore');
    expect(analyze).toContain('trap restore_target_config EXIT');
  });

  it('contains the real hostile index build and exposes only dedicated writable stores', () => {
    const script = jobRun('analyze', 'Build the exact-head graph index');
    const readOnlySource =
      '--ro-bind "${GITHUB_WORKSPACE}/pr-target" "${GITHUB_WORKSPACE}/pr-target"';
    const writableStorage = '--bind "${storage_dir}" "${storage_dir}"';
    const analyzeInvocation = '"${wrapper}" analyze --force --pdg --index-only --no-stats';

    expect(script).toContain('storage_dir="${GITHUB_WORKSPACE}/pr-target/.gitnexus"');
    expect(script).toContain('storage_quarantine=');
    expect(script).toContain('mv -- "${storage_dir}" "${storage_quarantine}"');
    expect(script).toContain('test ! -e "${storage_dir}" && test ! -L "${storage_dir}"');
    expect(script).toContain('install -d -m 0700 "${storage_dir}"');
    expect(script).toContain("stat -c '%u'");
    expect(script).toContain("stat -c '%a'");
    expect(script).toContain('--unshare-user');
    expect(script).toContain('--unshare-pid');
    expect(script).toContain('--unshare-net');
    expect(script).toContain('--die-with-parent');
    expect(script).toContain('--new-session');
    expect(script).toContain('--ro-bind / /');
    expect(script).toContain(readOnlySource);
    expect(script).toContain(writableStorage);
    expect(script).toContain('--bind "${index_home}" "${index_home}"');
    expect(script).toContain('--bind "${sandbox_home}" "${sandbox_home}"');
    expect(script).toContain('--bind "${sandbox_tmp}" "${sandbox_tmp}"');
    expect(script).toContain('/usr/bin/env -i');
    expect(script).toContain(analyzeInvocation);
    expect(script.indexOf(readOnlySource)).toBeLessThan(script.indexOf(writableStorage));
    expect(script.indexOf(writableStorage)).toBeLessThan(script.indexOf(analyzeInvocation));
    expect(script).not.toContain(
      '--bind "${GITHUB_WORKSPACE}/pr-target" "${GITHUB_WORKSPACE}/pr-target"',
    );
    expect(script).not.toContain('"${RUNNER_TEMP}/gitnexus-review" analyze');
  });

  it('builds exact head and merge-base graphs from trusted name-status topology', () => {
    const runtime = jobRun('analyze', 'Prepare exact GitNexus runtime and strict MCP config');
    const mergeBase = jobRun('analyze', 'Materialize the exact merge-base graph source');
    const index = jobRun('analyze', 'Build the exact-head graph index');
    const inputs = jobRun('analyze', 'Prepare exact merge-base review inputs');
    const prescan = jobRun('analyze', 'Prescan exact changed-symbol graph evidence');

    expect(mergeBase).toContain('git -C pr-target merge-base');
    expect(mergeBase).toContain('checkout --quiet --detach "${merge_base}"');
    expect(mergeBase).toContain('write-tree');
    expect(mergeBase).toContain('Escaping merge-base symlink');
    expect(index).toContain('gitnexus-review-merge-base');
    expect(index).toContain('gitnexus-review-hostile-base-dot-gitnexus');
    expect(index.match(/analyze --force --pdg --index-only --no-stats/g)).toHaveLength(2);
    expect(runtime).toContain('GITNEXUS_MCP_ALLOWED_REPOS=${source_dir},${base_source_dir}');
    expect(runtime).toContain('--ro-bind "${base_source_dir}" "${base_source_dir}"');
    expect(inputs).toContain('--name-status');
    expect(inputs).toContain("schema: 'gitnexus.changed-paths/v2'");
    expect(inputs).toContain("status.startsWith('R')");
    expect(inputs).toContain('basePaths.add(oldPath)');
    expect(inputs).toContain('basePrescanPaths.add(oldPath)');
    expect(inputs).toContain('headPaths.add(newPath)');
    expect(prescan).toContain('manifest.base_prescan_paths');
    expect(prescan).toContain("['cypher', statement, '--repo', repo, '--limit', '1']");
    expect(prescan).toContain("AND NOT n.id STARTS WITH 'BasicBlock:'");
    expect(prescan).toContain('no_indexable_changed_symbols');
  });

  it('parses deletion and rename-old paths into the merge-base evidence set', () => {
    const deletedPath = 'src/deleted.ts';
    const oldPath = 'src/old-name.ts';
    const newPath = 'src/new-name.ts';
    const copySource = 'src/copy-source.ts';
    const copyTarget = 'src/copy-target.ts';
    const addedPath = 'src/added.ts';
    const modifiedPath = 'src/modified.ts';
    const { manifest, result } = runChangedPathManifest(
      `D\0${deletedPath}\0R077\0${oldPath}\0${newPath}\0C050\0${copySource}\0${copyTarget}\0A\0${addedPath}\0M\0${modifiedPath}\0`,
    );

    expect(result.status).toBe(0);
    expect(manifest).toEqual({
      schema: 'gitnexus.changed-paths/v2',
      entries: [
        { status: 'D', base_path: deletedPath },
        { status: 'R077', base_path: oldPath, head_path: newPath },
        { status: 'C050', head_path: copyTarget, copy_source: copySource },
        { status: 'A', head_path: addedPath },
        {
          status: 'M',
          base_prescan_path: modifiedPath,
          head_path: modifiedPath,
        },
      ],
      head_paths: [newPath, copyTarget, addedPath, modifiedPath],
      base_paths: [deletedPath, oldPath],
      base_prescan_paths: [deletedPath, oldPath, modifiedPath],
      prescan: null,
    });

    const hostile = runChangedPathManifest('D\0../escape.ts\0');
    expect(hostile.result.status).not.toBe(0);
    expect(hostile.manifest).toBeUndefined();
    expect(hostile.result.stderr).toContain('invalid path');
  });

  it('accepts zero-padded rename and copy scores at the artifact boundary', () => {
    const renamedFrom = 'src/renamed-from.ts';
    const renamedTo = 'src/renamed-to.ts';
    const copiedFrom = 'src/copied-from.ts';
    const copiedTo = 'src/copied-to.ts';
    const { artifact } = runArtifactScenario({
      basePaths: [renamedFrom],
      changedPaths: [renamedTo, copiedTo],
      entries: [
        { status: 'R077', base_path: renamedFrom, head_path: renamedTo },
        { status: 'C050', copy_source: copiedFrom, head_path: copiedTo },
      ],
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolInput: { name: 'renamedCommand', file_path: renamedTo },
          toolResultContent: contextResultContent(renamedTo),
        }),
      ),
    });

    expect(artifact.status).toBe('success');
  });

  it('copies the indexed HEAD tree below the passive review root without prefix collisions', () => {
    const analyze = jobBlock('analyze');
    const prefixTemplate = analyze.match(/checkout-index --all --force --prefix="([^"]+)"/)?.[1];
    expect(prefixTemplate).toBe('${review_dir}/');

    const tempDir = mkdtempSync(path.join(tmpdir(), 'gitnexus-review-checkout-index-'));
    const repository = path.join(tempDir, 'source');
    const reviewDirectory = path.join(tempDir, 'control', 'pr-target');
    try {
      mkdirSync(path.join(repository, 'nested'), { recursive: true });
      mkdirSync(reviewDirectory, { recursive: true });
      writeFileSync(path.join(repository, 'root.txt'), 'root\n');
      writeFileSync(path.join(repository, 'nested', 'child.txt'), 'child\n');
      runGit(repository, ['init', '--quiet']);
      runGit(repository, ['add', 'root.txt', 'nested/child.txt']);

      const expandedPrefix = prefixTemplate?.replace('${review_dir}', reviewDirectory);
      expect(expandedPrefix).toBe(`${reviewDirectory}/`);
      runGit(repository, ['checkout-index', '--all', '--force', `--prefix=${expandedPrefix}`]);

      expect(existsSync(path.join(reviewDirectory, 'root.txt'))).toBe(true);
      expect(existsSync(path.join(reviewDirectory, 'nested', 'child.txt'))).toBe(true);
      expect(existsSync(`${reviewDirectory}root.txt`)).toBe(false);
    } finally {
      rmSync(tempDir, { recursive: true, force: true });
    }
  });

  it('keeps the model job read-only and the publisher secretless and checkout-free', () => {
    const analyze = jobBlock('analyze');
    const publish = jobBlock('publish');
    expect(workflow).toMatch(/^permissions:\s*\{\}\s*$/m);
    expect(analyze).toContain('contents: read');
    expect(analyze).toContain('pull-requests: read');
    expect(analyze).not.toContain('issues: write');
    expect(publish).toContain('issues: write');
    expect(publish).toContain('pull-requests: write');
    expect(publish).not.toContain('contents: write');
    expect(publish).not.toContain('actions/checkout@');
    expect(publish).not.toContain('ANTHROPIC_API_KEY');
    expect(publish).not.toContain('CLAUDE_CODE_OAUTH_TOKEN');
    expect(publish).not.toContain('secrets.');
    expect(publish).not.toContain('anthropics/claude-code-action/');
  });

  it('preflights the exact Bubblewrap primitive before exposing the model secret', () => {
    const analyze = jobBlock('analyze');
    const install = analyze.indexOf(
      'sudo apt-get install --yes --no-install-recommends bubblewrap',
    );
    const canary = analyze.indexOf('--unshare-user');
    const model = analyze.indexOf('claude_code_oauth_token:');

    expect(install).toBeGreaterThan(-1);
    expect(canary).toBeGreaterThan(install);
    expect(model).toBeGreaterThan(canary);
    expect(analyze).toContain('kernel.apparmor_restrict_unprivileged_userns=0');
    expect(analyze).toContain('--unshare-pid');
    expect(analyze).toContain('--die-with-parent');
    expect(analyze).toContain('--new-session');
    expect(analyze).toContain('--ro-bind / /');
    expect(analyze).toContain('CLAUDE_CODE_SUBPROCESS_ENV_SCRUB');
  });

  it('rechecks the pinned Claude executable immediately before exposing the secret', () => {
    const recheck = jobRun('analyze', 'Reverify exact Claude executable at secret boundary');
    const inputs = workflow.indexOf('- name: Prepare exact merge-base review inputs');
    const recheckStep = workflow.indexOf(
      '- name: Reverify exact Claude executable at secret boundary',
    );
    const modelStep = workflow.indexOf('- name: Run read-only graph-backed review');
    const token = workflow.indexOf('claude_code_oauth_token:');

    expect(recheck).toContain('cmp --silent -- "${native_binary}" "${claude_binary}"');
    expect(recheck).toContain('3c029136f7c81f54ed4a38e9d52e655aad536433dbbde50519c8c31bb646ad14');
    expect(recheck).toContain("'2.1.214 (Claude Code)'");
    expect(inputs).toBeGreaterThan(-1);
    expect(recheckStep).toBeGreaterThan(inputs);
    expect(modelStep).toBeGreaterThan(recheckStep);
    expect(token).toBeGreaterThan(modelStep);
    expect(workflow).toContain("steps.claude-recheck.outcome == 'success'");
    expect(workflow).toContain('CLAUDE_RECHECK_OUTCOME: ${{ steps.claude-recheck.outcome }}');
    expect(workflow).toContain("process.env.CLAUDE_RECHECK_OUTCOME !== 'success'");
  });

  it('uses only trusted agent configuration and a strict, exact analyzer MCP', () => {
    const analyze = jobBlock('analyze');
    expect(analyze).toContain('anthropics/claude-code-action/base-action@');
    expect(analyze).not.toContain('uses: anthropics/claude-code-action@');
    expect(analyze).not.toContain('github_token:');
    expect(analyze).toContain('--add-dir "${{ runner.temp }}/gitnexus-review-pr-target"');
    expect(analyze).toContain('Read(${{ runner.temp }}/gitnexus-review-pr-target/**)');
    expect(analyze).toContain('review_dir="${RUNNER_TEMP}/gitnexus-review-pr-target"');
    expect(analyze).not.toContain('review_dir="${control_dir}/pr-target"');
    expect(analyze).toContain(
      'CLAUDE_CONFIG_DIR: ${{ runner.temp }}/gitnexus-review-claude-config',
    );
    expect(analyze).toContain('CLAUDE_WORKING_DIR: ${{ runner.temp }}/gitnexus-review-control');
    expect(analyze).toContain("NODE_VERSION: '22.18.0'");
    expect(analyze).toContain('checkout-index --all --force');
    expect(analyze).toContain('find "${review_dir}" -type l -print0');
    expect(analyze).toContain('Escaping copied review symlink');
    expect(analyze).toContain(
      'cp -a -- .claude/skills/gitnexus-review/. "${control_dir}/trusted-skill/"',
    );
    expect(analyze).toContain('trusted-skill/SKILL.md');
    expect(analyze).toContain('"disableAllHooks":true');
    expect(analyze).toContain('"disableSkillShellExecution":true');
    expect(analyze).toContain('--setting-sources user');
    expect(analyze).not.toContain('--setting-sources ""');
    expect(analyze).toContain('--strict-mcp-config');
    expect(analyze).toContain('--mcp-config');
    expect(analyze).toContain('--disable-slash-commands');
    expect(analyze).toContain('.github/gitnexus-review-runtime/package-lock.json');
    expect(analyze).toContain('GITNEXUS_MCP_READ_ONLY=1');
    expect(analyze).toContain('GITNEXUS_MCP_ALLOWED_REPOS');
    expect(analyze).toContain('GITNEXUS_MCP_DEFAULT_REPO');
    expect(analyze).toContain('NPM_CONFIG_IGNORE_SCRIPTS');
    expect(analyze).toContain("CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: '1'");
    expect(analyze).toContain("CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '0'");
    expect(analyze).toContain('--disallowedTools');
    expect(analyze).toContain('Bash');
    expect(analyze).toContain('Write');
    expect(analyze).toContain('Edit');
    const allowedTools = analyze.match(/--allowedTools "([^"]+)"/)?.[1] ?? '';
    const allowedToolRules = allowedTools.split(',');
    expect(allowedToolRules).toContain('Read(./**)');
    expect(allowedToolRules).not.toContain('Read');
    // Glob/Grep are neither enabled nor allow-listed: bare Glob/Grep are separate
    // tools that the Read()-scoped path denies below (/proc, github.workspace,
    // ...) do not cover, so allow-listing them would open an undenied read path
    // to the raw checkouts and host paths. Leaving them in --tools while denying
    // them by omission only burned model turns on denied calls, so they are off
    // the tool set entirely; lanes read via the scoped Read() rules and the MCP.
    expect(allowedToolRules).not.toContain('Glob');
    expect(allowedToolRules).not.toContain('Grep');
    // The merge-base source checkout is readable so lanes can inspect deleted or
    // rename-old source; a Read() allow rule grants access without triggering
    // --add-dir agent discovery.
    expect(allowedTools).toContain('Read(${{ runner.temp }}/gitnexus-review-merge-base/**)');
    expect(allowedTools).toContain('mcp__gitnexus__impact');
    expect(allowedTools).not.toContain('mcp__gitnexus__detect_changes');
    expect(allowedTools).not.toContain('mcp__gitnexus__rename');
    expect(allowedTools).not.toContain('mcp__gitnexus__cypher');
    // Swarm posture: the orchestrator dispatches subagents via the Agent tool
    // (renamed from Task in Claude Code 2.1.63), scoped to the six trusted
    // control-SHA personas; Agent is not bare-denied (deny beats allow), and
    // lane calls cannot satisfy the evidence gate.
    expect(analyze).toContain('--tools "Read,Agent"');
    // One rule per persona, never a grouped Agent(a,b,c): the pinned base
    // action parses allowedTools with `.flatMap((v) => v.split(","))`, which
    // would shatter a grouped rule into `Agent(ci-correctness-lens`, bare
    // names, and `ci-critic-lens)` before the SDK ever sees it.
    expect(allowedToolRules).toContain('Agent(ci-correctness-lens)');
    expect(allowedToolRules).toContain('Agent(ci-critic-lens)');
    expect(allowedTools).not.toMatch(/Agent\([^)]*,/);
    expect(allowedTools).not.toContain('Task');
    const disallowedTools = analyze.match(/--disallowedTools "([^"]+)"/)?.[1] ?? '';
    const disallowedToolRules = disallowedTools.split(',');
    expect(disallowedToolRules).toContain('Bash');
    expect(disallowedToolRules).not.toContain('Agent');
    expect(disallowedTools).not.toContain('Task');
    expect(analyze).toContain(
      'cp -a -- .claude/skills/gitnexus-review/ci-personas/. "${claude_config}/agents/"',
    );
    // The passive add-dir tree is scanned for agent definitions; drop any
    // PR-controlled ones at any depth so only the trusted control-SHA personas
    // can be dispatched. Skills under the copy are NOT pruned (a skill-editing
    // PR must stay reviewable).
    expect(analyze).toContain(
      `find "\${review_dir}" -type d -path '*/.claude/agents' -prune -exec rm -rf -- {} +`,
    );
    expect(analyze).not.toContain(".claude/skills' -prune");
    expect(analyze).toContain("satisfy the publisher's context-evidence gate");
    // The orchestrator's own evidence call is a precondition of dispatch, so a
    // fully-delegated run cannot leave the gate unsatisfied.
    expect(analyze).toContain('dispatching any lane');
    expect(analyze).toContain('Read(/proc/**)');
    expect(analyze).toContain('Read(${{ github.workspace }}/**)');
    expect(analyze).toContain(
      'mcp__gitnexus__detect_changes,mcp__gitnexus__rename,mcp__gitnexus__cypher',
    );
    expect(analyze).toContain('# shellcheck disable=SC2016');
    expect(analyze).toContain('HEAD_SHA: ${{ steps.context.outputs.head_sha }}');
    expect(analyze).toContain('test "$(git -C pr-target rev-parse HEAD)" = "${HEAD_SHA}"');
    expect(analyze).not.toContain(
      'test "$(git -C pr-target rev-parse HEAD)" = "${{ steps.context.outputs.head_sha }}"',
    );
  });

  it('scopes Agent dispatch to exactly the installed ci-personas', () => {
    // Real dispatch cannot be proven without a model turn (print mode silently
    // ignores invalid settings and does not validate permission-rule content at
    // parse time), so the canary is the acceptance gate for that. What a unit
    // test CAN pin is that the scoped allowlist, the persona filenames, and each
    // persona's frontmatter name are the same set — catching a rename or typo in
    // any of the three without auth. Names are read one-per-rule because the
    // pinned action splits allowedTools on commas.
    const analyze = jobBlock('analyze');
    const allowed = analyze.match(/--allowedTools "([^"]+)"/)?.[1] ?? '';
    const allowlistNames = [...allowed.matchAll(/Agent\(([^),]+)\)/g)]
      .map((match) => match[1].trim())
      .sort();

    const personasDir = path.resolve(
      __dirname,
      '../../../.claude/skills/gitnexus-review/ci-personas',
    );
    const personaStems = readdirSync(personasDir)
      .filter((file) => file.endsWith('.md'))
      .map((file) => file.replace(/\.md$/, ''))
      .sort();

    expect(allowlistNames).toEqual(personaStems);

    const frontmatterNames = personaStems.map((stem) => {
      const body = readFileSync(path.join(personasDir, `${stem}.md`), 'utf8');
      return body.match(/^name:\s*(\S+)\s*$/m)?.[1] ?? '';
    });
    expect(frontmatterNames).toEqual(personaStems);

    // The install source the workflow copies matches the directory the
    // allowlist scopes to, so the six names above are the six spawnable agents.
    expect(analyze).toContain('cp -a -- .claude/skills/gitnexus-review/ci-personas/.');
  });

  it('bounds swarm transcript volume with per-persona maxTurns that fit the caps', () => {
    const analyze = jobBlock('analyze');
    const orchestratorTurns = Number(analyze.match(/--max-turns (\d+)/)?.[1] ?? '0');
    const maxMessages = Number(
      (analyze.match(/MAX_TRANSCRIPT_MESSAGES = ([\d_]+)/)?.[1] ?? '0').replace(/_/g, ''),
    );
    expect(orchestratorTurns).toBeGreaterThan(0);
    expect(maxMessages).toBeGreaterThan(0);

    const personasDir = path.resolve(
      __dirname,
      '../../../.claude/skills/gitnexus-review/ci-personas',
    );
    const laneTurns = readdirSync(personasDir)
      .filter((file) => file.endsWith('.md'))
      .map((file) => {
        const body = readFileSync(path.join(personasDir, file), 'utf8');
        const value = Number(body.match(/^maxTurns:\s*(\d+)\s*$/m)?.[1] ?? '0');
        // Every lane declares a positive-integer turn budget so the transcript
        // is deterministically bounded (the runtime rejects non-positive values).
        expect(value).toBeGreaterThan(0);
        return { file, value };
      });
    expect(laneTurns).toHaveLength(6);

    const criticTurns = laneTurns.find((lane) => lane.file === 'ci-critic-lens.md')?.value ?? 0;
    const totalLaneTurns = laneTurns.reduce((sum, lane) => sum + lane.value, 0);
    // Worst case: the orchestrator, every lane once, and a second critic pass,
    // each turn yielding at most an assistant + a user(tool_result) message. The
    // bound must stay under the transcript cap so a full swarm run never bricks a
    // valid review; this fails if maxTurns is bumped without revisiting the cap.
    const worstCaseMessages = 2 * (orchestratorTurns + totalLaneTurns + criticTurns);
    expect(worstCaseMessages).toBeLessThan(maxMessages);
  });

  it('marks the review in progress from a write-scoped job without weakening analyze', () => {
    const acknowledge = jobBlock('acknowledge');
    // A dedicated, write-scoped job posts the in-progress marker under the same
    // authorization gate as analyze, so the model-facing analyze job stays
    // secretless and read-only.
    expect(acknowledge).toContain('pull-requests: write');
    expect(acknowledge).toContain("github.event.comment.body == '@gitnexus review'");
    expect(acknowledge).toContain("author_association == 'OWNER'");
    expect(acknowledge).toContain('<!-- gitnexus-review-agent:progress:');
    expect(acknowledge).toContain('GitNexus review in progress');

    const analyze = jobBlock('analyze');
    expect(analyze).toContain('pull-requests: read');
    expect(analyze).not.toContain('pull-requests: write');

    // The publisher removes the marker when the review — or a clean failure —
    // posts, so a stale "in progress" note never lingers.
    const publish = jobBlock('publish');
    expect(publish).toContain('Remove the in-progress marker');
    expect(publish).toContain('github.rest.issues.deleteComment');
    expect(publish).toContain('<!-- gitnexus-review-agent:progress:');
  });

  it('bounds and validates the structured artifact across the trust boundary', () => {
    const analyze = jobBlock('analyze');
    const publish = jobBlock('publish');
    expect(analyze).toContain('if: always()');
    expect(analyze).toContain('gitnexus.review/v2');
    expect(analyze).toContain('--json-schema');
    expect(analyze).toContain('steps.claude.outputs.structured_output');
    expect(analyze).toContain('steps.claude.outputs.execution_file');
    expect(analyze).toContain('fs.constants.O_NOFOLLOW');
    expect(analyze).toContain('fs.fstatSync(descriptor)');
    expect(analyze).toContain('fs.readSync(descriptor');
    expect(analyze).toContain('fs.closeSync(descriptor)');
    expect(analyze).toContain('Buffer.byteLength');
    expect(analyze).toContain('60_000');
    expect(analyze).toContain('actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a');
    expect(publish).toContain('actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c');
    expect(analyze).toContain('name=gitnexus-review-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}');
    expect(analyze).toContain('artifact_name: ${{ steps.artifact.outputs.name }}');
    expect(analyze).toContain('name: ${{ steps.artifact.outputs.name }}');
    expect(analyze).toContain('fs.appendFileSync(process.env.GITHUB_OUTPUT');
    expect(analyze).toContain('`status=${artifact.status}\\n`');
    expect(analyze).toContain('id: upload');
    expect(analyze).toContain('Fail incomplete analysis after preserving the publisher handoff');
    expect(analyze).toContain("steps.artifact.outputs.status != 'success'");
    expect(analyze.indexOf('id: upload')).toBeLessThan(
      analyze.indexOf('Fail incomplete analysis after preserving the publisher handoff'),
    );
    expect(publish).toContain('name: ${{ needs.analyze.outputs.artifact_name }}');
    expect(publish).toContain('gitnexus.review/v2');
    expect(publish).toContain('Buffer.byteLength');
    expect(publish).toContain('60_000');
    expect(publish).toContain('RESERVED_MARKER_RE');
    expect(publish).toContain('\\u200b');
  });

  it('accepts a structured review only after a substantive exact-path context result', () => {
    const { artifact } = runArtifactScenario();

    expect(artifact).toEqual({
      schema: 'gitnexus.review/v2',
      pr_number: PR_NUMBER,
      control_sha: CONTROL_SHA,
      head_sha: HEAD_SHA,
      base_sha: BASE_SHA,
      status: 'success',
      body: ACCEPTED_BODY,
      failure_code: null,
      graph_evidence: {
        mode: 'context',
        head_has_indexable_symbol: true,
        base_has_indexable_symbol: false,
      },
    });
  });

  it('accepts deletion and rename-old evidence only from the exact merge-base graph', () => {
    const deletedPath = 'gitnexus/src/cli/deleted-command.ts';
    const fromMergeBase = runArtifactScenario({
      basePaths: [deletedPath],
      changedPaths: [],
      rawTranscript: (runnerTemp) =>
        JSON.stringify(
          reviewTranscript({
            toolInput: {
              name: 'deletedCommand',
              file_path: deletedPath,
              repo: path.join(runnerTemp, 'gitnexus-review-merge-base'),
            },
            toolResultContent: contextResultContent(deletedPath),
          }),
        ),
    });
    expect(fromMergeBase.artifact).toMatchObject({
      status: 'success',
      failure_code: null,
      graph_evidence: {
        mode: 'context',
        head_has_indexable_symbol: false,
        base_has_indexable_symbol: true,
      },
    });

    const renameOldPath = 'gitnexus/src/cli/renamed-command.ts';
    const renameNewPath = 'gitnexus/src/cli/current-command.ts';
    const fromRenameOld = runArtifactScenario({
      basePaths: [renameOldPath],
      changedPaths: [renameNewPath],
      entries: [
        {
          status: 'R077',
          base_path: renameOldPath,
          head_path: renameNewPath,
        },
      ],
      rawTranscript: (runnerTemp) =>
        JSON.stringify(
          reviewTranscript({
            toolInput: {
              name: 'renamedCommand',
              file_path: renameOldPath,
              repo: path.join(runnerTemp, 'gitnexus-review-merge-base'),
            },
            toolResultContent: contextResultContent(renameOldPath),
          }),
        ),
    });
    expect(fromRenameOld.artifact).toMatchObject({
      status: 'success',
      failure_code: null,
      graph_evidence: { mode: 'context' },
    });

    const fromDefaultHead = runArtifactScenario({
      basePaths: [deletedPath],
      changedPaths: [],
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolInput: { name: 'deletedCommand', file_path: deletedPath },
          toolResultContent: contextResultContent(deletedPath),
        }),
      ),
    });
    expect(fromDefaultHead.artifact.failure_code).toBe('missing_graph_evidence');
  });

  it('rejects merge-base context for modified paths that are prescan-only there', () => {
    const modifiedPath = 'gitnexus/src/cli/modified-command.ts';
    const fromMergeBase = runArtifactScenario({
      basePrescanPaths: [modifiedPath],
      changedPaths: [modifiedPath],
      entries: [
        {
          status: 'M',
          base_prescan_path: modifiedPath,
          head_path: modifiedPath,
        },
      ],
      rawTranscript: (runnerTemp) =>
        JSON.stringify(
          reviewTranscript({
            toolInput: {
              name: 'modifiedCommand',
              file_path: modifiedPath,
              repo: path.join(runnerTemp, 'gitnexus-review-merge-base'),
            },
            toolResultContent: contextResultContent(modifiedPath),
          }),
        ),
    });

    expect(fromMergeBase.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'missing_graph_evidence',
    });

    const inconsistent = runArtifactScenario({
      basePaths: [modifiedPath],
      basePrescanPaths: [modifiedPath],
      changedPaths: [modifiedPath],
      entries: [
        {
          status: 'M',
          base_prescan_path: modifiedPath,
          head_path: modifiedPath,
        },
      ],
    });
    expect(inconsistent.artifact.failure_code).toBe('invalid_execution_transcript');
    expect(inconsistent.stderr).toContain('changed-path manifest topology is inconsistent');
  });

  it('permits the explicit no-indexable mode only when the trusted prescan proves it', () => {
    const accepted = runArtifactScenario({
      changedPaths: ['docs/review-agent.md'],
      noIndexableChangedSymbols: true,
      rawTranscript: JSON.stringify(reviewTranscriptWithoutTools()),
    });
    expect(accepted.artifact).toMatchObject({
      status: 'success',
      failure_code: null,
      graph_evidence: {
        mode: 'no_indexable_changed_symbols',
        head_has_indexable_symbol: false,
        base_has_indexable_symbol: false,
      },
    });

    const rejected = runArtifactScenario({
      changedPaths: [CHANGED_PATH],
      noIndexableChangedSymbols: false,
      rawTranscript: JSON.stringify(reviewTranscriptWithoutTools()),
    });
    expect(rejected.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'missing_graph_evidence',
      graph_evidence: null,
    });
  });

  it('rejects graph evidence that only a subagent sidechain produced', () => {
    const sidechainOnly = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ parentToolUseId: 'toolu-parent-1' })),
    });
    expect(sidechainOnly.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'missing_graph_evidence',
    });

    const side = reviewTranscript({
      parentToolUseId: 'toolu-parent-1',
      toolUseId: 'tool-side-1',
    });
    const main = reviewTranscript();
    const combined = [main[0], side[1], side[2], main[1], main[2], main[3]];
    const withMainline = runArtifactScenario({
      rawTranscript: JSON.stringify(combined),
    });
    expect(withMainline.artifact).toMatchObject({
      status: 'success',
      failure_code: null,
    });

    const malformed = reviewTranscript();
    (malformed[1] as Record<string, unknown>).parent_tool_use_id = 42;
    const invalidLinkage = runArtifactScenario({
      rawTranscript: JSON.stringify(malformed),
    });
    expect(invalidLinkage.artifact.failure_code).toBe('invalid_execution_transcript');
    expect(invalidLinkage.stderr).toContain('parent linkage');
  });

  it('pins each sidechain guard independently with cross-wired transcripts', () => {
    // A real sidechain turn carries parent_tool_use_id on BOTH its call and its
    // result, so the two !sidechain guards are mutually redundant on realistic
    // input — deleting either alone would still pass the symmetric fixtures.
    // These asymmetric fixtures isolate each guard.

    // Mainline call + sidechain result: the mainline call registers an evidence
    // candidate, but the result is sidechain — only the acceptance-side guard
    // (registration already happened) can reject it.
    const mainCallSidechainResult = reviewTranscript();
    (mainCallSidechainResult[2] as Record<string, unknown>).parent_tool_use_id = 'toolu-parent-1';
    expect(
      runArtifactScenario({ rawTranscript: JSON.stringify(mainCallSidechainResult) }).artifact,
    ).toMatchObject({
      status: 'failure',
      failure_code: 'missing_graph_evidence',
    });

    // Sidechain call + mainline result: only the registration-side guard stops
    // the sidechain call from becoming a candidate the mainline result satisfies.
    const sidechainCallMainResult = reviewTranscript();
    (sidechainCallMainResult[1] as Record<string, unknown>).parent_tool_use_id = 'toolu-parent-1';
    expect(
      runArtifactScenario({ rawTranscript: JSON.stringify(sidechainCallMainResult) }).artifact,
    ).toMatchObject({
      status: 'failure',
      failure_code: 'missing_graph_evidence',
    });
  });

  it('rejects non-context tools and context calls not tied to an exact changed path', () => {
    const listOnly = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolName: 'mcp__gitnexus__list_repos',
          toolInput: {},
        }),
      ),
    });
    expect(listOnly.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'missing_graph_evidence',
    });
    expect(listOnly.artifact.body).toContain('successful GitNexus context result');
    expect(listOnly.stderr).toContain('no substantive exact-path GitNexus context result');

    const unknownRepo = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolInput: { name: 'statusCommand', repo: '/tmp/some-other-checkout' },
        }),
      ),
    });
    expect(unknownRepo.artifact.failure_code).toBe('missing_graph_evidence');

    const failedQuery = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ resultIsError: true })),
    });
    expect(failedQuery.artifact.failure_code).toBe('missing_graph_evidence');
  });

  it('accepts a name-only context call whose result resolves an exact changed path', () => {
    // The gate proves evidence from the RESULT, so the plain context({name})
    // call the review skill teaches counts; requiring file_path in the call
    // starved the gate: 17 of 26 real review-agent run failures were this.
    const nameOnly = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ toolInput: { name: 'statusCommand' } })),
    });
    expect(nameOnly.artifact).toMatchObject({
      status: 'success',
      failure_code: null,
      graph_evidence: { mode: 'context' },
    });

    const uidOnly = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolInput: { uid: 'Function:gitnexus/src/cli/status.ts:statusCommand' },
        }),
      ),
    });
    expect(uidOnly.artifact).toMatchObject({
      status: 'success',
      failure_code: null,
      graph_evidence: { mode: 'context' },
    });

    const noSelector = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ toolInput: { kind: 'Function' } })),
    });
    expect(noSelector.artifact.failure_code).toBe('missing_graph_evidence');
  });

  it('diagnoses why an unproven review was rejected', () => {
    const offPath = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({ toolResultContent: contextResultContent('gitnexus/src/cli/index.ts') }),
      ),
    });
    expect(offPath.stderr).toContain('Evidence diagnosis: orchestrator context calls in scope: 1');
    expect(offPath.stderr).toContain('results outside the changed paths: 1');
    expect(offPath.stderr).toContain('gitnexus/src/cli/index.ts');

    const sidechainOnly = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ parentToolUseId: 'toolu-parent-1' })),
    });
    expect(sidechainOnly.stderr).toContain('sidechain context calls ignored: 1');

    // Without this counter "in scope: 0" cannot distinguish a review that never
    // called context from one that called it against an unrecognized repo.
    const unknownRepo = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({ toolInput: { name: 'statusCommand', repo: '/tmp/other-checkout' } }),
      ),
    });
    expect(unknownRepo.stderr).toContain(
      'orchestrator context calls out of scope (no selector or unknown repo): 1',
    );

    const noCalls = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscriptWithoutTools()),
    });
    expect(noCalls.stderr).toContain('orchestrator context calls in scope: 0');
    expect(noCalls.stderr).toContain(
      'orchestrator context calls out of scope (no selector or unknown repo): 0',
    );

    // An in-scope call whose result errored must not read as "no calls made".
    const erroredResult = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ resultIsError: true })),
    });
    expect(erroredResult.stderr).toContain('in-scope calls with no usable result: 1 (errored 1');

    const unresolved = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolResultContent: `${JSON.stringify({ error: "Symbol 'x' not found" })}\n\n---\n**Next:** retry.`,
        }),
      ),
    });
    expect(unresolved.stderr).toContain('results that resolved nothing: 1');
  });

  it('treats a malformed context payload as non-evidence, not as a corrupt transcript', () => {
    // The MCP truncates any context payload over GITNEXUS_MCP_DEFAULT_MAX_TOKENS
    // mid-JSON. Every orchestrator context call is a candidate, so throwing on a
    // payload-shape failure would let one truncated exploratory call discard a
    // review that an earlier call already proved.
    const proved = contextResultContent();
    const truncated = `${JSON.stringify({ status: 'found', symbol: { uid: 'u' } }).slice(0, 30)}\n…`;
    const provedThenTruncated = runArtifactScenario({
      rawTranscript: JSON.stringify(
        twoCallTranscript({ firstResult: proved, secondResult: truncated }),
      ),
    });
    expect(provedThenTruncated.artifact).toMatchObject({
      status: 'success',
      failure_code: null,
      graph_evidence: { mode: 'context' },
    });

    // With no proving call, the same truncated payload is counted, not thrown.
    const truncatedOnly = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ toolResultContent: truncated })),
    });
    expect(truncatedOnly.artifact.failure_code).toBe('missing_graph_evidence');
    expect(truncatedOnly.stderr).toContain('results too malformed or truncated to parse: 1');

    // Structural transcript invariants must still fail closed.
    const structural = runArtifactScenario({ rawTranscript: '{not-json' });
    expect(structural.artifact.failure_code).toBe('invalid_execution_transcript');
  });

  it('refuses a bare File node as evidence, matching the prescan definition of indexable', () => {
    const fileNode = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolInput: { name: 'status.ts' },
          toolResultContent: JSON.stringify({
            status: 'found',
            symbol: {
              uid: `File:${CHANGED_PATH}`,
              name: 'status.ts',
              kind: 'File',
              filePath: CHANGED_PATH,
            },
          }),
        }),
      ),
    });
    expect(fileNode.artifact.failure_code).toBe('missing_graph_evidence');
    expect(fileNode.stderr).toContain('results that resolved nothing: 1');
  });

  it('scopes a deletion-only PR to the merge-base set instead of an empty head set', () => {
    const deletedPath = 'gitnexus/src/cli/deleted-command.ts';
    const headScopedCall = runArtifactScenario({
      basePaths: [deletedPath],
      changedPaths: [],
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolInput: { name: 'deletedCommand' },
          toolResultContent: contextResultContent(deletedPath),
        }),
      ),
    });
    // headPaths is empty, so the call can never be satisfied: report it as out
    // of scope rather than as a result "outside the changed paths".
    expect(headScopedCall.artifact.failure_code).toBe('missing_graph_evidence');
    expect(headScopedCall.stderr).toContain('orchestrator context calls in scope: 0');
    expect(headScopedCall.stderr).toContain(
      'orchestrator context calls out of scope (no selector or unknown repo): 1',
    );
    expect(headScopedCall.stderr).toContain('results outside the changed paths: 0');
  });

  it('publishes an incomplete analysis as a labelled failure, never as an accepted review', () => {
    const incomplete = runArtifactScenario({
      structuredOutput: JSON.stringify({
        body: `Partial review, two lanes died. ${'The correctness lane covered the changed parser path. '.repeat(4)}`,
        complete: false,
      }),
    });
    expect(incomplete.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'incomplete_analysis',
      graph_evidence: null,
    });
    expect(incomplete.artifact.body).toContain('could not complete this analysis');
    expect(incomplete.artifact.body).toContain('Partial review, two lanes died');
    expect(incomplete.stderr).toContain('the model reported an incomplete analysis');

    const missingField = runArtifactScenario({
      structuredOutput: JSON.stringify({ body: 'No completeness signal' }),
    });
    expect(missingField.artifact.failure_code).toBe('invalid_model_output');
  });

  it('accepts SDK text-block results with omitted is_error', () => {
    const result = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          resultIsError: undefined,
          toolResultContent: [{ type: 'text', text: contextResultContent() }],
        }),
      ),
    });

    expect(result.artifact).toMatchObject({ status: 'success', failure_code: null });
  });

  it('rejects semantic errors, no-results, and context results for another file', () => {
    const semanticError = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolResultContent: `${JSON.stringify({ error: "Symbol 'statusCommand' not found" })}\n\n---\n**Next:** retry.`,
        }),
      ),
    });
    expect(semanticError.artifact.failure_code).toBe('missing_graph_evidence');

    const noResults = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ toolResultContent: 'No results found.' })),
    });
    expect(noResults.artifact.failure_code).toBe('missing_graph_evidence');

    const wrongPath = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({ toolResultContent: contextResultContent('gitnexus/src/cli/index.ts') }),
      ),
    });
    expect(wrongPath.artifact.failure_code).toBe('missing_graph_evidence');
  });

  it('separates malformed context payloads from structurally invalid tool results', () => {
    // Payload shape is the MCP's business and can fail for benign reasons
    // (truncation at the output budget), so it demotes one call to non-evidence.
    const malformed = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ toolResultContent: '{not-json' })),
    });
    expect(malformed.artifact.failure_code).toBe('missing_graph_evidence');
    expect(malformed.stderr).toContain('results too malformed or truncated to parse: 1');

    // An empty tool_result is a transcript-structural violation, not a payload
    // shape, and still fails the whole run closed.
    const empty = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscript({ toolResultContent: '   ' })),
    });
    expect(empty.artifact.failure_code).toBe('invalid_execution_transcript');
    expect(empty.stderr).toContain('tool result content is empty');
  });

  it('fails closed on malformed execution transcript data', () => {
    const malformed = runArtifactScenario({ rawTranscript: '{not-json' });

    expect(malformed.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'invalid_execution_transcript',
    });
    expect(malformed.artifact.body).toContain('failed strict validation');
    expect(malformed.stderr).toContain('execution transcript validation failed');
  });

  it('fails closed before parsing an oversized execution transcript', () => {
    const oversized = runArtifactScenario({
      rawTranscript: new Uint8Array(8_000_001).fill(0x20),
    });

    expect(oversized.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'invalid_execution_transcript',
    });
    expect(oversized.stderr).toContain('execution transcript type or size is invalid');
  });

  it('publishes idempotently and discards every stale tuple', () => {
    const publish = jobBlock('publish');
    expect(publish).toContain('github.rest.pulls.get');
    expect(publish).toContain('github-actions[bot]');
    expect(publish).toContain('gitnexus-review-agent:');
    expect(publish).toContain('sameShaComment');
    expect(publish).toContain('analyzedTupleValid');
    expect(publish).toContain('publicationHead');
    expect(publish).toContain('no model output was accepted');
    expect(publish).toContain('currentBase !== baseSha');
    expect(publish).toContain('if (isStale)');
    expect(publish).toContain('stale output was discarded');
    expect(publish).toContain('github.paginate.iterator');
    expect(publish).toContain('MAX_COMMENT_PAGES = 20');
    expect(publish).toContain('MAX_COMMENTS = 2_000');
    expect(publish).toContain('sameShaComment && !publicationSucceeded');
    expect(publish).not.toContain('currentHeadComment');
    expect(publish).toContain('github.rest.issues.updateComment');
    expect(publish).toContain('github.rest.issues.createComment');
  });

  it('preserves an existing same-tuple comment when a rerun fails', async () => {
    const existing = {
      id: 17,
      user: { login: 'github-actions[bot]' },
      body: `<!-- gitnexus-review-agent:${PR_NUMBER}:${HEAD_SHA}:${BASE_SHA} -->\nAccepted earlier review`,
    };

    const { createComment, updateComment } = await runPublisherScenario({
      artifactStatus: 'failure',
      comments: [existing],
    });

    expect(updateComment).not.toHaveBeenCalled();
    expect(createComment).not.toHaveBeenCalled();
  });

  it('replaces a same-tuple failure with a later accepted review', async () => {
    const existing = {
      id: 18,
      user: { login: 'github-actions[bot]' },
      body: `<!-- gitnexus-review-agent:${PR_NUMBER}:${HEAD_SHA}:${BASE_SHA} -->\nEarlier failure`,
    };

    const { createComment, updateComment } = await runPublisherScenario({
      artifactStatus: 'success',
      comments: [existing],
    });

    expect(createComment).not.toHaveBeenCalled();
    expect(updateComment).toHaveBeenCalledOnce();
    expect(updateComment.mock.calls[0]?.[0]).toMatchObject({
      comment_id: existing.id,
    });
    expect(updateComment.mock.calls[0]?.[0].body).toContain('Accepted review body');
  });

  it('re-fetches the PR tuple immediately before update or create and rejects a late move', async () => {
    const movedHead = '7'.repeat(40);
    const result = await runPublisherScenario({
      artifactStatus: 'success',
      finalHead: movedHead,
    });

    expect(result.getPull).toHaveBeenCalledTimes(2);
    expect(result.paginate).toHaveBeenCalledOnce();
    expect(result.updateComment).not.toHaveBeenCalled();
    expect(result.createComment).not.toHaveBeenCalled();
    expect(result.core.setFailed).toHaveBeenCalledWith(
      expect.stringContaining('changed immediately before publication'),
    );
  });

  it('publishes an initial failure fallback but fails every stale tuple', async () => {
    const failed = await runPublisherScenario({ artifactStatus: 'failure' });
    expect(failed.updateComment).not.toHaveBeenCalled();
    expect(failed.createComment).toHaveBeenCalledOnce();
    expect(failed.createComment.mock.calls[0]?.[0].body).toContain(
      'GitNexus review — unable to complete',
    );

    const stale = await runPublisherScenario({
      artifactStatus: 'success',
      currentHead: '4'.repeat(40),
    });
    expect(stale.updateComment).not.toHaveBeenCalled();
    expect(stale.createComment).not.toHaveBeenCalled();
    expect(stale.paginate).not.toHaveBeenCalled();
    expect(stale.core.setFailed).toHaveBeenCalledWith(expect.stringContaining('stale output'));

    const staleBase = await runPublisherScenario({
      artifactStatus: 'success',
      currentBase: '5'.repeat(40),
    });
    expect(staleBase.createComment).not.toHaveBeenCalled();
    expect(staleBase.paginate).not.toHaveBeenCalled();
    expect(staleBase.core.setFailed).toHaveBeenCalledWith(expect.stringContaining('stale output'));
  });

  it('rejects malformed and mismatched artifacts at the publisher boundary', async () => {
    const scenarios: PublisherScenario[] = [
      { artifactStatus: 'success', rawArtifact: '{not-json' },
      { artifactStatus: 'success', rawArtifact: Uint8Array.from([0xff, 0xfe]) },
      { artifactStatus: 'success', artifactOverrides: { unexpected: true } },
      { artifactStatus: 'success', artifactOverrides: { base_sha: '6'.repeat(40) } },
      { artifactStatus: 'success', artifactOverrides: { body: 'x'.repeat(54_001) } },
      { artifactStatus: 'success', artifactOverrides: { failure_code: 'model_failed' } },
      {
        artifactStatus: 'success',
        artifactOverrides: {
          graph_evidence: {
            mode: 'no_indexable_changed_symbols',
            head_has_indexable_symbol: true,
            base_has_indexable_symbol: false,
          },
        },
      },
    ];

    for (const scenario of scenarios) {
      const result = await runPublisherScenario(scenario);
      expect(result.updateComment).not.toHaveBeenCalled();
      expect(result.createComment).toHaveBeenCalledOnce();
      expect(result.createComment.mock.calls[0]?.[0].body).toContain('failed safely');
      expect(result.core.warning).toHaveBeenCalledOnce();
    }
  });

  it('streams bounded comment pages and keeps only the latest matching marker', async () => {
    const first = {
      id: 20,
      user: { login: 'github-actions[bot]' },
      body: `<!-- gitnexus-review-agent:${PR_NUMBER}:${HEAD_SHA}:${BASE_SHA} -->\nFirst`,
    };
    const latest = { ...first, id: 21, body: `${first.body}\nLatest` };
    const bounded = await runPublisherScenario({
      artifactStatus: 'success',
      commentPages: [[first], [latest]],
    });
    expect(bounded.paginate).toHaveBeenCalledOnce();
    expect(bounded.updateComment).toHaveBeenCalledOnce();
    expect(bounded.updateComment.mock.calls[0]?.[0]).toMatchObject({ comment_id: latest.id });

    const overCap = await runPublisherScenario({
      artifactStatus: 'success',
      commentPages: Array.from({ length: 21 }, () => []),
    });
    expect(overCap.createComment).not.toHaveBeenCalled();
    expect(overCap.updateComment).not.toHaveBeenCalled();
    expect(overCap.core.setFailed).toHaveBeenCalledWith(
      expect.stringContaining('exceeded the bounded publication scan'),
    );
  });
  it('retries a failing pinned install, then gives up, and stops on first success', () => {
    // The string assertions above cannot tell a working retry from a loop whose
    // `npm ci` was moved outside it, so drive the real helper with a stub npm.
    const script = path.resolve(__dirname, '../../../.github/scripts/npm-ci-retry.sh');
    const runHelper = (failures: number) => {
      const dir = mkdtempSync(path.join(tmpdir(), 'npm-ci-retry-'));
      const counter = path.join(dir, 'attempts');
      writeFileSync(counter, '');
      writeFileSync(
        path.join(dir, 'npm'),
        `#!/usr/bin/env bash\nprintf 'x' >> ${counter}\nattempts=$(wc -c < ${counter})\n` +
          `if [ "$attempts" -le ${failures} ]; then exit 1; fi\nexit 0\n`,
        { mode: 0o755 },
      );
      writeFileSync(path.join(dir, 'sleep'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
      const result = spawnSync('bash', [script, 'test runtime', dir, path.join(dir, '.npmrc')], {
        encoding: 'utf8',
        env: { ...process.env, PATH: `${dir}:${process.env.PATH ?? ''}` },
      });
      const attempts = readFileSync(counter, 'utf8').length;
      rmSync(dir, { recursive: true, force: true });
      return { status: result.status, stderr: result.stderr, attempts };
    };

    expect(runHelper(0)).toMatchObject({ status: 0, attempts: 1 });
    const recovered = runHelper(2);
    expect(recovered).toMatchObject({ status: 0, attempts: 3 });
    expect(recovered.stderr).toContain('retrying (1/3)');
    const exhausted = runHelper(3);
    expect(exhausted).toMatchObject({ status: 1, attempts: 3 });
    expect(exhausted.stderr).toContain('failed after 3 attempts');
  });

  it('ships the install helper executable, since the workflow invokes it directly', () => {
    // Committed as 100644 it would fail on the runner with permission denied,
    // and no other check in this suite would notice.
    const mode = spawnSync('git', ['ls-files', '-s', '.github/scripts/npm-ci-retry.sh'], {
      cwd: path.resolve(__dirname, '../../..'),
      encoding: 'utf8',
    }).stdout;
    expect(mode.startsWith('100755')).toBe(true);
  });

  it('bounds every install attempt so a hung registry cannot eat the job budget', () => {
    const helper = readFileSync(
      path.resolve(__dirname, '../../../.github/scripts/npm-ci-retry.sh'),
      'utf8',
    );
    expect(helper).toContain('timeout "${attempt_timeout}" npm ci');
    expect(helper).toContain('NPM_CI_ATTEMPT_TIMEOUT_SECONDS:-600');
    expect(helper).toContain('set -euo pipefail');
  });

  it('rejects a context result whose filePath is not a string', () => {
    // The transcript is treated as hostile-adjacent data: a non-string filePath
    // must be a clean reject, never an uncaught type error.
    const nonString = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolResultContent: JSON.stringify({
            status: 'found',
            symbol: { uid: 'u', name: 'n', filePath: 42, startLine: 1, endLine: 2 },
          }),
        }),
      ),
    });
    expect(nonString.artifact.failure_code).toBe('missing_graph_evidence');
    expect(nonString.stderr).toContain('results outside the changed paths: 1');
  });

  it('sanitizes an adversarial resolved path before it reaches the job log', () => {
    const hostile = 'src/\u001b[31m\n::set-output name=x::y\u202egnp.js';
    const sanitized = runArtifactScenario({
      rawTranscript: JSON.stringify(
        reviewTranscript({
          toolResultContent: JSON.stringify({
            status: 'found',
            symbol: { uid: 'u', name: 'n', filePath: hostile, startLine: 1, endLine: 2 },
          }),
        }),
      ),
    });
    expect(sanitized.artifact.failure_code).toBe('missing_graph_evidence');
    expect(sanitized.stderr).not.toContain('::set-output');
    expect(sanitized.stderr).not.toContain('\u001b');
    expect(sanitized.stderr).not.toContain('\u202e');
    expect(sanitized.stderr).toContain('src/?');
  });

  it('names the transcript shape when the envelope is rejected', () => {
    const wrongFirstMessage = runArtifactScenario({
      rawTranscript: JSON.stringify([
        { type: 'assistant', message: { role: 'assistant', content: [] } },
        { type: 'result', subtype: 'success', is_error: false },
      ]),
    });
    expect(wrongFirstMessage.artifact.failure_code).toBe('invalid_execution_transcript');
    expect(wrongFirstMessage.stderr).toContain('2 messages, first assistant/undefined');
  });

  it('counts an in-scope call whose result never arrives', () => {
    const noResult = runArtifactScenario({
      rawTranscript: JSON.stringify([
        { type: 'system', subtype: 'init', session_id: 's', uuid: '1' },
        {
          type: 'assistant',
          parent_tool_use_id: null,
          session_id: 's',
          uuid: '2',
          message: {
            role: 'assistant',
            content: [
              {
                type: 'tool_use',
                id: 'tool-1',
                name: 'mcp__gitnexus__context',
                input: { name: 'statusCommand' },
              },
            ],
          },
        },
        { type: 'result', subtype: 'success', is_error: false, session_id: 's', uuid: '3' },
      ]),
    });
    expect(noResult.artifact.failure_code).toBe('missing_graph_evidence');
    expect(noResult.stderr).toContain('in-scope calls with no usable result: 1');
  });

  it('always clears the in-progress marker, even when analysis was never authorized', () => {
    // The acknowledge job posts the marker from the event alone, so gating the
    // whole publish job on authorization stranded it on the PR forever.
    const publish = jobBlock('publish');
    expect(publish).toContain('if: always()');
    expect(publish).toContain('- name: Remove the in-progress marker');
    const removalIndex = publish.indexOf('- name: Remove the in-progress marker');
    const gatedIndex = publish.indexOf(
      'Validate freshness and upsert an accepted same-SHA comment',
    );
    expect(gatedIndex).toBeGreaterThan(-1);
    expect(removalIndex).toBeGreaterThan(gatedIndex);
    // Publication itself stays authorization-gated.
    expect(publish).toContain("needs.analyze.outputs.authorized == 'true'");
  });

  it('refuses a stub body even when the model admits it is incomplete', () => {
    // A production run returned {"body":"placeholder","complete":false}: the
    // gate had already been satisfied, so this published a maintainer-visible
    // comment whose entire content was that word.
    const stub = runArtifactScenario({
      structuredOutput: JSON.stringify({ body: 'placeholder', complete: false }),
    });
    expect(stub.artifact).toMatchObject({
      status: 'failure',
      failure_code: 'invalid_model_output',
    });
    expect(stub.artifact.body).not.toContain('placeholder');

    const stubButComplete = runArtifactScenario({
      structuredOutput: JSON.stringify({ body: 'LGTM', complete: true }),
    });
    expect(stubButComplete.artifact.failure_code).toBe('invalid_model_output');

    // A real partial review still publishes, labelled incomplete.
    const realPartial = runArtifactScenario({
      structuredOutput: JSON.stringify({
        body: `**REQUEST CHANGES.** ${'This is a genuine partial review of the diff. '.repeat(6)}`,
        complete: false,
      }),
    });
    expect(realPartial.artifact.failure_code).toBe('incomplete_analysis');
    expect(realPartial.artifact.body).toContain('genuine partial review');
  });

  it('reports swarm dispatch from the transcript on every run', () => {
    const laneTranscript = (lanes: number) => {
      const messages: Array<Record<string, unknown>> = [
        { type: 'system', subtype: 'init', session_id: 's', uuid: '1' },
        {
          type: 'assistant',
          parent_tool_use_id: null,
          session_id: 's',
          uuid: '2',
          message: {
            role: 'assistant',
            content: [
              ...Array.from({ length: lanes }, (_unused, index) => ({
                type: 'tool_use',
                id: `lane-${index}`,
                name: 'Agent',
                input: { subagent_type: 'ci-correctness-lens' },
              })),
              {
                type: 'tool_use',
                id: 'tool-1',
                name: 'mcp__gitnexus__context',
                input: { name: 'statusCommand' },
              },
            ],
          },
        },
        ...Array.from({ length: lanes }, (_unused, index) => ({
          type: 'assistant',
          parent_tool_use_id: `lane-${index}`,
          session_id: 's',
          uuid: `lane-turn-${index}`,
          message: { role: 'assistant', content: [] },
        })),
        {
          type: 'user',
          parent_tool_use_id: null,
          session_id: 's',
          uuid: '3',
          message: {
            role: 'user',
            content: [
              {
                type: 'tool_result',
                tool_use_id: 'tool-1',
                is_error: false,
                content: contextResultContent(),
              },
            ],
          },
        },
        { type: 'result', subtype: 'success', is_error: false, session_id: 's', uuid: '4' },
      ];
      return JSON.stringify(messages);
    };

    const dispatched = runArtifactScenario({ rawTranscript: laneTranscript(6) });
    expect(dispatched.artifact.failure_code).toBeNull();
    expect(dispatched.stdout).toContain(
      'Swarm dispatch: lane dispatches requested: 6; lanes that produced transcript turns: 6',
    );

    // The failure this exists to expose: dispatch attempted, nothing came back.
    const silent = runArtifactScenario({ rawTranscript: laneTranscript(0) });
    expect(silent.stdout).toContain(
      'Swarm dispatch: lane dispatches requested: 0; lanes that produced transcript turns: 0',
    );
  });
  it('rejects a review that cites a location which does not exist', () => {
    const cite = (sha: string, file: string, line: string) =>
      `https://github.com/owner/repo/blob/${sha}/${file}#L${line}`;
    const withBody = (link: string) =>
      runArtifactScenario({
        structuredOutput: JSON.stringify({
          body: `**APPROVE.** ${'Reviewed the changed surface in detail. '.repeat(5)} See [here](${link}).`,
          complete: true,
        }),
      });

    const real = withBody(cite(HEAD_SHA, CHANGED_PATH, '12-L20'));
    expect(real.artifact).toMatchObject({ status: 'success', failure_code: null });
    expect(real.stdout).toContain('1 checked, 1 resolve, 1 land in the diff, 0 unverifiable');

    const pastEof = withBody(cite(HEAD_SHA, CHANGED_PATH, '900'));
    expect(pastEof.artifact.failure_code).toBe('unverifiable_citations');
    expect(pastEof.stderr).toContain('cites line 900 of a 40-line file');

    const missingFile = withBody(cite(HEAD_SHA, 'gitnexus/src/cli/invented.ts', '3'));
    expect(missingFile.artifact.failure_code).toBe('unverifiable_citations');
    expect(missingFile.stderr).toContain('cites a path that does not exist at that commit');

    const foreignSha = withBody(cite('f'.repeat(40), CHANGED_PATH, '3'));
    expect(foreignSha.artifact.failure_code).toBe('unverifiable_citations');
    expect(foreignSha.stderr).toContain('cites a commit that was not analyzed');

    // The published body never carries the unverifiable text.
    expect(missingFile.artifact.body).toContain('do not exist at the analyzed commits');
    expect(missingFile.artifact.body).not.toContain('invented.ts');
  });

  it('allows citing an unchanged file, and reports grounding without enforcing it', () => {
    // A caller the change breaks lives outside the diff; citing it is correct
    // review work, so existence is enforced and diff-membership is only logged.
    const unchanged = 'gitnexus/src/cli/untouched.ts';
    const result = runArtifactScenario({
      changedPaths: [CHANGED_PATH, unchanged],
      structuredOutput: JSON.stringify({
        body: `**APPROVE.** ${'Reviewed the changed surface. '.repeat(6)} See [caller](https://github.com/owner/repo/blob/${HEAD_SHA}/${unchanged}#L5).`,
        complete: true,
      }),
    });
    expect(result.artifact).toMatchObject({ status: 'success', failure_code: null });
    expect(result.stdout).toContain('1 checked, 1 resolve, 1 land in the diff');

    const noCitations = runArtifactScenario();
    expect(noCitations.artifact.failure_code).toBeNull();
    expect(noCitations.stdout).toContain('0 checked, 0 resolve, 0 land in the diff');
  });
  it('hands the rejection reason back to the model and publishes the repaired review', () => {
    // Before this, every rejection was terminal: the gate runs after the
    // transcript closes, so the model never learned why it failed.
    const analyze = jobBlock('analyze');
    expect(analyze).toContain('- name: Check the model result before the transcript closes');
    expect(analyze).toContain('review-precheck.cjs');
    expect(analyze).toContain("steps.precheck.outputs.repair_reason != ''");
    // The binary is re-verified before the secret is exposed a second time.
    const recheckIndex = analyze.indexOf(
      '- name: Reverify exact Claude executable before the repair',
    );
    const repairIndex = analyze.indexOf(
      '- name: Repair the review once when the first result is unpublishable',
    );
    expect(recheckIndex).toBeGreaterThan(-1);
    expect(repairIndex).toBeGreaterThan(recheckIndex);
    expect(analyze).toContain("steps.repair-recheck.outcome == 'success'");
    // The repair is bounded well below the first attempt.
    const turnCaps = [...analyze.matchAll(/--max-turns (\d+)/g)].map((match) => Number(match[1]));
    expect(turnCaps).toEqual([150, 60]);

    const repaired = runArtifactScenario({
      structuredOutput: JSON.stringify({ body: 'placeholder', complete: false }),
      repairStructuredOutput: JSON.stringify({
        body: `**APPROVE.** ${'The repaired review covers the changed surface. '.repeat(5)}`,
        complete: true,
      }),
    });
    expect(repaired.artifact).toMatchObject({ status: 'success', failure_code: null });
    expect(repaired.artifact.body).toContain('repaired review covers');
    expect(repaired.stdout).toContain('Publishing the repaired review');

    // A repair that itself fails must not rescue the rejected first result.
    const repairFailed = runArtifactScenario({
      structuredOutput: JSON.stringify({ body: 'placeholder', complete: false }),
      repairOutcome: 'failure',
    });
    expect(repairFailed.artifact.failure_code).toBe('invalid_model_output');
  });

  it('skips a request whose exact head and base already carry an accepted review', async () => {
    // Real PRs took two and three full runs each; a repeat request reproduced a
    // comment that was already on the page, at full model cost.
    const marker = `<!-- gitnexus-review-agent:${PR_NUMBER}:${HEAD_SHA}:${BASE_SHA} -->`;
    const accepted = await runContextScenario({
      permission: 'write',
      existingComments: [
        { user: { login: 'github-actions[bot]' }, body: `${marker}\n**APPROVE.** Looks good.` },
      ],
    });
    expect(accepted.outputs.get('ready')).toBe('false');
    expect(accepted.outputs.get('failure_code')).toBe('already_reviewed');

    // A previous FAILURE at the same tuple must still be retryable.
    const previouslyFailed = await runContextScenario({
      permission: 'write',
      existingComments: [
        {
          user: { login: 'github-actions[bot]' },
          body: `${marker}\n### GitNexus review — unable to complete\n\nnope`,
        },
      ],
    });
    expect(previouslyFailed.outputs.get('ready')).toBe('true');

    // A review of a different commit does not suppress this one.
    const otherCommit = await runContextScenario({
      permission: 'write',
      existingComments: [
        {
          user: { login: 'github-actions[bot]' },
          body: `<!-- gitnexus-review-agent:${PR_NUMBER}:${'9'.repeat(40)}:${BASE_SHA} -->\nold`,
        },
      ],
    });
    expect(otherCommit.outputs.get('ready')).toBe('true');

    // And a human comment quoting the marker cannot suppress a review.
    const impostor = await runContextScenario({
      permission: 'write',
      existingComments: [{ user: { login: 'someone' }, body: `${marker}\nlooks fine to me` }],
    });
    expect(impostor.outputs.get('ready')).toBe('true');
  });

  it('stops before the model when the pull request moved during preparation', () => {
    const analyze = jobBlock('analyze');
    expect(analyze).toContain(
      '- name: Confirm the pull request has not moved before spending the model',
    );
    expect(analyze).toContain("steps.freshness.outcome == 'success'");
    const freshnessIndex = analyze.indexOf('id: freshness');
    const modelIndex = analyze.indexOf('- name: Run read-only graph-backed review');
    expect(freshnessIndex).toBeGreaterThan(-1);
    expect(modelIndex).toBeGreaterThan(freshnessIndex);
    const freshnessScript = jobScript(
      'analyze',
      'Confirm the pull request has not moved before spending the model',
    );
    expect(freshnessScript).toContain('core.setFailed');
    expect(freshnessScript).toContain('stopping before the model runs');
  });

  it('records what each run spent so waste is visible without grepping logs', () => {
    const spent = runArtifactScenario();
    expect(spent.stdout).toContain('Model spend: turns: 25; cost: $3.88.');

    // A transcript without the fields must not break the run.
    const unknown = runArtifactScenario({
      rawTranscript: JSON.stringify(reviewTranscriptWithoutTools()),
      changedPaths: ['docs/x.md'],
      noIndexableChangedSymbols: true,
    });
    expect(unknown.stdout).toContain('Model spend: turns: unknown; cost: unknown.');
  });
});
