mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
fix(eve): clear stale model setup warning (#3523)
Signed-off-by: Casey Gowrie <ctgowrie@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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({
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user