Use app-server auth status for Codex readiness (#177)
* Use app-server auth status for Codex readiness * fix: reuse existing app server for auth checks
This commit is contained in:
parent
f17e7f8486
commit
62c351a7bf
@ -11,8 +11,8 @@ import {
|
||||
buildPersistentTaskThreadName,
|
||||
DEFAULT_CONTINUE_PROMPT,
|
||||
findLatestTaskThread,
|
||||
getCodexAuthStatus,
|
||||
getCodexAvailability,
|
||||
getCodexLoginStatus,
|
||||
getSessionRuntimeStatus,
|
||||
interruptAppServerTurn,
|
||||
parseStructuredOutput,
|
||||
@ -176,19 +176,19 @@ function firstMeaningfulLine(text, fallback) {
|
||||
return line ?? fallback;
|
||||
}
|
||||
|
||||
function buildSetupReport(cwd, actionsTaken = []) {
|
||||
async function buildSetupReport(cwd, actionsTaken = []) {
|
||||
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
||||
const nodeStatus = binaryAvailable("node", ["--version"], { cwd });
|
||||
const npmStatus = binaryAvailable("npm", ["--version"], { cwd });
|
||||
const codexStatus = getCodexAvailability(cwd);
|
||||
const authStatus = getCodexLoginStatus(cwd);
|
||||
const authStatus = await getCodexAuthStatus(cwd);
|
||||
const config = getConfig(workspaceRoot);
|
||||
|
||||
const nextSteps = [];
|
||||
if (!codexStatus.available) {
|
||||
nextSteps.push("Install Codex with `npm install -g @openai/codex`.");
|
||||
}
|
||||
if (codexStatus.available && !authStatus.loggedIn) {
|
||||
if (codexStatus.available && !authStatus.loggedIn && authStatus.requiresOpenaiAuth) {
|
||||
nextSteps.push("Run `!codex login`.");
|
||||
nextSteps.push("If browser login is blocked, retry with `!codex login --device-auth` or `!codex login --with-api-key`.");
|
||||
}
|
||||
@ -209,7 +209,7 @@ function buildSetupReport(cwd, actionsTaken = []) {
|
||||
};
|
||||
}
|
||||
|
||||
function handleSetup(argv) {
|
||||
async function handleSetup(argv) {
|
||||
const { options } = parseCommandInput(argv, {
|
||||
valueOptions: ["cwd"],
|
||||
booleanOptions: ["json", "enable-review-gate", "disable-review-gate"]
|
||||
@ -231,7 +231,7 @@ function handleSetup(argv) {
|
||||
actionsTaken.push(`Disabled the stop-time review gate for ${workspaceRoot}.`);
|
||||
}
|
||||
|
||||
const finalReport = buildSetupReport(cwd, actionsTaken);
|
||||
const finalReport = await buildSetupReport(cwd, actionsTaken);
|
||||
outputResult(options.json ? finalReport : renderSetupReport(finalReport), options.json);
|
||||
}
|
||||
|
||||
@ -245,14 +245,11 @@ function buildAdversarialReviewPrompt(context, focusText) {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureCodexReady(cwd) {
|
||||
const authStatus = getCodexLoginStatus(cwd);
|
||||
if (!authStatus.available) {
|
||||
function ensureCodexAvailable(cwd) {
|
||||
const availability = getCodexAvailability(cwd);
|
||||
if (!availability.available) {
|
||||
throw new Error("Codex CLI is not installed or is missing required runtime support. Install it with `npm install -g @openai/codex`, then rerun `/codex:setup`.");
|
||||
}
|
||||
if (!authStatus.loggedIn) {
|
||||
throw new Error("Codex CLI is not authenticated. Run `!codex login` and retry.");
|
||||
}
|
||||
}
|
||||
|
||||
function buildNativeReviewTarget(target) {
|
||||
@ -325,7 +322,7 @@ async function resolveLatestTrackedTaskThread(cwd, options = {}) {
|
||||
}
|
||||
|
||||
async function executeReviewRun(request) {
|
||||
ensureCodexReady(request.cwd);
|
||||
ensureCodexAvailable(request.cwd);
|
||||
ensureGitRepository(request.cwd);
|
||||
|
||||
const target = resolveReviewTarget(request.cwd, {
|
||||
@ -429,7 +426,7 @@ async function executeReviewRun(request) {
|
||||
|
||||
async function executeTaskRun(request) {
|
||||
const workspaceRoot = resolveWorkspaceRoot(request.cwd);
|
||||
ensureCodexReady(request.cwd);
|
||||
ensureCodexAvailable(request.cwd);
|
||||
|
||||
const taskMetadata = buildTaskRunMetadata({
|
||||
prompt: request.prompt,
|
||||
@ -728,7 +725,7 @@ async function handleTask(argv) {
|
||||
});
|
||||
|
||||
if (options.background) {
|
||||
ensureCodexReady(cwd);
|
||||
ensureCodexAvailable(cwd);
|
||||
requireTaskRequest(prompt, resumeLast);
|
||||
|
||||
const job = buildTaskJob(workspaceRoot, taskMetadata, write);
|
||||
@ -967,7 +964,7 @@ async function main() {
|
||||
|
||||
switch (subcommand) {
|
||||
case "setup":
|
||||
handleSetup(argv);
|
||||
await handleSetup(argv);
|
||||
break;
|
||||
case "review":
|
||||
await handleReview(argv);
|
||||
|
||||
@ -51,6 +51,7 @@ export interface CodexAppServerClientOptions {
|
||||
capabilities?: InitializeCapabilities;
|
||||
brokerEndpoint?: string;
|
||||
disableBroker?: boolean;
|
||||
reuseExistingBroker?: boolean;
|
||||
}
|
||||
|
||||
export interface AppServerMethodMap {
|
||||
|
||||
@ -13,7 +13,7 @@ import process from "node:process";
|
||||
import { spawn } from "node:child_process";
|
||||
import readline from "node:readline";
|
||||
import { parseBrokerEndpoint } from "./broker-endpoint.mjs";
|
||||
import { ensureBrokerSession } from "./broker-lifecycle.mjs";
|
||||
import { ensureBrokerSession, loadBrokerSession } from "./broker-lifecycle.mjs";
|
||||
import { terminateProcessTree } from "./process.mjs";
|
||||
|
||||
const PLUGIN_MANIFEST_URL = new URL("../../.claude-plugin/plugin.json", import.meta.url);
|
||||
@ -333,7 +333,10 @@ export class CodexAppServerClient {
|
||||
let brokerEndpoint = null;
|
||||
if (!options.disableBroker) {
|
||||
brokerEndpoint = options.brokerEndpoint ?? options.env?.[BROKER_ENDPOINT_ENV] ?? process.env[BROKER_ENDPOINT_ENV] ?? null;
|
||||
if (!brokerEndpoint) {
|
||||
if (!brokerEndpoint && options.reuseExistingBroker) {
|
||||
brokerEndpoint = loadBrokerSession(cwd)?.endpoint ?? null;
|
||||
}
|
||||
if (!brokerEndpoint && !options.reuseExistingBroker) {
|
||||
const brokerSession = await ensureBrokerSession(cwd, { env: options.env });
|
||||
brokerEndpoint = brokerSession?.endpoint ?? null;
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
import { readJsonFile } from "./fs.mjs";
|
||||
import { BROKER_BUSY_RPC_CODE, BROKER_ENDPOINT_ENV, CodexAppServerClient } from "./app-server.mjs";
|
||||
import { loadBrokerSession } from "./broker-lifecycle.mjs";
|
||||
import { binaryAvailable, runCommand } from "./process.mjs";
|
||||
import { binaryAvailable } from "./process.mjs";
|
||||
|
||||
const SERVICE_NAME = "claude_code_codex_plugin";
|
||||
const TASK_THREAD_PREFIX = "Codex Companion Task";
|
||||
@ -652,6 +652,134 @@ function buildResultStatus(turnState) {
|
||||
return turnState.finalTurn?.status === "completed" ? 0 : 1;
|
||||
}
|
||||
|
||||
const BUILTIN_PROVIDER_LABELS = new Map([
|
||||
["openai", "OpenAI"],
|
||||
["ollama", "Ollama"],
|
||||
["lmstudio", "LM Studio"]
|
||||
]);
|
||||
|
||||
function normalizeProviderId(value) {
|
||||
const providerId = typeof value === "string" ? value.trim() : "";
|
||||
return providerId || null;
|
||||
}
|
||||
|
||||
function formatProviderLabel(providerId, providerConfig = null) {
|
||||
const configuredName = typeof providerConfig?.name === "string" ? providerConfig.name.trim() : "";
|
||||
if (configuredName) {
|
||||
return configuredName;
|
||||
}
|
||||
if (!providerId) {
|
||||
return "The active provider";
|
||||
}
|
||||
return BUILTIN_PROVIDER_LABELS.get(providerId) ?? providerId;
|
||||
}
|
||||
|
||||
function buildAuthStatus(fields = {}) {
|
||||
return {
|
||||
available: true,
|
||||
loggedIn: false,
|
||||
detail: "not authenticated",
|
||||
source: "unknown",
|
||||
authMethod: null,
|
||||
verified: null,
|
||||
requiresOpenaiAuth: null,
|
||||
provider: null,
|
||||
...fields
|
||||
};
|
||||
}
|
||||
|
||||
function resolveProviderConfig(configResponse) {
|
||||
const config = configResponse?.config;
|
||||
if (!config || typeof config !== "object") {
|
||||
return {
|
||||
providerId: null,
|
||||
providerConfig: null
|
||||
};
|
||||
}
|
||||
|
||||
const providerId = normalizeProviderId(config.model_provider);
|
||||
const providers =
|
||||
config.model_providers && typeof config.model_providers === "object" && !Array.isArray(config.model_providers)
|
||||
? config.model_providers
|
||||
: null;
|
||||
const providerConfig =
|
||||
providerId && providers?.[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null;
|
||||
|
||||
return {
|
||||
providerId,
|
||||
providerConfig
|
||||
};
|
||||
}
|
||||
|
||||
function buildAppServerAuthStatus(accountResponse, configResponse) {
|
||||
const account = accountResponse?.account ?? null;
|
||||
const requiresOpenaiAuth =
|
||||
typeof accountResponse?.requiresOpenaiAuth === "boolean" ? accountResponse.requiresOpenaiAuth : null;
|
||||
const { providerId, providerConfig } = resolveProviderConfig(configResponse);
|
||||
const providerLabel = formatProviderLabel(providerId, providerConfig);
|
||||
|
||||
if (account?.type === "chatgpt") {
|
||||
const email = typeof account.email === "string" && account.email.trim() ? account.email.trim() : null;
|
||||
return buildAuthStatus({
|
||||
loggedIn: true,
|
||||
detail: email ? `ChatGPT login active for ${email}` : "ChatGPT login active",
|
||||
source: "app-server",
|
||||
authMethod: "chatgpt",
|
||||
verified: true,
|
||||
requiresOpenaiAuth,
|
||||
provider: providerId
|
||||
});
|
||||
}
|
||||
|
||||
if (account?.type === "apiKey") {
|
||||
return buildAuthStatus({
|
||||
loggedIn: true,
|
||||
detail: "API key configured (unverified)",
|
||||
source: "app-server",
|
||||
authMethod: "apiKey",
|
||||
verified: false,
|
||||
requiresOpenaiAuth,
|
||||
provider: providerId
|
||||
});
|
||||
}
|
||||
|
||||
if (requiresOpenaiAuth === false) {
|
||||
return buildAuthStatus({
|
||||
loggedIn: true,
|
||||
detail: `${providerLabel} is configured and does not require OpenAI authentication`,
|
||||
source: "app-server",
|
||||
requiresOpenaiAuth,
|
||||
provider: providerId
|
||||
});
|
||||
}
|
||||
|
||||
return buildAuthStatus({
|
||||
loggedIn: false,
|
||||
detail: `${providerLabel} requires OpenAI authentication`,
|
||||
source: "app-server",
|
||||
requiresOpenaiAuth,
|
||||
provider: providerId
|
||||
});
|
||||
}
|
||||
|
||||
async function getCodexAuthStatusFromClient(client, cwd) {
|
||||
try {
|
||||
const accountResponse = await client.request("account/read", { refreshToken: false });
|
||||
const configResponse = await client.request("config/read", {
|
||||
includeLayers: false,
|
||||
cwd
|
||||
});
|
||||
|
||||
return buildAppServerAuthStatus(accountResponse, configResponse);
|
||||
} catch (error) {
|
||||
return buildAuthStatus({
|
||||
loggedIn: false,
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
source: "app-server"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getCodexAvailability(cwd) {
|
||||
const versionStatus = binaryAvailable("codex", ["--version"], { cwd });
|
||||
if (!versionStatus.available) {
|
||||
@ -691,38 +819,39 @@ export function getSessionRuntimeStatus(env = process.env, cwd = process.cwd())
|
||||
};
|
||||
}
|
||||
|
||||
export function getCodexLoginStatus(cwd) {
|
||||
export async function getCodexAuthStatus(cwd, options = {}) {
|
||||
const availability = getCodexAvailability(cwd);
|
||||
if (!availability.available) {
|
||||
return {
|
||||
available: false,
|
||||
loggedIn: false,
|
||||
detail: availability.detail
|
||||
detail: availability.detail,
|
||||
source: "availability",
|
||||
authMethod: null,
|
||||
verified: null,
|
||||
requiresOpenaiAuth: null,
|
||||
provider: null
|
||||
};
|
||||
}
|
||||
|
||||
const result = runCommand("codex", ["login", "status"], { cwd });
|
||||
if (result.error) {
|
||||
return {
|
||||
available: true,
|
||||
let client = null;
|
||||
try {
|
||||
client = await CodexAppServerClient.connect(cwd, {
|
||||
env: options.env,
|
||||
reuseExistingBroker: true
|
||||
});
|
||||
return await getCodexAuthStatusFromClient(client, cwd);
|
||||
} catch (error) {
|
||||
return buildAuthStatus({
|
||||
loggedIn: false,
|
||||
detail: result.error.message
|
||||
};
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
source: "app-server"
|
||||
});
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
if (result.status === 0) {
|
||||
return {
|
||||
available: true,
|
||||
loggedIn: true,
|
||||
detail: result.stdout.trim() || "authenticated"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
loggedIn: false,
|
||||
detail: result.stderr.trim() || result.stdout.trim() || "not authenticated"
|
||||
};
|
||||
}
|
||||
|
||||
export async function interruptAppServerTurn(cwd, { threadId, turnId }) {
|
||||
@ -745,12 +874,9 @@ export async function interruptAppServerTurn(cwd, { threadId, turnId }) {
|
||||
};
|
||||
}
|
||||
|
||||
const brokerEndpoint = process.env[BROKER_ENDPOINT_ENV] ?? loadBrokerSession(cwd)?.endpoint ?? null;
|
||||
let client = null;
|
||||
try {
|
||||
client = brokerEndpoint
|
||||
? await CodexAppServerClient.connect(cwd, { brokerEndpoint })
|
||||
: await CodexAppServerClient.connect(cwd, { disableBroker: true });
|
||||
client = await CodexAppServerClient.connect(cwd, { reuseExistingBroker: true });
|
||||
await client.request("turn/interrupt", { threadId, turnId });
|
||||
return {
|
||||
attempted: true,
|
||||
|
||||
@ -6,7 +6,7 @@ import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { getCodexLoginStatus } from "./lib/codex.mjs";
|
||||
import { getCodexAvailability } from "./lib/codex.mjs";
|
||||
import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs";
|
||||
import { getConfig, listJobs } from "./lib/state.mjs";
|
||||
import { sortJobsNewestFirst } from "./lib/job-control.mjs";
|
||||
@ -57,13 +57,13 @@ function buildStopReviewPrompt(input = {}) {
|
||||
}
|
||||
|
||||
function buildSetupNote(cwd) {
|
||||
const authStatus = getCodexLoginStatus(cwd);
|
||||
if (authStatus.available && authStatus.loggedIn) {
|
||||
const availability = getCodexAvailability(cwd);
|
||||
if (availability.available) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detail = authStatus.detail ? ` ${authStatus.detail}.` : "";
|
||||
return `Codex is not set up for the review gate.${detail} Run /codex:setup and, if needed, !codex login.`;
|
||||
const detail = availability.detail ? ` ${availability.detail}.` : "";
|
||||
return `Codex is not set up for the review gate.${detail} Run /codex:setup.`;
|
||||
}
|
||||
|
||||
function parseStopReviewOutput(rawOutput) {
|
||||
@ -175,4 +175,10 @@ function main() {
|
||||
logNote(runningTaskNote);
|
||||
}
|
||||
|
||||
main();
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
@ -63,6 +63,54 @@ function buildTurn(id, status = "inProgress", error = null) {
|
||||
return { id, status, items: [], error };
|
||||
}
|
||||
|
||||
function buildAccountReadResult() {
|
||||
switch (BEHAVIOR) {
|
||||
case "logged-out":
|
||||
case "refreshable-auth":
|
||||
case "auth-run-fails":
|
||||
return { account: null, requiresOpenaiAuth: true };
|
||||
case "provider-no-auth":
|
||||
case "env-key-provider":
|
||||
return { account: null, requiresOpenaiAuth: false };
|
||||
case "api-key-account-only":
|
||||
return { account: { type: "apiKey" }, requiresOpenaiAuth: true };
|
||||
default:
|
||||
return {
|
||||
account: { type: "chatgpt", email: "test@example.com", planType: "plus" },
|
||||
requiresOpenaiAuth: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildConfigReadResult() {
|
||||
switch (BEHAVIOR) {
|
||||
case "provider-no-auth":
|
||||
return {
|
||||
config: { model_provider: "ollama" },
|
||||
origins: {}
|
||||
};
|
||||
case "env-key-provider":
|
||||
return {
|
||||
config: {
|
||||
model_provider: "openai-custom",
|
||||
model_providers: {
|
||||
"openai-custom": {
|
||||
name: "OpenAI custom",
|
||||
env_key: "OPENAI_API_KEY",
|
||||
requires_openai_auth: false
|
||||
}
|
||||
}
|
||||
},
|
||||
origins: {}
|
||||
};
|
||||
default:
|
||||
return {
|
||||
config: { model_provider: "openai" },
|
||||
origins: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function send(message) {
|
||||
process.stdout.write(JSON.stringify(message) + "\\n");
|
||||
}
|
||||
@ -193,7 +241,7 @@ if (args[0] === "app-server" && args[1] === "--help") {
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === "login" && args[1] === "status") {
|
||||
if (BEHAVIOR === "logged-out") {
|
||||
if (BEHAVIOR === "logged-out" || BEHAVIOR === "refreshable-auth" || BEHAVIOR === "auth-run-fails" || BEHAVIOR === "provider-no-auth" || BEHAVIOR === "env-key-provider" || BEHAVIOR === "api-key-account-only") {
|
||||
console.error("not authenticated");
|
||||
process.exit(1);
|
||||
}
|
||||
@ -230,7 +278,21 @@ rl.on("line", (line) => {
|
||||
case "initialized":
|
||||
break;
|
||||
|
||||
case "account/read":
|
||||
send({ id: message.id, result: buildAccountReadResult() });
|
||||
break;
|
||||
|
||||
case "config/read":
|
||||
if (BEHAVIOR === "config-read-fails") {
|
||||
throw new Error("config/read failed for cwd");
|
||||
}
|
||||
send({ id: message.id, result: buildConfigReadResult() });
|
||||
break;
|
||||
|
||||
case "thread/start": {
|
||||
if (BEHAVIOR === "auth-run-fails") {
|
||||
throw new Error("authentication expired; run codex login");
|
||||
}
|
||||
if (requiresExperimental("persistExtendedHistory", message, state) || requiresExperimental("persistFullHistory", message, state)) {
|
||||
throw new Error("thread/start.persistFullHistory requires experimentalApi capability");
|
||||
}
|
||||
|
||||
@ -65,6 +65,77 @@ test("setup is ready without npm when Codex is already installed and authenticat
|
||||
assert.equal(payload.auth.loggedIn, true);
|
||||
});
|
||||
|
||||
test("setup trusts app-server API key auth even when login status alone would fail", () => {
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "api-key-account-only");
|
||||
|
||||
const result = run("node", [SCRIPT, "setup", "--json"], {
|
||||
cwd: ROOT,
|
||||
env: buildEnv(binDir)
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const payload = JSON.parse(result.stdout);
|
||||
assert.equal(payload.ready, true);
|
||||
assert.equal(payload.auth.loggedIn, true);
|
||||
assert.equal(payload.auth.authMethod, "apiKey");
|
||||
assert.equal(payload.auth.source, "app-server");
|
||||
assert.match(payload.auth.detail, /API key configured \(unverified\)/);
|
||||
});
|
||||
|
||||
test("setup is ready when the active provider does not require OpenAI login", () => {
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "provider-no-auth");
|
||||
|
||||
const result = run("node", [SCRIPT, "setup", "--json"], {
|
||||
cwd: ROOT,
|
||||
env: buildEnv(binDir)
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const payload = JSON.parse(result.stdout);
|
||||
assert.equal(payload.ready, true);
|
||||
assert.equal(payload.auth.loggedIn, true);
|
||||
assert.equal(payload.auth.authMethod, null);
|
||||
assert.equal(payload.auth.source, "app-server");
|
||||
assert.match(payload.auth.detail, /configured and does not require OpenAI authentication/i);
|
||||
});
|
||||
|
||||
test("setup treats custom providers with app-server-ready config as ready", () => {
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "env-key-provider");
|
||||
|
||||
const result = run("node", [SCRIPT, "setup", "--json"], {
|
||||
cwd: ROOT,
|
||||
env: buildEnv(binDir)
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const payload = JSON.parse(result.stdout);
|
||||
assert.equal(payload.ready, true);
|
||||
assert.equal(payload.auth.loggedIn, true);
|
||||
assert.equal(payload.auth.authMethod, null);
|
||||
assert.equal(payload.auth.source, "app-server");
|
||||
assert.match(payload.auth.detail, /configured and does not require OpenAI authentication/i);
|
||||
});
|
||||
|
||||
test("setup reports not ready when app-server config read fails", () => {
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "config-read-fails");
|
||||
|
||||
const result = run("node", [SCRIPT, "setup", "--json"], {
|
||||
cwd: ROOT,
|
||||
env: buildEnv(binDir)
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const payload = JSON.parse(result.stdout);
|
||||
assert.equal(payload.ready, false);
|
||||
assert.equal(payload.auth.loggedIn, false);
|
||||
assert.equal(payload.auth.source, "app-server");
|
||||
assert.match(payload.auth.detail, /config\/read failed for cwd/);
|
||||
});
|
||||
|
||||
test("review renders a no-findings result from app-server review/start", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
@ -86,6 +157,60 @@ test("review renders a no-findings result from app-server review/start", () => {
|
||||
assert.match(result.stdout, /No material issues found/);
|
||||
});
|
||||
|
||||
test("task runs when the active provider does not require OpenAI login", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "provider-no-auth");
|
||||
initGitRepo(repo);
|
||||
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
|
||||
run("git", ["add", "README.md"], { cwd: repo });
|
||||
run("git", ["commit", "-m", "init"], { cwd: repo });
|
||||
|
||||
const result = run("node", [SCRIPT, "task", "check auth preflight"], {
|
||||
cwd: repo,
|
||||
env: buildEnv(binDir)
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /Handled the requested task/);
|
||||
});
|
||||
|
||||
test("task runs without auth preflight so Codex can refresh an expired session", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "refreshable-auth");
|
||||
initGitRepo(repo);
|
||||
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
|
||||
run("git", ["add", "README.md"], { cwd: repo });
|
||||
run("git", ["commit", "-m", "init"], { cwd: repo });
|
||||
|
||||
const result = run("node", [SCRIPT, "task", "check refreshable auth"], {
|
||||
cwd: repo,
|
||||
env: buildEnv(binDir)
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /Handled the requested task/);
|
||||
});
|
||||
|
||||
test("task reports the actual Codex auth error when the run is rejected", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "auth-run-fails");
|
||||
initGitRepo(repo);
|
||||
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
|
||||
run("git", ["add", "README.md"], { cwd: repo });
|
||||
run("git", ["commit", "-m", "init"], { cwd: repo });
|
||||
|
||||
const result = run("node", [SCRIPT, "task", "check failed auth"], {
|
||||
cwd: repo,
|
||||
env: buildEnv(binDir)
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /authentication expired; run codex login/);
|
||||
});
|
||||
|
||||
test("review accepts the quoted raw argument style for built-in base-branch review", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
@ -1598,10 +1723,10 @@ test("stop hook does not block when Codex is unavailable even if the review gate
|
||||
assert.match(allowed.stderr, /Run \/codex:setup/i);
|
||||
});
|
||||
|
||||
test("stop hook does not block when Codex is not authenticated even if the review gate is enabled", () => {
|
||||
test("stop hook runs the actual task when auth status looks stale", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
installFakeCodex(binDir, "logged-out");
|
||||
installFakeCodex(binDir, "refreshable-auth");
|
||||
initGitRepo(repo);
|
||||
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
|
||||
run("git", ["add", "README.md"], { cwd: repo });
|
||||
@ -1620,10 +1745,10 @@ test("stop hook does not block when Codex is not authenticated even if the revie
|
||||
});
|
||||
|
||||
assert.equal(allowed.status, 0, allowed.stderr);
|
||||
assert.equal(allowed.stdout.trim(), "");
|
||||
assert.match(allowed.stderr, /Codex is not set up for the review gate/i);
|
||||
assert.match(allowed.stderr, /not authenticated/i);
|
||||
assert.match(allowed.stderr, /!codex login/i);
|
||||
assert.doesNotMatch(allowed.stderr, /Codex is not set up for the review gate/i);
|
||||
const payload = JSON.parse(allowed.stdout);
|
||||
assert.equal(payload.decision, "block");
|
||||
assert.match(payload.reason, /Missing empty-state guard/i);
|
||||
});
|
||||
|
||||
test("commands lazily start and reuse one shared app-server after first use", async () => {
|
||||
@ -1671,6 +1796,51 @@ test("commands lazily start and reuse one shared app-server after first use", as
|
||||
assert.equal(cleanup.status, 0, cleanup.stderr);
|
||||
});
|
||||
|
||||
test("setup reuses an existing shared app-server without starting another one", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
const fakeStatePath = path.join(binDir, "fake-codex-state.json");
|
||||
|
||||
installFakeCodex(binDir);
|
||||
initGitRepo(repo);
|
||||
fs.writeFileSync(path.join(repo, "README.md"), "hello\n");
|
||||
run("git", ["add", "README.md"], { cwd: repo });
|
||||
run("git", ["commit", "-m", "init"], { cwd: repo });
|
||||
fs.writeFileSync(path.join(repo, "README.md"), "hello again\n");
|
||||
|
||||
const env = buildEnv(binDir);
|
||||
|
||||
const review = run("node", [SCRIPT, "review"], {
|
||||
cwd: repo,
|
||||
env
|
||||
});
|
||||
assert.equal(review.status, 0, review.stderr);
|
||||
|
||||
const brokerSession = loadBrokerSession(repo);
|
||||
if (!brokerSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setup = run("node", [SCRIPT, "setup", "--json"], {
|
||||
cwd: repo,
|
||||
env
|
||||
});
|
||||
assert.equal(setup.status, 0, setup.stderr);
|
||||
|
||||
const fakeState = JSON.parse(fs.readFileSync(fakeStatePath, "utf8"));
|
||||
assert.equal(fakeState.appServerStarts, 1);
|
||||
|
||||
const cleanup = run("node", [SESSION_HOOK, "SessionEnd"], {
|
||||
cwd: repo,
|
||||
env,
|
||||
input: JSON.stringify({
|
||||
hook_event_name: "SessionEnd",
|
||||
cwd: repo
|
||||
})
|
||||
});
|
||||
assert.equal(cleanup.status, 0, cleanup.stderr);
|
||||
});
|
||||
|
||||
test("status reports shared session runtime when a lazy broker is active", () => {
|
||||
const repo = makeTempDir();
|
||||
const binDir = makeTempDir();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user