/**
 * Port drift guard — ensures .env.example.opensource ports stay consistent
 * with sync-to-opensource.sh transforms.
 *
 * Root cause of clowder-ai#87 / #55 / #56: the .env.example.opensource had
 * API_SERVER_PORT and FRONTEND_PORT swapped relative to the code defaults
 * that sync-to-opensource.sh produces. This test prevents that from recurring.
 *
 * Convention (set by _sanitize-rules.pl + sync-to-opensource.sh):
 *   Home:        API=3002, Frontend=3001
 *   Open-source: API=3004, Frontend=3003
 *   Redis:       stays 6399 in both repos
 *   (API = Frontend + 1 in both environments)
 */
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, resolve } from 'node:path';
import { describe, it } from 'node:test';

const ROOT = resolve(process.cwd());

// Detect repo context early — used by multiple describe blocks.
// Home repo has sync-to-opensource.sh; open-source repo does not.
const isHomeRepo = existsSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'));
const hasEnvExampleOpensource = existsSync(resolve(ROOT, '.env.example.opensource'));

function readEnvFile(relPath) {
  const content = readFileSync(resolve(ROOT, relPath), 'utf-8');
  const vars = {};
  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    const eqIdx = trimmed.indexOf('=');
    if (eqIdx < 0) continue;
    const key = trimmed.slice(0, eqIdx).trim();
    const val = trimmed.slice(eqIdx + 1).trim();
    vars[key] = val;
  }
  return vars;
}

function readEnvTemplateKeys(relPath) {
  const content = readFileSync(resolve(ROOT, relPath), 'utf-8');
  const keys = new Set();

  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    if (!trimmed) continue;

    const activeMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
    if (activeMatch) {
      keys.add(activeMatch[1]);
      continue;
    }

    const commentedMatch = trimmed.match(/^#\s*([A-Za-z_][A-Za-z0-9_]*)=/);
    if (commentedMatch) {
      keys.add(commentedMatch[1]);
    }
  }

  return keys;
}

function loadExampleRecommendedRegistryNames() {
  const src = readFileSync(resolve(ROOT, 'packages/api/src/config/env-registry.ts'), 'utf-8');
  const recommended = new Set();

  const objPattern = /\{([^}]+)\}/gs;
  for (const block of src.matchAll(objPattern)) {
    const body = block[1];
    const nameMatch = body.match(/name:\s*['"]([A-Z_][A-Z0-9_]*)['"]/);
    if (!nameMatch) continue;

    if (/exampleRecommended:\s*true/.test(body)) {
      recommended.add(nameMatch[1]);
    }
  }

  return recommended;
}

function readScriptFallback(relPath, varName) {
  const content = readFileSync(resolve(ROOT, relPath), 'utf-8');
  // Match pattern: VAR=${ENV_NAME:-DEFAULT}
  const re = new RegExp(`${varName}=\\$\\{\\w+:-([^}]+)\\}`);
  const m = content.match(re);
  return m ? m[1] : null;
}

function readTsFallback(relPath, pattern) {
  const content = readFileSync(resolve(ROOT, relPath), 'utf-8');
  const m = content.match(pattern);
  return m ? m[1] : null;
}

function readPowerShellFallback(relPath, pattern) {
  const content = readFileSync(resolve(ROOT, relPath), 'utf-8');
  const m = content.match(pattern);
  return m ? m[1] : null;
}

function normalizeYamlListItem(line) {
  return line
    .replace(/\s+#.*$/, '')
    .replaceAll('"', '')
    .trim();
}

function readYamlTopLevelKey(line) {
  return line.match(/^([A-Za-z0-9_-]+):\s*$/)?.[1] ?? null;
}

function parseYamlTopLevelList(content, sectionName) {
  const lines = content.split('\n');
  const values = [];
  let inSection = false;

  for (const line of lines) {
    const topLevelKey = readYamlTopLevelKey(line);
    if (topLevelKey === sectionName) {
      inSection = true;
      continue;
    }
    if (topLevelKey && inSection) {
      break;
    }

    if (!inSection) continue;

    const listItem = line.match(/^ {2}- (.+)$/)?.[1];
    if (listItem) {
      const normalized = normalizeYamlListItem(listItem);
      if (normalized.length > 0) {
        values.push(normalized);
      }
    }
  }

  return values;
}

function readYamlTopLevelList(relPath, sectionName) {
  return parseYamlTopLevelList(readFileSync(resolve(ROOT, relPath), 'utf-8'), sectionName);
}

function parseYamlTransformTargets(content) {
  const lines = content.split('\n');
  const targets = [];
  let inSection = false;

  for (const line of lines) {
    const topLevelKey = readYamlTopLevelKey(line);
    if (topLevelKey === 'transforms') {
      inSection = true;
      continue;
    }
    if (topLevelKey && inSection) {
      break;
    }

    if (!inSection) continue;

    const target = line.match(/^ {2}- target:\s*(.+)$/)?.[1];
    if (target) {
      targets.push(normalizeYamlListItem(target));
    }
  }

  return targets;
}

function readYamlTransformTargets(relPath) {
  return parseYamlTransformTargets(readFileSync(resolve(ROOT, relPath), 'utf-8'));
}

function sanitizeFixture(relPath, content) {
  const tempRoot = mkdtempSync(resolve(tmpdir(), 'cat-cafe-sanitize-'));
  const fixturePath = resolve(tempRoot, relPath);
  mkdirSync(dirname(fixturePath), { recursive: true });
  writeFileSync(fixturePath, content);

  try {
    execFileSync('perl', ['-pi', resolve(ROOT, 'scripts/_sanitize-rules.pl'), fixturePath], {
      cwd: ROOT,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    return readFileSync(fixturePath, 'utf-8');
  } finally {
    rmSync(tempRoot, { recursive: true, force: true });
  }
}

function assertPortableClaudeHookTemplate(template) {
  assert.doesNotMatch(template, /(?:\/(?:Users|home)\/[^"'\s]+|[A-Za-z]:(?:\/|\\\\+)Users(?:\/|\\\\+)[^"'\s]+)/);
}

function readClaudeHookTemplateCommands(template) {
  const parsed = JSON.parse(template);
  return [parsed.hooks.SessionStart[0].hooks[0].command, parsed.hooks.Stop[0].hooks[0].command];
}

function readSyncScript() {
  return readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
}

function readFunctionBody(content, functionName) {
  const start = content.indexOf(`${functionName}() {`);
  assert.notEqual(start, -1, `expected to find function ${functionName} in sync-to-opensource.sh`);

  const end = content.indexOf('\n}\n', start);
  assert.notEqual(end, -1, `expected to find the end of function ${functionName} in sync-to-opensource.sh`);

  return content.slice(start, end);
}

describe(
  '.env.example.opensource port consistency',
  { skip: !hasEnvExampleOpensource && '.env.example.opensource not present (open-source repo uses .env.example)' },
  () => {
    const env = readEnvFile('.env.example.opensource');
    const envTemplateKeys = readEnvTemplateKeys('.env.example.opensource');
    const recommendedRegistryNames = loadExampleRecommendedRegistryNames();

    it('API_SERVER_PORT matches sync convention (3004)', () => {
      assert.equal(
        env.API_SERVER_PORT,
        '3004',
        `API_SERVER_PORT should be 3004 (open-source convention), got ${env.API_SERVER_PORT}`,
      );
    });

    it('FRONTEND_PORT matches sync convention (3003)', () => {
      assert.equal(
        env.FRONTEND_PORT,
        '3003',
        `FRONTEND_PORT should be 3003 (open-source convention), got ${env.FRONTEND_PORT}`,
      );
    });

    it('NEXT_PUBLIC_API_URL uses API port (3004)', () => {
      assert.equal(
        env.NEXT_PUBLIC_API_URL,
        'http://localhost:3004',
        `NEXT_PUBLIC_API_URL should point to API port 3004, got ${env.NEXT_PUBLIC_API_URL}`,
      );
    });

    it('REDIS_PORT stays on 6399', () => {
      assert.equal(env.REDIS_PORT, '6399', `REDIS_PORT should stay 6399, got ${env.REDIS_PORT}`);
    });

    it('REDIS_URL stays on localhost:6399', () => {
      assert.equal(
        env.REDIS_URL,
        'redis://localhost:6399',
        `REDIS_URL should stay on localhost:6399, got ${env.REDIS_URL}`,
      );
    });

    it('.env.example.opensource comment header documents correct ports', () => {
      const content = readFileSync(resolve(ROOT, '.env.example.opensource'), 'utf-8');
      // The comment should say Frontend=3003, API=3004
      assert.ok(
        content.includes('3004') && content.includes('3003'),
        'Comment header should mention both 3003 and 3004',
      );
    });

    it('includes every exampleRecommended env var from env-registry', () => {
      const missing = [...recommendedRegistryNames].filter((name) => !envTemplateKeys.has(name));
      assert.deepEqual(
        missing,
        [],
        `Missing exampleRecommended env vars in .env.example.opensource: ${missing.join(', ')}`,
      );
    });

    it('documents the private-network access pair for LAN / Tailscale setups', () => {
      assert.ok(envTemplateKeys.has('API_SERVER_HOST'), 'Expected .env.example.opensource to document API_SERVER_HOST');
      assert.ok(
        envTemplateKeys.has('CORS_ALLOW_PRIVATE_NETWORK'),
        'Expected .env.example.opensource to document CORS_ALLOW_PRIVATE_NETWORK',
      );
    });
  },
);

// In the home repo (cat-cafe), code defaults are API=3002 / Frontend=3001.
// In the open-source repo (clowder-ai), sync transforms them to Frontend=3003 / API=3004.
const expectedApiPort = isHomeRepo ? '3002' : '3004';
const expectedFrontendPort = isHomeRepo ? '3001' : '3003';
const repoLabel = isHomeRepo ? 'home' : 'open-source';

describe(`Code-side port defaults are internally consistent (${repoLabel}: API=${expectedApiPort}, Frontend=${expectedFrontendPort})`, () => {
  it(`start-dev.sh API fallback is ${expectedApiPort}`, () => {
    const fallback = readScriptFallback('scripts/start-dev.sh', 'API_PORT');
    assert.equal(
      fallback,
      expectedApiPort,
      `start-dev.sh API_PORT fallback should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`start-dev.sh Frontend fallback is ${expectedFrontendPort}`, () => {
    const fallback = readScriptFallback('scripts/start-dev.sh', 'WEB_PORT');
    assert.equal(
      fallback,
      expectedFrontendPort,
      `start-dev.sh WEB_PORT fallback should be ${expectedFrontendPort}, got ${fallback}`,
    );
  });

  it(`index.ts API port fallback is ${expectedApiPort}`, () => {
    const fallback = readTsFallback('packages/api/src/index.ts', /API_SERVER_PORT\s*\?\?\s*'(\d+)'/);
    assert.equal(fallback, expectedApiPort, `index.ts API fallback should be ${expectedApiPort}, got ${fallback}`);
  });

  it(`env-registry.ts API_SERVER_PORT defaultValue is ${expectedApiPort}`, () => {
    const fallback = readTsFallback(
      'packages/api/src/config/env-registry.ts',
      /name:\s*'API_SERVER_PORT',\s*defaultValue:\s*'(\d+)'/,
    );
    assert.equal(
      fallback,
      expectedApiPort,
      `env-registry API_SERVER_PORT default should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`ConfigRegistry.ts API port fallback is ${expectedApiPort}`, () => {
    const fallback = readTsFallback('packages/api/src/config/ConfigRegistry.ts', /API_SERVER_PORT\s*\?\?\s*'(\d+)'/);
    assert.equal(
      fallback,
      expectedApiPort,
      `ConfigRegistry API fallback should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`frontend-origin.ts DEFAULT_FRONTEND_BASE_URL uses port ${expectedFrontendPort}`, () => {
    const fallback = readTsFallback(
      'packages/api/src/config/frontend-origin.ts',
      /DEFAULT_FRONTEND_BASE_URL\s*=\s*'http:\/\/localhost:(\d+)'/,
    );
    assert.equal(
      fallback,
      expectedFrontendPort,
      `frontend-origin DEFAULT_FRONTEND_BASE_URL should use ${expectedFrontendPort}, got ${fallback}`,
    );
  });

  it(`setup.sh API_SERVER_PORT is ${expectedApiPort}`, () => {
    const content = readFileSync(resolve(ROOT, 'scripts/setup.sh'), 'utf-8');
    assert.ok(
      content.includes(`API_SERVER_PORT=${expectedApiPort}`),
      `setup.sh should set API_SERVER_PORT=${expectedApiPort}`,
    );
  });

  it(`setup.sh FRONTEND_PORT is ${expectedFrontendPort}`, () => {
    const content = readFileSync(resolve(ROOT, 'scripts/setup.sh'), 'utf-8');
    assert.ok(
      content.includes(`FRONTEND_PORT=${expectedFrontendPort}`),
      `setup.sh should set FRONTEND_PORT=${expectedFrontendPort}`,
    );
  });

  it(`setup.sh NEXT_PUBLIC_API_URL uses port ${expectedApiPort}`, () => {
    const content = readFileSync(resolve(ROOT, 'scripts/setup.sh'), 'utf-8');
    assert.ok(
      content.includes(`NEXT_PUBLIC_API_URL=http://localhost:${expectedApiPort}`),
      `setup.sh should set NEXT_PUBLIC_API_URL to localhost:${expectedApiPort}`,
    );
  });

  it(`runtime-worktree.sh API port fallback is ${expectedApiPort}`, () => {
    const fallback = readTsFallback('scripts/runtime-worktree.sh', /API_SERVER_PORT:-(\d+)/);
    assert.equal(
      fallback,
      expectedApiPort,
      `runtime-worktree.sh API port fallback should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`platform-status.mjs API status fallback is ${expectedApiPort}`, () => {
    const fallback = readTsFallback('scripts/lib/platform-status.mjs', /DEFAULT_API_PORT = '(\d+)'/);
    assert.equal(
      fallback,
      expectedApiPort,
      `platform-status API status fallback should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`platform-status.mjs Frontend status fallback is ${expectedFrontendPort}`, () => {
    const fallback = readTsFallback('scripts/lib/platform-status.mjs', /DEFAULT_WEB_PORT = '(\d+)'/);
    assert.equal(
      fallback,
      expectedFrontendPort,
      `platform-status Frontend status fallback should be ${expectedFrontendPort}, got ${fallback}`,
    );
  });

  it(`AgentRouter.ts API port fallback is ${expectedApiPort}`, () => {
    const fallback = readTsFallback(
      'packages/api/src/domains/cats/services/agents/routing/AgentRouter.ts',
      /API_SERVER_PORT\s*\?\?\s*'(\d+)'/,
    );
    assert.equal(
      fallback,
      expectedApiPort,
      `AgentRouter.ts API fallback should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`start-windows.ps1 API fallback is ${expectedApiPort}`, () => {
    const fallback = readPowerShellFallback(
      'scripts/start-windows.ps1',
      /\$ApiPort = if \(\$env:API_SERVER_PORT\) \{ \$env:API_SERVER_PORT \} else \{ "(\d+)" \}/,
    );
    assert.equal(
      fallback,
      expectedApiPort,
      `start-windows.ps1 API fallback should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`start-windows.ps1 Frontend fallback is ${expectedFrontendPort}`, () => {
    const fallback = readPowerShellFallback(
      'scripts/start-windows.ps1',
      /\$WebPort = if \(\$env:FRONTEND_PORT\) \{ \$env:FRONTEND_PORT \} else \{ "(\d+)" \}/,
    );
    assert.equal(
      fallback,
      expectedFrontendPort,
      `start-windows.ps1 Frontend fallback should be ${expectedFrontendPort}, got ${fallback}`,
    );
  });

  it('start-windows.ps1 Redis fallback uses repo-local default', () => {
    const fallback = readPowerShellFallback(
      'scripts/start-windows.ps1',
      /\$RedisPort = if \(\$env:REDIS_PORT\) \{ \$env:REDIS_PORT \} else \{ "(\d+)" \}/,
    );
    assert.equal(fallback, '6399');
  });

  it(`stop-windows.ps1 API fallback is ${expectedApiPort}`, () => {
    const fallback = readPowerShellFallback('scripts/stop-windows.ps1', /\$ApiPort = (\d+)/);
    assert.equal(
      fallback,
      expectedApiPort,
      `stop-windows.ps1 API fallback should be ${expectedApiPort}, got ${fallback}`,
    );
  });

  it(`stop-windows.ps1 Frontend fallback is ${expectedFrontendPort}`, () => {
    const fallback = readPowerShellFallback('scripts/stop-windows.ps1', /\$WebPort = (\d+)/);
    assert.equal(
      fallback,
      expectedFrontendPort,
      `stop-windows.ps1 Frontend fallback should be ${expectedFrontendPort}, got ${fallback}`,
    );
  });

  it('stop-windows.ps1 Redis fallback uses repo-local default', () => {
    const fallback = readPowerShellFallback('scripts/stop-windows.ps1', /\$RedisPort = (\d+)/);
    assert.equal(fallback, '6399');
  });

  it(`install.ps1 minimal .env fallback uses API ${expectedApiPort} and Frontend ${expectedFrontendPort}`, () => {
    const content = readFileSync(resolve(ROOT, 'scripts/install.ps1'), 'utf-8');
    assert.ok(
      content.includes(`FRONTEND_PORT=${expectedFrontendPort}`),
      `install.ps1 minimal .env should set FRONTEND_PORT=${expectedFrontendPort}`,
    );
    assert.ok(
      content.includes(`API_SERVER_PORT=${expectedApiPort}`),
      `install.ps1 minimal .env should set API_SERVER_PORT=${expectedApiPort}`,
    );
    assert.ok(
      content.includes(`NEXT_PUBLIC_API_URL=http://localhost:${expectedApiPort}`),
      `install.ps1 minimal .env should set NEXT_PUBLIC_API_URL to localhost:${expectedApiPort}`,
    );
  });

  it('install.ps1 Redis fallback uses repo-local default', () => {
    const content = readFileSync(resolve(ROOT, 'scripts/install.ps1'), 'utf-8');
    assert.ok(content.includes('REDIS_PORT=6399'));
  });

  it(`install.ps1 post-install open URL fallback uses frontend port ${expectedFrontendPort}`, () => {
    const fallback = readPowerShellFallback(
      'scripts/install.ps1',
      /if \(-not \$frontendPort\) \{ \$frontendPort = "(\d+)" \}/,
    );
    assert.equal(
      fallback,
      expectedFrontendPort,
      `install.ps1 final frontend fallback should be ${expectedFrontendPort}, got ${fallback}`,
    );
  });
});

describe(
  'Sync transform rules match convention',
  { skip: !isHomeRepo && 'sync infrastructure not present (open-source repo)' },
  () => {
    it('_sanitize-rules.pl transforms 3002→3004 (API)', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/_sanitize-rules.pl'), 'utf-8');
      assert.ok(
        content.includes('s#localhost:3004#localhost:3004#g'),
        'sanitize rules should transform localhost:3004 → localhost:3004',
      );
      assert.ok(
        content.includes('s#\\[::1\\]:3002#[::1]:3004#g'),
        'sanitize rules should transform IPv6 loopback [::1]:3004 → [::1]:3004',
      );
    });

    it('_sanitize-rules.pl transforms 3001→3003 (Frontend)', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/_sanitize-rules.pl'), 'utf-8');
      assert.ok(
        content.includes('s#localhost:3003#localhost:3003#g'),
        'sanitize rules should transform localhost:3003 → localhost:3003',
      );
    });

    it('_sanitize-rules.pl keeps api-client port+1 tests internally consistent', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/_sanitize-rules.pl'), 'utf-8');
      assert.ok(
        content.includes("s#port: '3001'#port: '3003'#g"),
        'api-client-resolve test input ports should transform alongside expected port+1 assertions',
      );
    });

    it('sync-to-opensource.sh runs sanitizer over CommonJS test files', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(
        content.includes('-name "*.cjs"'),
        'sync sanitizer should include .cjs files such as packages/web/test/next-config.test.cjs',
      );
    });

    it('sync-to-opensource.sh runs sanitizer over ES module utility files', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(
        content.includes('-name "*.mjs"'),
        'sync sanitizer should include .mjs files such as scripts/lib/platform-status.mjs',
      );
    });

    it('sync-to-opensource.sh transforms start-dev.sh API fallback to 3004', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      const expected = "'s/API_PORT=$" + '{API_SERVER_PORT:-3004}/API_PORT=$' + "{API_SERVER_PORT:-3004}/g'";
      assert.ok(content.includes(expected), 'sync script should transform start-dev.sh API fallback 3002→3004');
    });

    it('sync-to-opensource.sh transforms start-dev.sh Frontend fallback to 3003', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      const expected = "'s/WEB_PORT=$" + '{FRONTEND_PORT:-3003}/WEB_PORT=$' + "{FRONTEND_PORT:-3003}/g'";
      assert.ok(content.includes(expected), 'sync script should transform start-dev.sh Frontend fallback 3001→3003');
    });

    it('sync-to-opensource.sh transforms setup.sh API port to 3004', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(
        content.includes("'s/API_SERVER_PORT=3004/API_SERVER_PORT=3004/g'"),
        'sync script should transform setup.sh API_SERVER_PORT 3002→3004',
      );
    });

    it('sync-to-opensource.sh transforms setup.sh Frontend port to 3003', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(
        content.includes("'s/FRONTEND_PORT=3003/FRONTEND_PORT=3003/g'"),
        'sync script should transform setup.sh FRONTEND_PORT 3001→3003',
      );
    });

    it('sync-to-opensource.sh transforms runtime-worktree.sh API port to 3004', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(
        content.includes("'s/API_SERVER_PORT:-3004/API_SERVER_PORT:-3004/g'"),
        'sync script should transform runtime-worktree.sh API port 3004→3004',
      );
    });

    it('sync-to-opensource.sh transforms install.ps1 to public defaults', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(content.includes("'s/FRONTEND_PORT=3003/FRONTEND_PORT=3003/g'"));
      assert.ok(content.includes("'s/API_SERVER_PORT=3004/API_SERVER_PORT=3004/g'"));
      assert.ok(content.includes('$frontendPort = "3003"'));
    });

    it('sync-to-opensource.sh transforms start-windows.ps1 API/frontend defaults', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(content.includes('s/else { "3002" }/else { "3004" }/g'));
      assert.ok(content.includes('s/else { "3001" }/else { "3003" }/g'));
    });

    it('sync-to-opensource.sh transforms stop-windows.ps1 API/frontend defaults', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(content.includes('s/\\$ApiPort = 3002/$ApiPort = 3004/g'));
      assert.ok(content.includes('s/\\$WebPort = 3001/$WebPort = 3003/g'));
    });

    it('sync-to-opensource.sh keeps Windows Redis defaults unchanged', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(!content.includes("'s/REDIS_PORT=6399/REDIS_PORT=6379/g'"));
      assert.ok(!content.includes('s/else { "6399" }/else { "6379" }/g'));
      assert.ok(!content.includes('s/\\$RedisPort = 6399/$RedisPort = 6379/g'));
      assert.ok(!content.includes('s/\\$redisPort = "6399"/$redisPort = "6379"/g'));
    });

    it('sync shell parsers preserve # inside YAML values but strip inline comments', () => {
      const outbound = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      const hotfix = readFileSync(resolve(ROOT, 'scripts/sync-hotfix.sh'), 'utf-8');

      assert.match(outbound, /sub\(\/\[\[:space:\]\]#\.\*\/,\s*"",\s*line\)/);
      assert.match(hotfix, /sub\(\/\[\[:space:\]\]#\.\*\/,\s*"",\s*l\)/);
    });

    it('YAML parser scopes list membership to managed_scripts only', () => {
      const fixture = `
managed_scripts:
  - scripts/install.ps1 # keep this in sync
  - scripts/start-windows.ps1
  - scripts/foo#1.ps1
excluded:
  - scripts/install.ps1
`;

      assert.deepEqual(parseYamlTopLevelList(fixture, 'managed_scripts'), [
        'scripts/install.ps1',
        'scripts/start-windows.ps1',
        'scripts/foo#1.ps1',
      ]);
    });

    it('sync-manifest exports the Windows deploy scripts needed by F113', () => {
      const managedScripts = readYamlTopLevelList('sync-manifest.yaml', 'managed_scripts');
      const requiredScripts = [
        'scripts/install-auth-config.mjs',
        'scripts/install-windows-helpers.ps1',
        'scripts/install.ps1',
        'scripts/start-windows.ps1',
        'scripts/start.bat',
        'scripts/stop-windows.ps1',
        'scripts/windows-command-helpers.ps1',
        'scripts/windows-installer-ui.ps1',
      ];

      for (const scriptPath of requiredScripts) {
        assert.ok(
          managedScripts.includes(scriptPath),
          `sync-manifest should export ${scriptPath} instead of deleting it from clowder-ai`,
        );
      }
    });

    it('sync-manifest exports root package check script targets', () => {
      const managedRoots = readYamlTopLevelList('sync-manifest.yaml', 'managed_roots');
      const managedScripts = readYamlTopLevelList('sync-manifest.yaml', 'managed_scripts');
      const requiredScripts = [
        'scripts/check-followup-tails.mjs',
        'scripts/derive-worktree-ports.mjs',
        'scripts/derive-worktree-ports.test.mjs',
        'scripts/check-worktree-port-offset.mjs',
        'scripts/sop-definitions.mjs',
        'scripts/sop-definitions.test.mjs',
        'scripts/lib/sop-definition-codegen.mjs',
      ];

      assert.ok(
        managedRoots.includes('sop-definitions'),
        'sync-manifest should export sop-definitions because root package.json check:sop-definitions reads it',
      );

      for (const scriptPath of requiredScripts) {
        assert.ok(
          managedScripts.includes(scriptPath),
          `sync-manifest should export ${scriptPath} because root package.json references it`,
        );
      }
    });

    it('sync-manifest exports root package operational helper scripts', () => {
      const managedScripts = readYamlTopLevelList('sync-manifest.yaml', 'managed_scripts');
      const requiredScripts = ['scripts/cleanup-stale-dev-processes.mjs'];

      for (const scriptPath of requiredScripts) {
        assert.ok(
          managedScripts.includes(scriptPath),
          `sync-manifest should export ${scriptPath} because public package.json exposes it`,
        );
      }
    });

    it('sync-manifest exports start-dev sourced shell closure', () => {
      const managedScripts = readYamlTopLevelList('sync-manifest.yaml', 'managed_scripts');
      const requiredScripts = [
        'scripts/start-dev.sh',
        'scripts/download-source-overrides.sh',
        'scripts/lib/node-runtime-guard.sh',
        'scripts/lib/redis-rdb-first.sh',
      ];

      for (const scriptPath of requiredScripts) {
        assert.ok(
          managedScripts.includes(scriptPath),
          `sync-manifest should export ${scriptPath} because public start-dev.sh sources it at runtime`,
        );
      }
    });

    it('sync-manifest does not protect managed service wrappers as target-owned', () => {
      const managedScripts = readYamlTopLevelList('sync-manifest.yaml', 'managed_scripts');
      const targetOwnedFiles = readYamlTopLevelList('sync-manifest.yaml', 'target_owned_files');
      const managedServiceWrappers = managedScripts.filter((entry) => entry.startsWith('scripts/services/'));

      for (const entry of targetOwnedFiles) {
        const overlappingManaged = managedServiceWrappers.find(
          (managed) => managed === entry || managed.startsWith(entry),
        );
        assert.equal(
          overlappingManaged,
          undefined,
          `target_owned_files should not restore over managed public service wrapper ${overlappingManaged ?? entry}`,
        );
      }
    });

    it('sync-to-opensource.sh drops home-only root package scripts whose targets are not exported', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');

      assert.ok(
        content.includes('pkg.scripts.check === "node scripts/run-checks.mjs"'),
        'public package.json should rewrite source-only run-checks wrapper into an explicit public check chain',
      );
      assert.ok(
        content.includes('pkg.scripts["check:pre-merge-gate"] ='),
        'public package.json should strip source-only run-checks.test.mjs from check:pre-merge-gate',
      );
      assert.ok(
        content.includes('delete pkg.scripts["check:architecture-ownership"]'),
        'public package.json should not expose check:architecture-ownership without exporting its script target',
      );
      assert.ok(
        content.includes('delete pkg.scripts["test:architecture-ownership"]'),
        'public package.json should not expose test:architecture-ownership without exporting its script target',
      );
    });

    it('sync-manifest exports F180 user-level hook truth source', () => {
      const managedScripts = readYamlTopLevelList('sync-manifest.yaml', 'managed_scripts');
      const requiredHookScripts = [
        '.claude/hooks/user-level/session-start-recall.sh',
        '.claude/hooks/user-level/session-stop-check.sh',
      ];

      for (const scriptPath of requiredHookScripts) {
        assert.ok(
          managedScripts.includes(scriptPath),
          `sync-manifest should export ${scriptPath} so open-source users have hook templates`,
        );
      }
    });

    it('sync-manifest exports F203 native L0 runtime closure', () => {
      const managedFiles = readYamlTopLevelList('sync-manifest.yaml', 'managed_files');
      const managedScripts = readYamlTopLevelList('sync-manifest.yaml', 'managed_scripts');

      assert.ok(
        managedFiles.includes('assets/system-prompts/system-prompt-l0.md'),
        'sync-manifest should export the F203 L0 template required by native prompt compilation',
      );
      assert.ok(
        managedScripts.includes('scripts/compile-system-prompt-l0.mjs'),
        'sync-manifest should export the F203 L0 compiler required by carrier invocation',
      );
    });

    it('sync-manifest exports public harness eval fixtures used by test:public', () => {
      const managedFiles = readYamlTopLevelList('sync-manifest.yaml', 'managed_files');
      const managedRoots = readYamlTopLevelList('sync-manifest.yaml', 'managed_roots');

      // eval-domains/ is exported as a managed_root (whole directory), so the individual
      // domain registries (eval-a2a/eval-memory/eval-sop) + community-fixtures sync
      // automatically — they need NOT be listed in managed_files. This guard moved from
      // per-file enumeration to root-coverage after the recurring "F192 added a new domain
      // but forgot to update managed_files" sync-config debt (PR #1929).
      assert.ok(
        managedRoots.includes('docs/harness-feedback/eval-domains'),
        'eval-domains/ must be a managed_root so all domain registries + community-fixtures sync to the public repo',
      );

      // verdicts/ + bundles/ + F210 assets stay curated in managed_files (verdicts/bundles
      // may contain internal-only entries, so they are NOT blanket-exported as a root).
      const fixturePaths = [
        'docs/harness-feedback/verdicts/fixtures/2026-05-21-eval-a2a-contract-demo.md',
        'docs/harness-feedback/verdicts/2026-05-23-eval-a2a-live-verdict.md',
        'docs/harness-feedback/bundles/2026-05-23-eval-a2a-live-verdict/attribution.json',
        'docs/harness-feedback/bundles/2026-05-23-eval-a2a-live-verdict/provenance.json',
        'docs/harness-feedback/bundles/2026-05-23-eval-a2a-live-verdict/snapshot.json',
        'docs/features/assets/F210/agy-conversation-resume.txt',
        'docs/features/assets/F210/agy-print-timeout.txt',
        'docs/features/assets/F210/agy-real-home-no-default-model.txt',
        'docs/features/assets/F210/agy-real-home-print-success.txt',
      ];

      for (const fixturePath of fixturePaths) {
        assert.ok(
          managedFiles.includes(fixturePath),
          `sync-manifest should export ${fixturePath} because packages/api test:public loads it`,
        );
      }
    });

    it('sync-manifest marks the F203 native L0 template as a sanitized transform', () => {
      const transformTargets = readYamlTransformTargets('sync-manifest.yaml');

      assert.ok(
        transformTargets.includes('assets/system-prompts/system-prompt-l0.md'),
        'system-prompt-l0.md carries governance rules and must be explicitly tracked as a sanitize transform',
      );
    });

    it('public F203 native L0 template sanitization removes home-only runtime rules', () => {
      const sourceL0 = readFileSync(resolve(ROOT, 'assets/system-prompts/system-prompt-l0.md'), 'utf-8');
      const sanitized = sanitizeFixture('assets/system-prompts/system-prompt-l0.md', sourceL0);

      assert.doesNotMatch(sanitized, /Redis production Redis \(sacred\)/);
      assert.doesNotMatch(sanitized, /Redis production Redis (sacred)/);
      assert.doesNotMatch(sanitized, /\b6398\b|\b6399\b/);
      assert.match(sanitized, /\*\*Runtime data safety\*\*/);
      assert.doesNotMatch(sanitized, /Cat Café 的护城河是情感壁垒不是技术壁垒/);
    });

    it('sync-to-opensource.sh hard-fails if public L0 still contains internal patterns', () => {
      const content = readSyncScript();

      assert.match(
        content,
        /l0_internal_found=\$\(printf '%s\\n' "\$found" \| grep -F 'assets\/system-prompts\/system-prompt-l0\.md'/,
        'internal-pattern scan should isolate system-prompt-l0.md findings',
      );
      assert.match(
        content,
        /l0_internal_found[\s\S]*?SCAN_FAILED=true/,
        'system-prompt-l0.md internal-pattern findings should fail the sync gate, not warn only',
      );
    });

    it('sync-manifest exports a portable F180 Claude settings hook template', () => {
      const managedFiles = readYamlTopLevelList('sync-manifest.yaml', 'managed_files');
      const templatePath = '.claude/hooks/user-level/claude-settings.template.json';

      assert.ok(
        managedFiles.includes(templatePath),
        `sync-manifest should export ${templatePath} as the Claude settings hook template`,
      );

      const template = readFileSync(resolve(ROOT, templatePath), 'utf-8');
      assertPortableClaudeHookTemplate(template);
      assert.doesNotThrow(() => JSON.parse(template));

      const commands = readClaudeHookTemplateCommands(template);
      assert.deepEqual(commands, [
        'bash "$HOME/.claude/hooks/session-start-recall.sh"',
        'bash "$HOME/.claude/hooks/session-stop-check.sh"',
      ]);
    });

    it('F180 Claude settings hook template guard rejects maintainer absolute-path variants', () => {
      const absolutePathTemplates = [
        '{"hooks":{"SessionStart":[{"hooks":[{"command":"/home/alice/.claude/hooks/session-start-recall.sh"}]}]}}',
        '{"hooks":{"SessionStart":[{"hooks":[{"command":"C:/home/user/session-start-recall.sh"}]}]}}',
        '{"hooks":{"SessionStart":[{"hooks":[{"command":"C:\\\\Users\\\\Alice\\\\.claude\\\\hooks\\\\session-start-recall.sh"}]}]}}',
        '{"hooks":{"SessionStart":[{"hooks":[{"command":"bash \\"/home/alice/.claude/hooks/session-start-recall.sh\\""}]}]}}',
      ];

      for (const template of absolutePathTemplates) {
        assert.throws(() => assertPortableClaudeHookTemplate(template));
      }
    });

    it('source install/setup attempts F180 agent hook sync as a nonfatal step', () => {
      const install = readFileSync(resolve(ROOT, 'scripts/install.sh'), 'utf-8');
      const setup = readFileSync(resolve(ROOT, 'scripts/setup.sh'), 'utf-8');

      for (const [name, content] of [
        ['install.sh', install],
        ['setup.sh', setup],
      ]) {
        assert.match(content, /sync_agent_hooks_best_effort\(\)/, `${name} should call the shared hook sync step`);
        assert.match(
          content,
          /pnpm exec tsx scripts\/sync-system-prompts\.ts --apply --agent-hooks-only/,
          `${name} should reuse sync-system-prompts hook targets without syncing non-hook prompts`,
        );
        assert.match(
          content,
          /Agent CLI hook sync failed[\s\S]*continuing/,
          `${name} should warn and continue when hook sync fails`,
        );
      }
    });

    it('F210 source installer provisions Antigravity CLI through native bootstrapper', () => {
      const install = readFileSync(resolve(ROOT, 'scripts/install.sh'), 'utf-8');
      const installPs = readFileSync(resolve(ROOT, 'scripts/install.ps1'), 'utf-8');

      assert.match(install, /install_antigravity_cli\(\)/);
      assert.match(install, /https:\/\/antigravity\.google\/cli\/install\.sh/);
      assert.match(install, /MISSING_AGENTS\+=\("agy"\)/);
      assert.match(install, /agy\)\s+install_antigravity_cli/s);
      assert.doesNotMatch(install, /install_npm_cli "Gemini CLI" "gemini" "@google\/gemini-cli"/);

      assert.match(installPs, /Name = "Antigravity"; Label = "Antigravity CLI"; Cmd = "agy"/);
      assert.match(installPs, /InstallKind = "antigravity-native"/);
      assert.match(installPs, /https:\/\/antigravity\.google\/cli\/install\.cmd/);
      assert.match(installPs, /Resolve-ToolCommandWithRetry -Name "agy" -Attempts 6/);
      assert.doesNotMatch(installPs, /Name = "Gemini"; Label = "Gemini"; Cmd = "gemini"; Pkg = "@google\/gemini-cli"/);
    });

    it('sync-system-prompts agent hook mode also configures Claude settings', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-system-prompts.ts'), 'utf-8');

      assert.match(
        content,
        /syncClaudeSettings/,
        'agent hook sync CLI should reuse the same Claude settings merge helper as the Hub sync API',
      );
      assert.match(
        content,
        /if\s*\(\s*isAgentHooksOnly\s*&&\s*!isDryRun\s*\)[\s\S]*await syncClaudeSettings\(syncTargetRoot\)/,
        '--agent-hooks-only --apply must configure Claude settings, not only copy hook scripts and Codex hooks',
      );
    });

    it('desktop installer bundles F180 hook truth source and offline sync helper', () => {
      const inno = readFileSync(resolve(ROOT, 'desktop/installer/cat-cafe.iss'), 'utf-8');
      const desktopPackage = readFileSync(resolve(ROOT, 'desktop/package.json'), 'utf-8');

      assert.match(
        inno,
        /Source: "\.\.\\\.\.\\\.claude\\hooks\\user-level\\\*";\s+DestDir: "\{app\}\\\.claude\\hooks\\user-level"/,
        'Windows installer should ship the user-level hook truth source',
      );
      assert.match(
        inno,
        /Source: "\.\.\\scripts\\sync-agent-hooks-offline\.mjs";\s+DestDir: "\{app\}\\scripts"/,
        'Windows installer should ship the offline hook sync helper',
      );
      assert.match(
        desktopPackage,
        /"from": "\.\.\/\.claude\/hooks\/user-level"[\s\S]*"to": "\.claude\/hooks\/user-level"/,
        'macOS DMG resources should ship the user-level hook truth source',
      );
      assert.match(
        desktopPackage,
        /"from": "\.\/scripts\/sync-agent-hooks-offline\.mjs"[\s\S]*"to": "scripts\/sync-agent-hooks-offline\.mjs"/,
        'macOS DMG resources should ship the offline hook sync helper',
      );
    });

    it('desktop installer runs F180 agent hook sync under the invoking user profile', () => {
      const inno = readFileSync(resolve(ROOT, 'desktop/installer/cat-cafe.iss'), 'utf-8');
      const postInstall = readFileSync(resolve(ROOT, 'desktop/scripts/post-install-offline.ps1'), 'utf-8');
      const adminPostInstallEntry = inno.match(
        /; Post-install:[\s\S]*?Filename: "powershell\.exe";[\s\S]*?Components: core/,
      )?.[0];

      assert.match(
        inno,
        /-AgentHooksOnly[\s\S]*Flags: runhidden waituntilterminated runasoriginaluser/,
        'Windows installer should run user-level hook sync with original user credentials, not the elevated admin profile',
      );
      assert.ok(adminPostInstallEntry, 'Windows installer should keep the elevated post-install entry');
      assert.doesNotMatch(
        adminPostInstallEntry,
        /-AgentHooksOnly/,
        'admin post-install step should not also run AgentHooksOnly in the elevated profile',
      );
      assert.match(
        postInstall,
        /sync-agent-hooks-offline\.mjs/,
        'post-install should invoke the offline hook sync helper',
      );
      assert.match(
        postInstall,
        /\$targetRoot = Resolve-AgentHookTargetRoot[\s\S]*--target-root[\s\S]*\$targetRoot/,
        'offline helper should receive the resolved user profile explicitly as --target-root',
      );
      assert.match(
        postInstall,
        /Agent CLI hook sync failed[\s\S]*Hub health check can repair it later/,
        'post-install hook sync failure should be a nonfatal warning',
      );
    });

    it('desktop first-run mirrors F180 hook truth source into the writable API project', () => {
      const serviceManager = readFileSync(resolve(ROOT, 'desktop/service-manager.js'), 'utf-8');

      assert.match(
        serviceManager,
        /const mirrors = \['\.claude', 'cat-cafe-skills', 'docs', 'packages'\]/,
        'desktop service manager should mirror .claude into the writable project root for API hook health',
      );
    });

    it('sync-to-opensource.sh transforms AgentRouter.ts API port to 3004', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.ok(
        content.includes("process.env.API_SERVER_PORT ?? '3004'"),
        'sync script should transform AgentRouter.ts API port 3004→3004',
      );
    });

    it('sync-to-opensource.sh leaves sync tag publication to scripts/publish-sync-tag.sh', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      const publishScript = readFileSync(resolve(ROOT, 'scripts/publish-sync-tag.sh'), 'utf-8');
      assert.doesNotMatch(
        content,
        /git -C "\$SOURCE_DIR" tag "\$SYNC_TAG"/,
        'sync-to-opensource should not create a sync tag before the target sync lands',
      );
      assert.doesNotMatch(
        content,
        /git -C "\$SOURCE_DIR" push origin "refs\/tags\/\$SYNC_TAG"/,
        'sync-to-opensource should not publish a sync tag before the target sync is visible upstream',
      );
      assert.match(
        content,
        /if \[ "\$DRY_RUN" = false \] && \[ "\$VALIDATE" = false \]; then[\s\S]*After merge: \$PUBLISH_HANDOFF_CMD/,
        'sync-to-opensource should only print the post-merge publish handoff for real sync runs',
      );
      assert.match(
        content,
        /PUBLISH_HANDOFF_CMD="bash scripts\/publish-sync-tag\.sh --source-sha=\$\(git -C "\$SOURCE_DIR" rev-parse HEAD\) --push"/,
        'sync-to-opensource should print the post-merge publish-sync-tag.sh handoff command',
      );
      assert.match(
        content,
        /PUBLISH_HANDOFF_CMD="CLOWDER_AI_DIR=\$\(printf '%q' "\$TARGET_DIR"\) \$PUBLISH_HANDOFF_CMD"/,
        'sync-to-opensource should preserve a custom CLOWDER_AI_DIR in the publish handoff',
      );
      assert.match(
        publishScript,
        /git -C "\$repo" tag "\$SYNC_TAG" "\$sha"/,
        'post-merge lane should contain a real tag creation command',
      );
      assert.match(
        publishScript,
        /TARGET_SHA=\$\(resolve_latest_landed_sync_commit "\$TARGET_MAIN_REF"\)/,
        'post-merge lane should auto-detect the latest landed target sync commit when --target-sha is omitted',
      );
      assert.match(
        publishScript,
        /ensure_tag_points_to "\$SOURCE_DIR" "cat-cafe" "\$SOURCE_SHA"/,
        'post-merge lane should have a real source-tag publication command',
      );
      assert.match(
        publishScript,
        /ensure_tag_points_to "\$TARGET_DIR" "clowder-ai" "\$TARGET_SHA"/,
        'post-merge lane should advance the matching clowder-ai tag too',
      );
    });

    it('sync-to-opensource.sh supports release-intended source snapshot tags and provenance mapping', () => {
      const content = readFileSync(resolve(ROOT, 'scripts/sync-to-opensource.sh'), 'utf-8');
      assert.match(
        content,
        /--release-tag=\*\) RELEASE_TAG="\$\{arg#--release-tag=\}" ;;/,
        'sync-to-opensource should parse --release-tag',
      );
      assert.match(
        content,
        /SOURCE_SNAPSHOT_TAG="\$\(derive_source_snapshot_tag "\$RELEASE_TAG"\)"/,
        'sync-to-opensource should derive a source snapshot tag from the release tag',
      );
      assert.match(
        content,
        /"release_tag": \$RELEASE_TAG_JSON,/,
        'sync-to-opensource should persist release_tag in .sync-provenance.json',
      );
      assert.match(
        content,
        /"source_snapshot_tag": \$SOURCE_SNAPSHOT_TAG_JSON,/,
        'sync-to-opensource should persist source_snapshot_tag in .sync-provenance.json',
      );
      assert.match(
        content,
        /ensure_source_snapshot_tag "\$SOURCE_SNAPSHOT_TAG" "\$SOURCE_SHA" "\$RELEASE_TAG"/,
        'sync-to-opensource should auto-create the source snapshot tag before touching the real target',
      );
      assert.match(
        content,
        /git -C "\$SOURCE_DIR" tag -a "\$tag" "\$sha" -m "source snapshot for clowder-ai \$release_tag"/,
        'release-intended sync should create an annotated source snapshot tag',
      );
      assert.match(
        content,
        /git -C "\$SOURCE_DIR" push origin "refs\/tags\/\$tag"/,
        'release-intended sync should publish the source snapshot tag to origin',
      );
      assert.match(
        content,
        /require_release_source_commit_on_main\(\) \{/,
        'release-intended sync should define a guard ensuring the source snapshot commit is on origin\\/main',
      );
      assert.match(
        content,
        /require_release_source_commit_on_main "\$SOURCE_SHA"/,
        'release-intended sync should verify the source commit is reachable from origin\\/main before syncing',
      );
    });

    it('sync-hotfix.sh selects the latest sync baseline by mirrored target tag commit time', () => {
      const hotfix = readFileSync(resolve(ROOT, 'scripts/sync-hotfix.sh'), 'utf-8');
      assert.match(
        hotfix,
        /git -C "\$SOURCE_DIR" fetch --quiet --force --prune --prune-tags origin[\s\\]+"\+refs\/tags\/sync\/\*:refs\/tags\/sync\/\*"/,
        'hotfix lane should refresh cat-cafe sync tags from origin before auto-selecting the baseline',
      );
      assert.match(
        hotfix,
        /git -C "\$TARGET_DIR" fetch --quiet origin main/,
        'hotfix lane should refresh clowder-ai origin\\/main before auto-selecting the baseline',
      );
      assert.match(
        hotfix,
        /TARGET_SYNC_TAG_REFS="refs\/cat-cafe-hotfix-sync-tags"/,
        'hotfix lane should mirror clowder-ai sync tags into a dedicated local ref namespace',
      );
      assert.doesNotMatch(
        hotfix,
        /git -C "\$TARGET_DIR" fetch --quiet --force origin[\s\\]+"\+refs\/tags\/sync\/\*:refs\/tags\/sync\/\*"/,
        'hotfix lane should not mirror sync tags into clowder-ai local tag refs during baseline selection',
      );
      assert.match(
        hotfix,
        /git -C "\$TARGET_DIR" fetch --quiet --force --prune origin[\s\\]+"\+refs\/tags\/sync\/\*:\$TARGET_SYNC_TAG_REFS\/sync\/\*"/,
        'hotfix lane should force-refresh the mirrored clowder-ai sync tag namespace',
      );
      assert.match(
        hotfix,
        /merge-base --is-ancestor[\s\\]+"\$TARGET_SYNC_TAG_REFS\/\$tag\^\{commit\}" refs\/remotes\/origin\/main/,
        'hotfix lane should ignore mirrored target sync tags that are no longer reachable from clowder-ai origin/main',
      );
      assert.match(
        hotfix,
        /show -s --format=%ct "\$TARGET_SYNC_TAG_REFS\/\$tag\^\{commit\}"/,
        'hotfix lane should compare mirrored clowder-ai tag commit times when choosing the latest sync baseline',
      );
      assert.match(
        hotfix,
        /rev-parse --verify "\$TARGET_SYNC_TAG_REFS\/\$SYNC_TAG\^\{commit\}"/,
        'hotfix lane should require explicit --tag baselines to exist in the mirrored clowder-ai origin tag namespace',
      );
      assert.match(
        hotfix,
        /merge-base --is-ancestor[\s\\]+"\$TARGET_SYNC_TAG_REFS\/\$SYNC_TAG\^\{commit\}" refs\/remotes\/origin\/main/,
        'hotfix lane should reject explicit --tag baselines that are no longer on clowder-ai origin/main',
      );
      assert.doesNotMatch(
        hotfix,
        /tag -l 'sync\/\*' --sort=-version:refname \| head -1/,
        'hotfix lane should not rely on tag-name sort alone for latest-sync selection',
      );
    });

    it('sync-to-opensource.sh blocks incomplete absorbed records whose files still differ from source', () => {
      const content = readSyncScript();
      const guardStart = content.indexOf('validate_incomplete_absorbed_overlaps() {');
      const guardEnd = content.indexOf('\nfind_available_port() {', guardStart);
      assert.notEqual(guardStart, -1, 'expected to find validate_incomplete_absorbed_overlaps');
      assert.notEqual(guardEnd, -1, 'expected to find the end of validate_incomplete_absorbed_overlaps');
      const guard = content.slice(guardStart, guardEnd);
      const ledgerGateIndex = content.indexOf('validate_incomplete_absorbed_overlaps "$INTAKE_LEDGER"');
      const headMatchedIndex = content.indexOf('Intake ledger up to date (target HEAD = ledger HEAD)');

      assert.notEqual(ledgerGateIndex, -1, 'pre-sync gate should call the incomplete absorbed overlap guard');
      assert.notEqual(headMatchedIndex, -1, 'expected to find the existing target HEAD ledger fast path');
      assert.ok(
        ledgerGateIndex < headMatchedIndex,
        'incomplete absorbed overlaps must be checked even when the ledger watermark equals target HEAD',
      );
      assert.match(
        guard,
        /decision === 'absorbed'[\s\S]*!entry\.intake_intent_issue[\s\S]*!entry\.review_proof/,
        'guard should only scrutinize absorbed entries missing complete intake proof',
      );
      assert.match(
        guard,
        /lastOutboundSyncIndex/,
        'guard should scope incomplete absorbed checks to commits after the latest outbound sync',
      );
      assert.match(
        guard,
        /index <= lastOutboundSyncIndex/,
        'guard should not rescan pre-sync historical ledger entries on every full sync',
      );
      assert.match(
        guard,
        /historical backfill|outbound-filed hotfix|skip-absorbed-guard/,
        'guard should skip controlled ledger exceptions that intentionally omit absorb PR proof',
      );
      assert.match(
        guard,
        /git -C "\$target_dir" show --name-only --format= "\$commit"/,
        'guard should inspect the files touched by each incomplete absorbed target commit',
      );
      assert.match(
        guard,
        /cmp -s "\$source_file" "\$target_file"/,
        'guard should block only when the current source payload would overwrite a different target file',
      );
      assert.match(
        guard,
        /recorded != absorbed-complete/,
        'guard output should explain the recorded vs complete distinction',
      );
    });
  },
);

describe(
  'Sync validation enforces static quality gates',
  { skip: !isHomeRepo && 'sync infrastructure not present (open-source repo)' },
  () => {
    it('validate mode runs the source-owned public gate on a temp target', () => {
      const content = readSyncScript();
      const staticGateFn = readFunctionBody(content, 'run_static_quality_gates');
      const validateBlock = content.match(
        /Validate temp target \(source-owned public gate\)[\s\S]*?\[VALIDATE\] Export at:/,
      )?.[0];

      assert.match(
        staticGateFn,
        /pnpm check:fix[\s\S]*pnpm check 2>&1[\s\S]*pnpm lint 2>&1/,
        'run_static_quality_gates should run pnpm check:fix → pnpm check → pnpm lint in order',
      );
      assert.ok(validateBlock, 'expected to find the validate block in sync-to-opensource.sh');
      assert.ok(
        validateBlock.includes('prepare_validation_target'),
        'validate mode should materialize a temp target before running the public gate',
      );
      assert.ok(
        validateBlock.includes('sync_filtered_into_target "$VALIDATION_TARGET_DIR"'),
        'validate mode should apply the exact exported payload to the temp target',
      );
      assert.ok(
        validateBlock.includes('run_target_public_gate "$VALIDATION_TARGET_DIR"'),
        'validate mode should reuse the same target/public gate as a real full sync',
      );
    });

    it('full sync runs the temp target public gate before touching the real target', () => {
      const content = readSyncScript();
      const tempGateIndex = content.indexOf('Source-owned public gate (temp target)...');
      const realSyncIndex = content.indexOf('sync_filtered_into_target "$TARGET_DIR"');
      const step6SummaryIndex = content.indexOf('[Step 6/6] Sync committed after source-owned public gate passed');

      assert.notEqual(tempGateIndex, -1, 'expected to find the temp target public gate block');
      assert.notEqual(realSyncIndex, -1, 'expected to find the real target sync call');
      assert.ok(
        tempGateIndex < realSyncIndex,
        'the real target sync must happen only after the temp target public gate block',
      );
      assert.ok(step6SummaryIndex > realSyncIndex, 'the final summary should only run after the real target sync');
      assert.match(
        content,
        /run_target_public_gate "\$VALIDATION_TARGET_DIR"/,
        'full sync should reuse run_target_public_gate for the temp target check',
      );
    });

    it('temp target public gate appends the validation checkout to PROJECT_ALLOWED_ROOTS', () => {
      const gate = readFunctionBody(readSyncScript(), 'run_target_public_gate');
      assert.match(
        gate,
        /gate_target_real="\$\(resolve_physical_path "\$gate_target"\)"/,
        'run_target_public_gate should canonicalize the temp target root before exporting PROJECT_ALLOWED_ROOTS',
      );
      assert.match(
        gate,
        /PROJECT_ALLOWED_ROOTS_APPEND=true[\s\\]+PROJECT_ALLOWED_ROOTS="\$gate_target_real"[\s\\]+pnpm --filter @cat-cafe\/api run test:public/,
        'test:public in the temp target should treat the validation checkout as an allowed project root',
      );
      assert.match(
        gate,
        /PROJECT_ALLOWED_ROOTS_APPEND=true[\s\\]+PROJECT_ALLOWED_ROOTS="\$gate_target_real"[\s\\]+API_SERVER_PORT=\$accept_api_port MEMORY_STORE=1 NODE_ENV=test/,
        'API startup acceptance should reuse the same temp-target allow-root so projectPath-based dispatch stays representative',
      );
    });

    it('target public gate smokes F203 native L0 compiler closure', () => {
      const gate = readFunctionBody(readSyncScript(), 'run_target_public_gate');

      assert.match(
        gate,
        /node scripts\/compile-system-prompt-l0\.mjs --cat codex >\/dev\/null/,
        'target public gate should compile codex L0 so missing F203 script/template fails before real sync',
      );
    });

    it('temp target public gate installs dependencies without inherited production env', () => {
      const content = readSyncScript();
      const envHelper = readFunctionBody(content, 'run_public_acceptance_env');
      const gate = readFunctionBody(content, 'run_target_public_gate');

      assert.match(
        envHelper,
        /-u NODE_ENV/,
        'run_public_acceptance_env should clear inherited NODE_ENV so temp target installs do not skip devDependencies',
      );
      assert.match(
        envHelper,
        /-u npm_config_production/,
        'run_public_acceptance_env should clear npm_config_production for temp target public gate',
      );
      assert.match(
        envHelper,
        /-u NPM_CONFIG_PRODUCTION/,
        'run_public_acceptance_env should clear uppercase production npm config as well',
      );
      assert.match(
        gate,
        /run_public_acceptance_env pnpm install --frozen-lockfile/,
        'temp target install must use the sanitized env helper so public gate sees devDependencies',
      );
    });

    it('temp target public gate preserves full test:public output before tailing', () => {
      const gate = readFunctionBody(readSyncScript(), 'run_target_public_gate');
      assert.match(
        gate,
        /test_public_log=\$\(mktemp "\$\{TMPDIR:-\/tmp\}\/cat-cafe-testpublic\.XXXXXX"\)/,
        'run_target_public_gate should capture test:public output in a dedicated temp log',
      );
      assert.match(
        gate,
        /pnpm --filter @cat-cafe\/api run test:public >"\$test_public_log" 2>&1/,
        'test:public should write its full output to a log file before summary tailing',
      );
      assert.match(
        gate,
        /tail -20 "\$test_public_log"/,
        'failure path should print a larger tail from the captured test:public log',
      );
      assert.doesNotMatch(
        gate,
        /pnpm --filter @cat-cafe\/api run test:public 2>&1 \| tail -5/,
        'test:public should not pipe directly into tail, or failures become opaque',
      );
    });

    it('real full sync exports from a detached origin/main source checkout', () => {
      const content = readSyncScript();
      assert.match(
        content,
        /prepare_source_sync_tree\(\) \{[\s\S]*git -C "\$SOURCE_DIR" fetch --no-tags origin main[\s\S]*git -C "\$SOURCE_DIR" worktree add --detach "\$SOURCE_SYNC_DIR" refs\/remotes\/origin\/main/m,
        'real full sync should materialize a detached source worktree from origin/main',
      );
      assert.match(
        content,
        /if \[ "\$DRY_RUN" = false \] && \[ "\$VALIDATE" = false \]; then[\s\S]*if \[ "\$SYNC_MODULE" = "all" \]; then[\s\S]*prepare_source_sync_tree/m,
        'only real full sync should switch the source baseline to origin/main',
      );
      assert.match(
        content,
        /MANIFEST="\$SOURCE_SYNC_DIR\/sync-manifest.yaml"/,
        'manifest parsing should follow the detached source checkout, not the caller worktree',
      );
      assert.match(
        content,
        /git -C "\$SOURCE_SYNC_DIR" archive HEAD \| tar -x -C "\$STAGING_DIR"/,
        'step 1 export should archive the detached origin/main checkout for real full sync',
      );
      assert.match(
        content,
        /SOURCE_DISPLAY_SHA="\$\{SOURCE_SHA_SHORT\} \(origin\/main\)"/,
        'operator-facing provenance should make it explicit that full sync used origin/main',
      );
      assert.match(
        content,
        /prepare_source_sync_tree[\s\S]*?trap 'cleanup_source_sync_tree' EXIT/,
        'source sync worktree cleanup must be registered immediately after creation (P2: no leaked worktrees on early exit)',
      );
      assert.match(
        content,
        /node "\$SOURCE_SYNC_DIR\/scripts\/export-public-feature-docs\.mjs"/,
        'feature-doc exporter must run from SOURCE_SYNC_DIR, not SOURCE_DIR (P1: no mixed provenance)',
      );
      assert.match(
        content,
        /SANITIZER="\$SOURCE_SYNC_DIR\/scripts\/_sanitize-rules\.pl"/,
        'sanitizer rules must load from SOURCE_SYNC_DIR, not SOURCE_DIR (P1: no mixed provenance)',
      );
    });
  },
);

describe(
  'Sync runtime-safety guards stay source-side and shell-safe',
  { skip: !isHomeRepo && 'sync infrastructure not present (open-source repo)' },
  () => {
    it('resolves TARGET_DIR through a physical-path helper before safety checks', () => {
      const content = readSyncScript();
      assert.match(
        content,
        /resolve_physical_path\(\) \{[\s\S]*os\.path\.realpath\(sys\.argv\[1\]\)/,
        'sync script should resolve TARGET_DIR through a realpath helper so symlink aliases cannot bypass the guard',
      );
      assert.match(
        content,
        /RESOLVED_TARGET="\$\(resolve_physical_path "\$TARGET_DIR"\)"/,
        'sync script should guard on the resolved physical TARGET_DIR path',
      );
      assert.match(
        content,
        /list_source_worktree_realpaths \| grep -qFx "\$RESOLVED_TARGET"/,
        'sync script should compare TARGET_DIR against source worktrees using resolved realpaths',
      );
    });

    it('recognizes git worktrees as valid target repos', () => {
      const content = readSyncScript();
      assert.match(
        content,
        /target_git_repo_exists\(\) \{\s+local repo_dir="\$1"\s+git -C "\$repo_dir" rev-parse --git-dir >/m,
        'sync script should detect target repos via git rev-parse so linked worktrees are accepted',
      );
      assert.match(
        content,
        /if ! target_git_repo_exists "\$TARGET_DIR"; then[\s\S]*Target git repo not found/m,
        'prepare_validation_target should use the git repo helper before rejecting the target',
      );
      assert.match(
        content,
        /if \[ "\$DRY_RUN" = false \] && \[ "\$VALIDATE" = false \] && target_git_repo_exists "\$TARGET_DIR"; then/m,
        'real sync target gates should treat linked worktrees as valid repos',
      );
      assert.match(
        content,
        /if target_git_repo_exists "\$TARGET_DIR"; then\s+cd "\$TARGET_DIR"\s+git add -A/m,
        'auto-commit finalization should also run for linked worktree targets',
      );
    });

    it('startup acceptance ports do not inherit runtime shell env', () => {
      const content = readSyncScript();
      assert.doesNotMatch(
        content,
        /ACCEPT_API_PORT=\$\{API_SERVER_PORT:-3004\}|accept_api_port=\$\{API_SERVER_PORT:-3004\}/,
        'startup acceptance must not inherit API_SERVER_PORT from the parent shell',
      );
      assert.doesNotMatch(
        content,
        /ACCEPT_WEB_PORT=\$\{FRONTEND_PORT:-3003\}|accept_web_port=\$\{FRONTEND_PORT:-3003\}/,
        'startup acceptance must not inherit FRONTEND_PORT from the parent shell',
      );
      assert.match(
        content,
        /accept_api_port="\$\(find_available_port 3004\)"/,
        'startup acceptance should choose its API port from a script-owned helper',
      );
      assert.match(
        content,
        /accept_web_port="\$\(find_available_port 3003 "\$accept_api_port"\)"/,
        'startup acceptance should choose a distinct frontend port from a script-owned helper',
      );
    });

    it('startup acceptance does not treat the public Preview Gateway port as forbidden leakage', () => {
      const gate = readFunctionBody(readSyncScript(), 'run_target_public_gate');
      assert.match(
        gate,
        /forbidden_ports="3001\|3002\|3011\|3012\|4111\|4000\|6398\|6399"/,
        'startup acceptance should only block internal/runtime ports, not the public Preview Gateway default',
      );
      assert.doesNotMatch(
        gate,
        /forbidden_ports=.*4100/,
        'startup acceptance must not reject the exported Preview Gateway default port 4100',
      );
    });

    it('startup acceptance forces watchpack polling for frontend dev server', () => {
      const gate = readFunctionBody(readSyncScript(), 'run_target_public_gate');
      assert.match(
        gate,
        /run_public_acceptance_env WATCHPACK_POLLING=true PORT=\$accept_web_port\s+\\\s+pnpm --filter @cat-cafe\/web dev -p \$accept_web_port/,
        'startup acceptance should avoid native watchpack EMFILE failures on release machines with many worktrees',
      );
    });

    it('startup acceptance bounds readiness probes by wall-clock deadlines', () => {
      const content = readSyncScript();
      const gate = readFunctionBody(content, 'run_target_public_gate');
      const remainingHelper = readFunctionBody(content, 'remaining_wall_clock_seconds');
      const timeoutHelper = readFunctionBody(content, 'curl_probe_timeout');

      assert.match(
        remainingHelper,
        /remaining=\$\(\( deadline - now \)\)/,
        'remaining_wall_clock_seconds should calculate time left from an absolute deadline',
      );
      assert.match(
        timeoutHelper,
        /if \[ "\$remaining" -lt "\$max_timeout" \]; then/,
        'curl_probe_timeout should cap each probe by the remaining wall-clock budget',
      );
      assert.match(
        gate,
        /web_deadline=\$\(\( \$\(date \+%s\) \+ web_wait_seconds \)\)/,
        'frontend readiness should use an absolute deadline derived from PUBLIC_GATE_FRONTEND_WAIT_SECONDS',
      );
      assert.match(
        gate,
        /remaining="\$\(remaining_wall_clock_seconds "\$web_deadline"\)"/,
        'frontend readiness should recompute remaining wall-clock time on each probe',
      );
      assert.match(
        gate,
        /curl_timeout="\$\(curl_probe_timeout "\$remaining" 5\)"/,
        'frontend readiness should cap each curl probe instead of multiplying a long timeout by loop count',
      );
      assert.doesNotMatch(
        gate,
        /for i in \$\(seq 1 "\$web_wait_seconds"\)[\s\S]*curl -sf --max-time 30/,
        'frontend readiness must not multiply a 30s curl timeout by the advertised wait seconds',
      );
    });
  },
);

describe(
  'Public-facing skill docs avoid home-only API defaults',
  { skip: !isHomeRepo && 'sync infrastructure not present (open-source repo)' },
  () => {
    it('workspace-navigator uses API_SERVER_PORT env instead of hardcoded 3002 fallbacks', () => {
      const content = readFileSync(resolve(ROOT, 'cat-cafe-skills/workspace-navigator/SKILL.md'), 'utf-8');
      assert.doesNotMatch(
        content,
        /API_SERVER_PORT=3004|API_SERVER_PORT:-3004/,
        'workspace-navigator should not hardcode the home-only API default in public-facing usage guidance',
      );
      assert.match(
        content,
        /API_PORT="\$\{API_SERVER_PORT:\?set API_SERVER_PORT before calling Navigate API\}"/,
        'workspace-navigator should teach readers to source the API port from the runtime environment',
      );
    });
  },
);
