fix(eve): clear stale model setup warning (#3523)

Signed-off-by: Casey Gowrie <ctgowrie@gmail.com>
This commit is contained in:
Casey Gowrie
2026-09-18 14:59:21 -04:00
committed by GitHub
parent 61d98c3a15
commit cbb2206ca5
6 changed files with 100 additions and 36 deletions
@@ -0,0 +1,5 @@
---
"eve": patch
---
Clear stale model setup warnings when the local development server becomes ready, and avoid diagnosing unavailable startup information as missing model credentials.
+60 -1
View File
@@ -39,7 +39,7 @@ import { interruptedError } from "./errors.js";
import type { RemoteAuthFlow } from "./remote-auth.js";
import type { RemoteAuthCompletedMutation } from "./remote-auth-result.js";
import type { RemoteConnectionControllerOptions } from "./remote-connection.js";
import type { BootDetection, SetupIssue } from "./setup-issues.js";
import type { BootDetection, BootDetectionContext, SetupIssue } from "./setup-issues.js";
import type { SetupFlowRenderer } from "./setup-flow.js";
import { createFakeSetupFlowRenderer } from "./test/fake-setup-flow-renderer.js";
import type { VercelStatusSnapshot } from "./vercel-status.js";
@@ -4045,6 +4045,65 @@ describe("EveTUIRunner boot setup detection", () => {
expect(warnings).toEqual(["1 setup issue: AI Gateway credentials · /model"]);
});
it("clears a startup warning when a later agent-info probe reports connected OAuth", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => Response.json({ revision: "snapshot-a" })),
);
const connectedGatewayInfo: AgentInfoResult = {
...AGENT_INFO,
agent: {
...AGENT_INFO.agent,
model: {
id: "gpt-5",
routing: { kind: "gateway", target: "openai" },
endpoint: {
kind: "gateway",
connected: true,
credential: "oauth",
team: "alice",
},
},
},
};
const client = stubClient();
vi.spyOn(client, "info")
.mockRejectedValueOnce(new Error("server not ready"))
.mockResolvedValue(connectedGatewayInfo);
const warningCleared = createDeferred<void>();
const renderSetupWarning = vi.fn();
const clearSetupWarning = vi.fn(() => warningCleared.resolve());
const detect = vi.fn(({ info }: BootDetectionContext) =>
info === undefined
? [{ kind: "attention" as const, label: "connect a model", command: "/login" }]
: [],
);
const runner = new EveTUIRunner({
session: stubSession(),
client,
renderer: fakeRenderer({
renderSetupWarning,
clearSetupWarning,
readPrompt: vi.fn(async () => {
await warningCleared.promise;
return undefined;
}),
}),
serverUrl: "http://localhost:3000",
name: "Weather Agent",
appRoot: "/tmp/weather-agent",
bootDetections: [{ id: "test", detect }],
detectProjectIdentity: vi.fn(async () => undefined),
});
await runner.run();
expect(renderSetupWarning).toHaveBeenCalledWith("1 setup issue: connect a model · /login");
expect(clearSetupWarning).toHaveBeenCalled();
expect(client.info).toHaveBeenCalledTimes(2);
expect(detect.mock.calls.at(-1)?.[0].info).toBe(connectedGatewayInfo);
});
it("does not auto-open /model outside the prefilled onboarding launch", async () => {
const handle = vi.fn(async () => ({ message: "/model dismissed." }));
const runner = new EveTUIRunner({
+11
View File
@@ -1334,6 +1334,7 @@ export class EveTUIRunner {
let stopped = false;
let refreshing = false;
let inFlightRefresh: Promise<void> | undefined;
let agentInfoRefreshPending = false;
let lastChatGptAuthRefresh = 0;
const refresh = async () => {
if (stopped || refreshing) {
@@ -1345,6 +1346,16 @@ export class EveTUIRunner {
await runtimeArtifacts.refreshIdle({
onRuntimeArtifactsChanged: () => this.#handleRuntimeArtifactsChanged(),
});
if (
this.#appRoot !== undefined &&
this.#agentInfo === undefined &&
!agentInfoRefreshPending
) {
agentInfoRefreshPending = true;
void this.#refreshAgentInfo().finally(() => {
agentInfoRefreshPending = false;
});
}
const endpoint = this.#agentInfo?.agent.model.endpoint;
const shouldRefreshChatGptAuth =
endpoint?.kind === "chatgpt" &&
@@ -51,12 +51,9 @@ describe("BOOT_DETECTIONS against a real directory", () => {
expect(issues).toEqual([]);
});
it("diagnoses missing credentials (not the link) when the directory is linked", async () => {
it("defers model diagnosis when runtime info is unavailable", async () => {
const appRoot = await linkedAppRoot();
const issues = await detectSetupIssues({ appRoot, env: {} });
expect(issues).toEqual([
{ kind: "attention", label: "AI Gateway credentials missing", command: "/login" },
]);
expect(await detectSetupIssues({ appRoot, env: {} })).toEqual([]);
});
it("diagnoses a linked project with disconnected model access", async () => {
@@ -35,9 +35,8 @@ function infoWithRouting(
}
describe("BOOT_DETECTIONS", () => {
it("keeps an unavailable runtime diagnostic-only", async () => {
const issues = await detectSetupIssues(context());
expect(issues).toEqual([{ kind: "attention", label: "connect a model", command: "/login" }]);
it("defers model diagnosis while runtime info is unavailable", async () => {
expect(await detectSetupIssues(context())).toEqual([]);
});
it("diagnoses a disconnected gateway", async () => {
@@ -92,9 +91,7 @@ describe("BOOT_DETECTIONS", () => {
["AI_GATEWAY_API_KEY", "key"],
["VERCEL_OIDC_TOKEN", "token"],
])("does not infer AI Gateway routing from a local credential alone", async (key, value) => {
const issues = await detectSetupIssues(context({ env: { [key]: value } }));
expect(issues).toEqual([{ kind: "attention", label: "connect a model", command: "/login" }]);
expect(await detectSetupIssues(context({ env: { [key]: value } }))).toEqual([]);
});
it("stays quiet for an external-provider model — gateway linking/credentials don't apply", async () => {
@@ -122,14 +119,17 @@ describe("BOOT_DETECTIONS", () => {
},
);
it("stays quiet when the runtime resolved linked-project OIDC", async () => {
const info = infoWithRouting(
{ kind: "gateway", target: "openai" },
{ kind: "gateway", connected: true, credential: "oidc" },
);
it.each(["oidc", "oauth"] as const)(
"stays quiet when the runtime reports a connected %s endpoint",
async (credential) => {
const info = infoWithRouting(
{ kind: "gateway", target: "openai" },
{ kind: "gateway", connected: true, credential },
);
expect(await detectSetupIssues(context({ info }))).toEqual([]);
});
expect(await detectSetupIssues(context({ info }))).toEqual([]);
},
);
it("skips a throwing detection instead of failing the boot", async () => {
const info = infoWithRouting({ kind: "gateway", target: "openai" });
+9 -17
View File
@@ -149,25 +149,17 @@ const modelProvider: BootDetection = {
// Dynamic selectors can return any provider; their credentials cannot be
// diagnosed until a model is selected at runtime.
if (access.kind === "external" || access.kind === "dynamic") return [];
if (access.kind === "gateway") {
if (access.runtime.status === "connected") return [];
if (access.runtime.status === "disconnected") {
const linked = await pathExists(join(appRoot, ".vercel", "project.json"));
return [
{
kind: "attention",
label: linked ? "AI Gateway credentials missing" : "connect a model",
command: "/login",
},
];
}
}
if (access.kind !== "gateway" || access.runtime.status === "unknown") return [];
if (access.runtime.status === "connected") return [];
const linked = await pathExists(join(appRoot, ".vercel", "project.json"));
if (linked) {
return [{ kind: "attention", label: "AI Gateway credentials missing", command: "/login" }];
}
return [{ kind: "attention", label: "connect a model", command: "/login" }];
return [
{
kind: "attention",
label: linked ? "AI Gateway credentials missing" : "connect a model",
command: "/login",
},
];
},
};