Ctrl K
ComunidadGuías…

tests/integration/cli.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { fakeFetch } from "../helpers/fetch";
import { afterAll, test } from "bun:test";
import assert from "node:assert/strict";
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createSqliteDb, openDatabase } from "../../src/runtime/bun/sqlite";
import { MIGRATIONS, SCHEMA_VERSION, schemaState } from "../../src/core/migrations";
import { fetchStableRelease, stableVersionStatus } from "../../src/runtime/bun/release";

const root = mkdtempSync(join(tmpdir(), "titen-cli-"));
const cli = join(import.meta.dir, "../../src/runtime/bun/cli.ts");
const version = (JSON.parse(readFileSync(join(import.meta.dir, "../../package.json"), "utf8")) as {
  version: string;
}).version;
const releasePreload = join(root, "release-preload.ts");
writeFileSync(releasePreload, `
const nativeFetch = globalThis.fetch;
globalThis.fetch = (input, init) => {
  if (String(input) === "https://titen.dev/version.json" && process.env.TITEN_TEST_RELEASE)
    return Promise.resolve(new Response(process.env.TITEN_TEST_RELEASE, { status: 200 }));
  return nativeFetch(input, init);
};
`);

afterAll(() => rmSync(root, { recursive: true, force: true }));

function run(name: string, args: string[], release?: unknown, env?: Record<string, string>) {
  const cwd = join(root, name);
  mkdirSync(cwd, { recursive: true });
  const result = Bun.spawnSync({
    cmd: release === undefined
      ? ["bun", cli, ...args]
      : ["bun", "--preload", releasePreload, cli, ...args],
    cwd,
    env: {
      ...process.env,
      ...env,
      ...(release === undefined ? {} : { TITEN_TEST_RELEASE: JSON.stringify(release) }),
    },
  });
  return {
    exitCode: result.exitCode,
    output: `${result.stdout.toString()}${result.stderr.toString()}`,
    files: readdirSync(cwd),
  };
}

test("help is side-effect free for every documented command", () => {
  const cases = [
    ["top", ["--help"]],
    ["version", ["version", "--help"]],
    ["import-source", ["import-source", "--help"]],
    ["mcp", ["mcp", "--help"]],
    ["dashboard", ["dashboard", "--help"]],
    ["serve", ["serve", "--help"]],
    ["migrate", ["migrate", "--help"]],
    ["bootstrap", ["bootstrap", "--help"]],
    ["key-create", ["key", "create", "--help"]],
    ["key-list", ["key", "list", "--help"]],
    ["key-revoke", ["key", "revoke", "--help"]],
    ["principal-reassign", ["principal", "reassign", "--help"]],
    ["jobs-list", ["jobs", "list", "--help"]],
    ["jobs-retry", ["jobs", "retry", "--help"]],
    ["jobs-ack", ["jobs", "ack", "--help"]],
    ["jobs-purge", ["jobs", "purge", "--help"]],
    ["project-create", ["project", "create", "--help"]],
    ["project-update", ["project", "update", "--help"]],
    ["backup", ["backup", "--help"]],
    ["schema", ["schema", "--help"]],
  ] as const;

  for (const [name, args] of cases) {
    const result = run(`help-${name}`, [...args]);
    assert.equal(result.exitCode, 0, `${name} help must succeed: ${result.output}`);
    assert.match(result.output, /Usage:/);
    assert.doesNotMatch(result.output, /api_key:|titen listening/);
    assert.deepEqual(result.files, [], `${name} help created state`);
  }
});

test("version is exact and side-effect free", () => {
  const result = run("version", ["--version"]);
  assert.equal(result.exitCode, 0);
  assert.equal(result.output, `${version}\n`);
  assert.deepEqual(result.files, []);

  const command = run("version-command", ["version"]);
  assert.equal(command.exitCode, 0);
  assert.equal(command.output, `${version}\nschema_version ${SCHEMA_VERSION}\n`);
  assert.deepEqual(command.files, []);
});

test("version check validates and reports the stable CLI and plugin release", async () => {
  const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version)!;
  const nextStable = `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
  const release = {
    schema: 1,
    channel: "stable",
    cli: { version: nextStable },
    plugin: { version: "0.2.0" },
  };
  const result = run("version-check", ["version", "--check"], release);
  assert.equal(result.exitCode, 0, result.output);
  assert.equal(result.output, [
    `CLI installed: ${version}`,
    `CLI stable:    ${nextStable}`,
    "CLI status:    update available",
    "Plugin stable: 0.2.0",
    `Release notes: https://titen.dev/releases/${nextStable}`,
    "Install/update: https://titen.dev/docs/install",
    "",
  ].join("\n"));
  assert.deepEqual(result.files, []);

  const invalid = run("version-check-invalid", ["version", "--check"], {
    ...release,
    channel: "preview",
  });
  assert.notEqual(invalid.exitCode, 0);
  assert.match(invalid.output, /error: invalid stable release manifest/);
  assert.deepEqual(invalid.files, []);

  let requestInit: RequestInit | undefined;
  assert.deepEqual(
    await fetchStableRelease(fakeFetch(async (_input, init) => {
      requestInit = init;
      return new Response(JSON.stringify(release));
    })),
    { cliVersion: nextStable, pluginVersion: "0.2.0" },
  );
  assert.equal(requestInit?.redirect, "error", "the fixed release URL must not follow redirects");

  const rejected = [
    [new Response("redirect", { status: 302 }), /release endpoint returned HTTP 302/],
    [new Response("unavailable", { status: 503 }), /release endpoint returned HTTP 503/],
    [new Response("{"), /release endpoint returned invalid JSON/],
    [new Response(JSON.stringify({ ...release, schema: 2 })), /invalid stable release manifest/],
    [new Response(JSON.stringify({ ...release, cli: { version: "v0.5.0" } })), /invalid stable release manifest/],
    [new Response(JSON.stringify({ ...release, plugin: { version: "latest" } })), /invalid stable release manifest/],
  ] as const;
  for (const [response, message] of rejected)
    await assert.rejects(fetchStableRelease(fakeFetch(async () => response)), message);

  assert.equal(stableVersionStatus("0.5.0", "0.5.0"), "current");
  assert.equal(stableVersionStatus("0.6.0", "0.5.0"), "ahead");
  assert.equal(stableVersionStatus("0.6.0-rc.1", "0.5.0"), "prerelease");
});

test("the bin names Bun and its install page when Bun is missing from PATH", () => {
  // npm and pnpm link `node_modules/.bin/titen` straight at this file, so the
  // kernel reads its shebang. Reproduce that exact entry rather than `bun cli.ts`.
  const linked = join(root, "bun-missing-bin");
  mkdirSync(linked, { recursive: true });
  const bin = join(linked, "titen");
  symlinkSync(cli, bin);
  const emptyPath = join(root, "bun-missing-path");
  mkdirSync(emptyPath, { recursive: true });

  const missing = Bun.spawnSync({
    cmd: [bin, "--version"],
    env: { PATH: emptyPath, HOME: process.env.HOME ?? "" },
  });
  const output = `${missing.stdout.toString()}${missing.stderr.toString()}`;
  assert.equal(missing.exitCode, 1, output);
  assert.match(output, /^titen: error: bun was not found on PATH\.$/m);
  assert.match(output, /Bun 1\.2 or newer.*https:\/\/bun\.sh/);
  assert.doesNotMatch(output, /No such file or directory/);

  // The same entry must still reach the CLI when Bun is the only runtime on PATH.
  const bunOnlyPath = join(root, "bun-only-path");
  mkdirSync(bunOnlyPath, { recursive: true });
  symlinkSync(process.execPath, join(bunOnlyPath, "bun"));
  const present = Bun.spawnSync({
    cmd: [bin, "--version"],
    env: { PATH: bunOnlyPath, HOME: process.env.HOME ?? "" },
  });
  assert.equal(
    present.exitCode,
    0,
    `${present.stdout.toString()}${present.stderr.toString()}`,
  );
  assert.equal(present.stdout.toString(), `${version}\n`);
});

test("malformed flags fail before side effects for every command", () => {
  const cases = [
    ["serve", ["serve", "--port", "nope"]],
    ["import-source", ["import-source", "memory.md", "--from"]],
    ["mcp", ["mcp", "--api-key", "must-not-be-accepted"]],
    ["dashboard", ["dashboard", "--port", "nope"]],
    ["migrate", ["migrate", "--db"]],
    ["bootstrap", ["bootstrap", "--org"]],
    ["key-create", ["key", "create", "--org-id"]],
    ["key-list", ["key", "list", "--db"]],
    ["key-revoke", ["key", "revoke", "--id"]],
    ["project-create", ["project", "create", "--org-id"]],
    ["project-update", ["project", "update", "--default-visibility"]],
    ["backup", ["backup", "--out"]],
    ["schema", ["schema", "--unknown"]],
  ] as const;

  for (const [name, args] of cases) {
    const result = run(`invalid-${name}`, [...args]);
    assert.notEqual(result.exitCode, 0, `${name} malformed input unexpectedly succeeded`);
    assert.match(result.output, /error:/);
    assert.doesNotMatch(result.output, /api_key:|titen listening/);
    assert.deepEqual(result.files, [], `${name} malformed input created state`);
  }
});

test("bootstrap creates owner login with a one-time random password", () => {
  const boot = run("bootstrap-owner-login", ["bootstrap", "--db", "titen.db", "--org", "Example"]);
  assert.equal(boot.exitCode, 0, boot.output);
  assert.match(boot.output, /^dashboard_username: owner$/m);
  const temporary = /^temporary_password: ([A-Za-z0-9_-]{24})$/m.exec(boot.output)?.[1];
  assert.ok(temporary);
  assert.match(boot.output, /Change the temporary password on first dashboard sign-in\./);
  const database = openDatabase(join(root, "bootstrap-owner-login", "titen.db"), { create: false, readonly: true });
  try {
    const account = database.query(`SELECT a.username, a.password_verifier, a.must_change_password, m.role
      FROM operator_accounts a JOIN memberships m
        ON m.org_id = a.org_id AND m.principal_id = a.principal_id
      WHERE a.username = 'owner'`).get() as {
        username: string;
        password_verifier: string;
        must_change_password: number;
        role: string;
      };
    assert.equal(account.username, "owner");
    assert.equal(account.role, "owner");
    assert.equal(account.must_change_password, 1);
    assert.match(account.password_verifier, /^pbkdf2-sha256\$100000x6\$/);
    assert.ok(!account.password_verifier.includes(temporary));
  } finally {
    database.close();
  }
});

test("port zero, non-decimal integers, and values outside the TCP range are rejected", () => {
  for (const value of ["0", "65536", "1.5", "1e3", "+80", "NaN"]) {
    const result = run(`port-${value.replace(".", "-")}`, ["serve", "--port", value]);
    assert.notEqual(result.exitCode, 0);
    assert.match(result.output, /--port must be an integer between 1 and 65535/);
    assert.deepEqual(result.files, []);
  }
});

test("key lifecycle flags survive listing and verified backup", async () => {
  const boot = run("key-lifecycle", ["bootstrap", "--db", "source.db"]);
  assert.equal(boot.exitCode, 0, boot.output);
  const orgId = /^organization: (org_[^ ]+)/m.exec(boot.output)?.[1];
  assert.ok(orgId);
  const notBefore = new Date(Date.now() + 86_400_000).toISOString();
  const expiresAt = new Date(Date.now() + 90_000_000).toISOString();
  const created = run("key-lifecycle", [
    "key", "create", "--db", "source.db", "--org-id", orgId,
    "--not-before", notBefore, "--expires-at", expiresAt,
  ]);
  assert.equal(created.exitCode, 0, created.output);
  const keyId = /^key_id: (key_[^\s]+)/m.exec(created.output)?.[1];
  assert.ok(keyId);
  const listed = run("key-lifecycle", ["key", "list", "--db", "source.db"]);
  assert.equal(listed.exitCode, 0, listed.output);
  assert.match(listed.output, new RegExp(`${keyId}  pending.*not_before=${notBefore}.*expires_at=${expiresAt}`));

  const expiredBefore = new Date(Date.now() - 7_200_000).toISOString();
  const expiredAt = new Date(Date.now() - 3_600_000).toISOString();
  const expired = run("key-lifecycle", [
    "key", "create", "--db", "source.db", "--org-id", orgId,
    "--not-before", expiredBefore, "--expires-at", expiredAt,
  ]);
  assert.equal(expired.exitCode, 0, expired.output);
  const expiredId = /^key_id: (key_[^\s]+)/m.exec(expired.output)?.[1];
  assert.ok(expiredId);
  const relisted = run("key-lifecycle", ["key", "list", "--db", "source.db"]);
  assert.match(relisted.output, new RegExp(`${expiredId}  expired`));

  const invalid = run("key-lifecycle", [
    "key", "create", "--db", "source.db", "--org-id", orgId,
    "--not-before", expiresAt, "--expires-at", notBefore,
  ]);
  assert.notEqual(invalid.exitCode, 0);
  assert.match(invalid.output, /--not-before must be earlier/u);

  const backup = run("key-lifecycle", [
    "backup", "--db", "source.db", "--out", "restored.db",
  ]);
  assert.equal(backup.exitCode, 0, backup.output);
  const handle = openDatabase(join(root, "key-lifecycle", "restored.db"), { create: false });
  try {
    const row = await createSqliteDb(handle).all<{
      not_before: string;
      expires_at: string;
      last_used_at: string | null;
    }>("SELECT not_before, expires_at, last_used_at FROM api_keys WHERE id = ?", [keyId]);
    assert.deepEqual(row, [{
      not_before: notBefore,
      expires_at: expiresAt,
      last_used_at: null,
    }]);
  } finally {
    handle.close();
  }
});

test("key create stores subject and project fences and key list prints them", () => {
  const boot = run("key-fences", ["bootstrap", "--db", "source.db"]);
  assert.equal(boot.exitCode, 0, boot.output);
  const orgId = /^organization: (org_[^ ]+)/m.exec(boot.output)?.[1];
  assert.ok(orgId);
  const created = run("key-fences", [
    "key", "create", "--db", "source.db", "--org-id", orgId,
    "--subjects", "castle:profile:alice,castle:shared",
    "--projects", "project_alice",
    "--read-subjects", "castle:profile:*",
  ]);
  assert.equal(created.exitCode, 0, created.output);
  const keyId = /^key_id: (key_[^\s]+)/m.exec(created.output)?.[1];
  assert.ok(keyId);
  const listed = run("key-fences", ["key", "list", "--db", "source.db"]);
  assert.match(listed.output, new RegExp(
    `${keyId} .* read_projects=-  read_subjects=castle:profile:\\*  write_projects=project_alice  write_subjects=castle:profile:alice,castle:shared`,
  ));
  const database = openDatabase(join(root, "key-fences", "source.db"), { create: false, readonly: true });
  try {
    const fences = database.query(
      `SELECT access, target_type, pattern FROM api_key_fences WHERE key_id = ? ORDER BY access, target_type, pattern`,
    ).all(keyId) as { access: string; target_type: string; pattern: string }[];
    assert.deepEqual(fences, [
      { access: "read", target_type: "subject", pattern: "castle:profile:*" },
      { access: "write", target_type: "project", pattern: "project_alice" },
      { access: "write", target_type: "subject", pattern: "castle:profile:alice" },
      { access: "write", target_type: "subject", pattern: "castle:shared" },
    ]);
    const audit = database.query(
      `SELECT actor_id, detail FROM audit_log WHERE action = 'key.create' AND resource_id = ?`,
    ).get(keyId) as { actor_id: string; detail: string };
    assert.equal(audit.actor_id, "cli");
    const detail = JSON.parse(audit.detail) as { write_subjects: string[]; write_projects: string[] };
    assert.deepEqual(detail.write_subjects, ["castle:profile:alice", "castle:shared"]);
    assert.deepEqual(detail.write_projects, ["project_alice"]);
  } finally {
    database.close();
  }

  const conflict = run("key-fences", [
    "key", "create", "--db", "source.db", "--org-id", orgId,
    "--subjects", "castle:shared", "--write-subjects", "castle:shared",
  ]);
  assert.notEqual(conflict.exitCode, 0);
  assert.match(conflict.output, /pass only one of --write-subjects and --subjects/);

  const printed = run("key-fence-sql", [
    "key", "create", "--print-sql", "--org-id", "org_remote",
    "--subjects", "castle:profile:alice",
  ]);
  assert.equal(printed.exitCode, 0, printed.output);
  assert.match(printed.output, /INSERT INTO api_key_fences/);
  assert.match(printed.output, /castle:profile:alice/);
  assert.deepEqual(printed.files, []);
});

test("local key administration fails closed for missing organizations and keys", () => {
  const missingCases = [
    ["list", ["key", "list", "--db", "missing.db"]],
    ["revoke", ["key", "revoke", "--db", "missing.db", "--id", "key_missing"]],
    ["create", ["key", "create", "--db", "missing.db", "--org-id", "org_missing"]],
  ] as const;
  for (const [name, args] of missingCases) {
    const result = run(`key-missing-${name}`, [...args]);
    assert.notEqual(result.exitCode, 0);
    assert.match(result.output, /error: database does not exist: .*missing\.db/);
    assert.doesNotMatch(result.output, /SQLiteError|node_modules|src\/runtime|\n\s+at /);
    assert.deepEqual(result.files, []);
  }

  const boot = run("key-fail-closed", ["bootstrap", "--db", "keys.db"]);
  assert.equal(boot.exitCode, 0, boot.output);
  const ownerKey = /^key_id: (key_[^\s]+)/m.exec(boot.output)?.[1];
  assert.ok(ownerKey);
  const path = join(root, "key-fail-closed", "keys.db");
  const countKeys = () => {
    const database = openDatabase(path, { create: false, readonly: true });
    try {
      return Number((database.query("SELECT COUNT(*) AS count FROM api_keys").get() as { count: number }).count);
    } finally {
      database.close();
    }
  };

  const before = countKeys();
  const unknownOrganization = run("key-fail-closed", [
    "key", "create", "--db", "keys.db", "--org-id", "org_missing",
  ]);
  assert.notEqual(unknownOrganization.exitCode, 0);
  assert.equal(unknownOrganization.output, "error: organization not found: org_missing\n");
  assert.doesNotMatch(unknownOrganization.output, /api_key:|SQLiteError|\n\s+at /);
  assert.equal(countKeys(), before);

  const unknownKey = run("key-fail-closed", [
    "key", "revoke", "--db", "keys.db", "--id", "key_missing",
  ]);
  assert.notEqual(unknownKey.exitCode, 0);
  assert.equal(unknownKey.output, "error: key not found: key_missing\n");

  const revoked = run("key-fail-closed", [
    "key", "revoke", "--db", "keys.db", "--id", ownerKey,
  ]);
  assert.equal(revoked.exitCode, 0, revoked.output);
  assert.equal(revoked.output, `revoked ${ownerKey}\n`);

  const repeated = run("key-fail-closed", [
    "key", "revoke", "--db", "keys.db", "--id", ownerKey,
  ]);
  assert.notEqual(repeated.exitCode, 0);
  assert.equal(repeated.output, `error: key already revoked: ${ownerKey}\n`);
});

test("backup refuses a missing source and atomically refreshes a fixed target", async () => {
  const missing = run("backup-missing", ["backup", "--db", "missing.db", "--out", "latest.db"]);
  assert.notEqual(missing.exitCode, 0);
  assert.match(missing.output, /error: database does not exist/);
  assert.doesNotMatch(missing.output, /SQLiteError|\n\s+at /);
  assert.deepEqual(missing.files, []);

  assert.equal(run("backup-repeat", ["bootstrap", "--db", "source.db", "--org", "First"]).exitCode, 0);
  const first = run("backup-repeat", ["backup", "--db", "source.db", "--out", "latest.db"]);
  assert.equal(first.exitCode, 0, first.output);
  assert.match(first.output, /backup verified:/);
  assert.equal(statSync(join(root, "backup-repeat", "latest.db")).mode & 0o777, 0o600);

  const sourcePath = join(root, "backup-repeat", "source.db");
  const source = openDatabase(sourcePath, { create: false });
  try {
    source.run(
      "INSERT INTO titen_migrations (version, applied_at) VALUES (?, ?)",
      [SCHEMA_VERSION + 1, "2026-07-31T00:00:00.000Z"],
    );
  } finally {
    source.close();
  }
  const failedRefresh = run("backup-repeat", ["backup", "--db", "source.db", "--out", "latest.db"]);
  assert.notEqual(failedRefresh.exitCode, 0);
  assert.match(failedRefresh.output, /error: backup failed: schema verification failed/);
  assert.doesNotMatch(failedRefresh.output, /SQLiteError|\n\s+at /);
  const preserved = openDatabase(join(root, "backup-repeat", "latest.db"), { create: false });
  try {
    assert.equal((preserved.query("SELECT COUNT(*) AS count FROM organizations").get() as { count: number }).count, 1);
  } finally {
    preserved.close();
  }
  const repaired = openDatabase(sourcePath, { create: false });
  try {
    repaired.run("DELETE FROM titen_migrations WHERE version = ?", [SCHEMA_VERSION + 1]);
  } finally {
    repaired.close();
  }

  assert.equal(run("backup-repeat", ["bootstrap", "--db", "source.db", "--org", "Second", "--username", "owner-second"]).exitCode, 0);
  const second = run("backup-repeat", ["backup", "--db", "source.db", "--out", "latest.db"]);
  assert.equal(second.exitCode, 0, second.output);
  assert.doesNotMatch(second.output, /SQLiteError|\n\s+at /);
  assert.equal(statSync(join(root, "backup-repeat", "latest.db")).mode & 0o777, 0o600);

  const copy = openDatabase(join(root, "backup-repeat", "latest.db"), { create: false });
  try {
    assert.equal((copy.query("SELECT COUNT(*) AS count FROM organizations").get() as { count: number }).count, 2);
    assert.deepEqual(await schemaState(createSqliteDb(copy)), {
      applied: SCHEMA_VERSION,
      expected: SCHEMA_VERSION,
      verified: true,
    });
    assert.equal((copy.query("PRAGMA integrity_check").get() as { integrity_check: string }).integrity_check, "ok");
  } finally {
    copy.close();
  }
  assert.ok(!readdirSync(join(root, "backup-repeat")).some((name) => name.includes(".tmp-")));
});

test("migrate dry-run is read-only and schema output is deterministic", () => {
  const fresh = run("dry-run-fresh", ["migrate", "--db", "missing.db", "--dry-run"]);
  assert.equal(fresh.exitCode, 0, fresh.output);
  assert.match(fresh.output, /-- migration 1/);
  assert.match(fresh.output, new RegExp(`-- ${MIGRATIONS.length} migration\\(s\\) pending; database unchanged`));
  assert.deepEqual(fresh.files, []);

  assert.equal(run("dry-run-current", ["bootstrap", "--db", "current.db"]).exitCode, 0);
  const currentDirectory = join(root, "dry-run-current");
  const currentPath = join(currentDirectory, "current.db");
  chmodSync(currentPath, 0o400);
  const before = statSync(currentPath);
  chmodSync(currentDirectory, 0o500);
  let current: ReturnType<typeof run>;
  try {
    current = run("dry-run-current", ["migrate", "--db", "current.db", "--dry-run"]);
  } finally {
    chmodSync(currentDirectory, 0o700);
  }
  const after = statSync(currentPath);
  assert.equal(current.exitCode, 0, current.output);
  assert.match(current.output, /-- 0 migration\(s\) pending; database unchanged/);
  assert.equal(after.size, before.size);
  assert.equal(after.mtimeMs, before.mtimeMs);
  assert.equal(after.mode & 0o777, 0o400);

  const first = run("schema-deterministic", ["schema"]);
  const second = run("schema-deterministic", ["schema"]);
  assert.equal(first.output, second.output);
  assert.match(first.output, /INSERT OR IGNORE INTO titen_migrations/);
  assert.match(first.output, /1970-01-01T00:00:00\.000Z/);
});

test("serve reports a port collision without a stack trace", async () => {
  const blocker = Bun.serve({
    port: 0,
    hostname: "127.0.0.1",
    fetch: () => new Response("busy"),
  });
  try {
    const boot = run("busy-port", ["bootstrap", "--db", "titen.db"]);
    assert.equal(boot.exitCode, 0, boot.output);
    const result = run("busy-port", ["serve", "--db", "titen.db", "--port", String(blocker.port)]);
    assert.notEqual(result.exitCode, 0);
    assert.equal(result.output, `error: port ${blocker.port} is already in use\n`);
    assert.doesNotMatch(result.output, /node_modules|src\/runtime|\bat\s/);
  } finally {
    await blocker.stop(true);
  }
});

test("serve --quiet accepts requests without startup or access output", async () => {
  const reservation = Bun.serve({
    port: 0,
    hostname: "127.0.0.1",
    fetch: () => new Response("reserved"),
  });
  const port = reservation.port;
  await reservation.stop(true);

  const cwd = join(root, "quiet-serve");
  mkdirSync(cwd);
  const boot = run("quiet-serve", ["bootstrap", "--db", "titen.db"]);
  assert.equal(boot.exitCode, 0, boot.output);
  const process = Bun.spawn({
    cmd: ["bun", cli, "serve", "--db", "titen.db", "--port", String(port), "--quiet"],
    cwd,
    stdout: "pipe",
    stderr: "pipe",
  });
  try {
    let response: Response | undefined;
    const deadline = Date.now() + 5_000;
    while (Date.now() < deadline) {
      try {
        response = await fetch(`http://127.0.0.1:${port}/healthz`);
        if (response.ok) break;
      } catch {
        await Bun.sleep(25);
      }
    }
    assert.equal(response?.status, 200, "quiet server never became healthy");
  } finally {
    process.kill("SIGTERM");
    await process.exited;
  }
  const output = `${await new Response(process.stdout).text()}${await new Response(process.stderr).text()}`;
  assert.equal(output, "");
});

test("bootstrap and serve share one stable per-user database across directories", async () => {
  const home = join(root, "stable-home");
  const first = run("stable-cwd-a", ["bootstrap", "--org", "Stable Org"], undefined, { HOME: home });
  assert.equal(first.exitCode, 0, first.output);
  const dbPath = join(home, ".titen", "service.db");
  assert.match(first.output, new RegExp(`^database: ${dbPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
  assert.ok(statSync(dbPath).isFile());
  assert.equal(statSync(join(home, ".titen")).mode & 0o777, 0o700);
  const key = /^api_key: (titen_sk_[A-Za-z0-9_-]+)$/m.exec(first.output)?.[1];
  assert.ok(key);

  const reservation = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: () => new Response("reserved") });
  const port = reservation.port;
  await reservation.stop(true);
  const cwd = join(root, "stable-cwd-b");
  mkdirSync(cwd, { recursive: true });
  const serverProcess = Bun.spawn({
    cmd: ["bun", cli, "serve", "--port", String(port), "--quiet"],
    cwd,
    env: { ...process.env, HOME: home },
    stdout: "pipe",
    stderr: "pipe",
  });
  try {
    let response: Response | undefined;
    const deadline = Date.now() + 5_000;
    while (Date.now() < deadline) {
      try {
        response = await fetch(`http://127.0.0.1:${port}/v1/observations`, {
          method: "POST",
          headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
          body: JSON.stringify({
            subject_id: "stable-default-store",
            kind: "system_event",
            content: "Bootstrap and serve resolved the same per-user store.",
            source: { type: "contract", ref: "stable-store#1" },
          }),
        });
        if (response.status !== 503) break;
      } catch {
        await Bun.sleep(25);
      }
    }
    assert.equal(response?.status, 201, await response?.text());
  } finally {
    serverProcess.kill("SIGTERM");
    await serverProcess.exited;
  }
});

test("serve refuses a missing store and unflagged commands do not bypass a legacy cwd store", () => {
  const home = join(root, "missing-home");
  const missing = run("missing-default-store", ["serve", "--quiet"], undefined, { HOME: home });
  assert.notEqual(missing.exitCode, 0);
  assert.match(missing.output, /database does not exist: .*\.titen\/service\.db/);
  assert.match(missing.output, /titen bootstrap/);
  assert.deepEqual(missing.files, []);

  const legacyCwd = join(root, "legacy-default-store");
  mkdirSync(legacyCwd, { recursive: true });
  writeFileSync(join(legacyCwd, "titen.db"), "legacy-placeholder");
  const legacy = run("legacy-default-store", ["bootstrap"], undefined, { HOME: join(root, "legacy-home") });
  assert.notEqual(legacy.exitCode, 0);
  assert.match(legacy.output, /legacy database found at .*titen\.db/);
  assert.match(legacy.output, /rerun with --db .*titen\.db/);
  assert.deepEqual(readdirSync(legacyCwd), ["titen.db"]);
});

test("host account recovery clears only its account and forces password rotation", async () => {
  const cwdName = "account-recovery";
  const dbPath = join(root, cwdName, "service.db");
  const bootstrap = run(cwdName, ["bootstrap", "--db", dbPath, "--username", "recovery-owner"]);
  assert.equal(bootstrap.exitCode, 0);
  const listed = run(cwdName, ["account", "list", "--db", dbPath]);
  assert.equal(listed.exitCode, 0, listed.output);
  assert.match(listed.output, /recovery-owner/);
  assert.match(listed.output, /passkey_count/);
  const handle = openDatabase(dbPath);
  const { sha256Hex } = await import("../../src/core/ids");
  const key = await sha256Hex("dashboard-login:recovery-owner");
  handle.run("INSERT INTO login_throttles VALUES (?, 5, ?, ?)", [key, Date.now() + 60_000, Date.now()]);
  handle.run("INSERT INTO login_throttles VALUES (?, 5, ?, ?)", [`${key}.client-a`, Date.now() + 60_000, Date.now()]);
  handle.run("INSERT INTO login_throttles VALUES ('other-account', 5, ?, ?)", [Date.now() + 60_000, Date.now()]);
  const unlocked = run(cwdName, ["account", "unlock", "--db", dbPath, "--username", "RECOVERY-OWNER"]);
  assert.equal(unlocked.exitCode, 0, unlocked.output);
  assert.equal((handle.query("SELECT COUNT(*) AS count FROM login_throttles").get() as { count: number }).count, 1);
  const account = handle.query("SELECT * FROM operator_accounts").get() as any;
  const { createApiKey } = await import("../../src/core/auth");
  const session = await createApiKey({ orgId: account.org_id, principalId: account.principal_id, principalKind: "human", label: "Dashboard session", scopes: ["*"], maxTrust: "asserted" });
  const agent = await createApiKey({ orgId: account.org_id, principalId: account.principal_id, principalKind: "agent", label: "Agent key", scopes: ["*"], maxTrust: "asserted" });
  await createSqliteDb(handle).batch([session.statement, agent.statement]);
  const reset = run(cwdName, ["account", "reset-password", "--db", dbPath, "--username", "recovery-owner"]);
  assert.equal(reset.exitCode, 0, reset.output);
  const temporary = reset.output.match(/temporary_password: (\S+)/)?.[1];
  assert.ok(temporary);
  assert.equal((handle.query("SELECT must_change_password FROM operator_accounts").get() as { must_change_password: number }).must_change_password, 1);
  assert.ok((handle.query("SELECT revoked_at FROM api_keys WHERE id = ?").get(session.id) as { revoked_at: string | null }).revoked_at);
  assert.equal((handle.query("SELECT revoked_at FROM api_keys WHERE id = ?").get(agent.id) as { revoked_at: string | null }).revoked_at, null);
  const { createApp } = await import("../../src/core/app");
  const app = createApp({ db: createSqliteDb(handle), runtime: "test" });
  const response = await app(new Request("http://test/v1/dashboard-sessions", { method: "POST", body: JSON.stringify({ username: "recovery-owner", password: temporary }) }));
  assert.equal(response.status, 201);
  assert.equal((await response.json() as any).data.auth_stage, "password_change");
  handle.close();
  assert.equal(run(cwdName, ["account", "unlock", "--db", dbPath, "--username", "missing-user"]).exitCode, 1);
  assert.equal(run(cwdName, ["account", "list", "--db", join(root, "absent-account.db")]).exitCode, 1);
});

test("project commands set default visibility on a new and an existing project", () => {
  const boot = run("project-visibility", ["bootstrap", "--db", "service.db", "--org", "Castle"]);
  assert.equal(boot.exitCode, 0, boot.output);
  const orgId = /^organization: (org_[^ ]+)/m.exec(boot.output)?.[1];
  assert.ok(orgId);
  const created = run("project-visibility", [
    "project", "create", "--db", "service.db", "--org-id", orgId,
    "--reference", "github.com/Castle/Shared.git",
    "--default-visibility", "organization",
  ]);
  assert.equal(created.exitCode, 0, created.output);
  assert.match(created.output, /reference: castle\/shared/);
  assert.match(created.output, /default_visibility: organization/);
  const projectId = /^project_id: (project_\S+)/m.exec(created.output)?.[1];
  assert.ok(projectId);

  const plain = run("project-visibility", [
    "project", "create", "--db", "service.db", "--org-id", orgId, "--reference", "castle/plain",
  ]);
  assert.equal(plain.exitCode, 0, plain.output);
  assert.match(plain.output, /default_visibility: private/);
  const plainId = /^project_id: (project_\S+)/m.exec(plain.output)?.[1];
  assert.ok(plainId);
  const updated = run("project-visibility", [
    "project", "update", "--db", "service.db", "--org-id", orgId,
    "--reference", "castle/plain", "--default-visibility", "organization",
  ]);
  assert.equal(updated.exitCode, 0, updated.output);
  assert.match(updated.output, new RegExp(`project_id: ${plainId}`));
  assert.match(updated.output, /default_visibility: organization/);

  const database = openDatabase(join(root, "project-visibility", "service.db"));
  const shared = database.query(
    "SELECT default_visibility FROM projects WHERE id = ?",
  ).get(projectId) as { default_visibility: string };
  const changed = database.query(
    "SELECT default_visibility FROM projects WHERE id = ?",
  ).get(plainId) as { default_visibility: string };
  assert.equal(shared.default_visibility, "organization");
  assert.equal(changed.default_visibility, "organization");
  database.close();
});

test("jobs ack clears readiness and principal reassign can preview private ownership", () => {
  const boot = run("ops-feedback", ["bootstrap", "--db", "titen.db", "--org", "Example"]);
  assert.equal(boot.exitCode, 0, boot.output);
  const dbPath = join(root, "ops-feedback", "titen.db");
  const database = openDatabase(dbPath);
  const org = database.query("SELECT id FROM organizations LIMIT 1").get() as { id: string };
  const hex = "ab".repeat(32);
  const now = "2026-01-01T00:00:00.000Z";
  database.query(
    `INSERT INTO enrichment_jobs (
       id, org_id, lane, derivation_key, input_ids, input_hash, subject_id, actor_id,
       model_id, model_fingerprint, prompt_fingerprint, schema_fingerprint, policy_fingerprint,
       state, attempts, max_attempts, next_attempt_at, created_at, updated_at
     ) VALUES (?, ?, 'derivation', ?, '[]', ?, 'team:shared', 'agent_old',
       'fixture-model', ?, ?, ?, ?, 'failed', 4, 4, ?, ?, ?)`,
  ).run("job_failed", org.id, hex, hex, hex, hex, hex, hex, now, now, now);
  database.query(
    `INSERT INTO observations (
       id, org_id, subject_id, actor_id, kind, content, content_hash, source_type, source_ref,
       trust, visibility, ingested_at
     ) VALUES (?, ?, 'team:shared', 'agent_old', 'user_statement', 'private note', ?, 'chat', 'note',
       'asserted', 'private', ?)`,
  ).run("obs_private", org.id, hex, now);
  database.close();

  const listed = run("ops-feedback", ["jobs", "list", "--db", "titen.db", "--status", "terminal_error"]);
  assert.equal(listed.exitCode, 0, listed.output);
  assert.match(listed.output, /job_failed/);
  const preview = run("ops-feedback", [
    "principal", "reassign", "--db", "titen.db", "--org-id", org.id,
    "--from", "agent_old", "--to", "agent_a", "--dry-run",
  ]);
  assert.equal(preview.exitCode, 0, preview.output);
  assert.match(preview.output, /"dry_run":true/);
  assert.match(preview.output, /"observations":1/);
  const acked = run("ops-feedback", ["jobs", "ack", "--db", "titen.db", "--status", "terminal_error", "--id", "job_failed"]);
  assert.equal(acked.exitCode, 0, acked.output);
  assert.match(acked.output, /"matched":1/);
  const again = run("ops-feedback", ["jobs", "list", "--db", "titen.db", "--status", "failed"]);
  assert.match(again.output, /job_failed/);
  const moved = run("ops-feedback", [
    "principal", "reassign", "--db", "titen.db", "--org-id", org.id,
    "--from", "agent_old", "--to", "agent_a",
  ]);
  assert.equal(moved.exitCode, 0, moved.output);
  const check = openDatabase(dbPath, { create: false, readonly: true });
  try {
    const actor = check.query("SELECT actor_id FROM observations WHERE id = 'obs_private'").get() as { actor_id: string };
    const ackedAt = check.query("SELECT acked_at FROM enrichment_jobs WHERE id = 'job_failed'").get() as { acked_at: string };
    assert.equal(actor.actor_id, "agent_a");
    assert.ok(ackedAt.acked_at);
  } finally {
    check.close();
  }
});