Files
jiang 6a49d2209e feat(agent): unify MCP tools and adopt Responses delivery
Move project and host queries into native MCP with host-owned OAuth/context,
while retaining memory, skills, actions and publication plugins. Add nearby-model
queries and explicit chat/task/learning model defaults.

Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped
session ownership, pending input edits and investigation communication.

Disable local-provider discovery: Server /health allowed native vLLM to probe
/v1/models on port 8000. A native before/after regression prevents recurrence.

Validation: 170 focused Bun tests; service/plugin typechecks; native discovery,
provider-discovery and input-inbox smoke checks; no paid model inference.
2026-10-19 18:46:58 +08:00

77 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { CredentialRefreshCoordinator, runWithCredentialRefresh, bindSessionCredential, credentialRefreshCoordinator, refreshSessionCredential, releaseSessionCredential, requestWithSessionCredential } from "../../src/product/credential-refresh.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
describe("credential renewal", () => {
it("requires a project before personal chats can call business APIs", async () => {
const sessionId = "personal-business-access";
let calls = 0;
setRuntimeSessionContext({ actorKey: "actor-personal", projectKey: "personal-actor-personal", sessionId,
clientSessionId: sessionId, traceId: "test" });
try {
await expect(requestWithSessionCredential(sessionId, async () => { calls++; return Response.json({}); })).rejects.toMatchObject({ status: 403, code: "PROJECT_CONTEXT_REQUIRED" });
expect(calls).toBe(0);
} finally { removeRuntimeSessionContext(sessionId); }
});
it("rejects stale replies and stops repeated requests when the renewed credential is still rejected", async () => {
const session = "rejected-credential-session";
bindSessionCredential(session, { authorization: "Bearer OLD", projectId: "p" });
let calls = 0;
let failures = 0;
const unsubscribe = credentialRefreshCoordinator.subscribe(session, (event) => {
if (event.type === "credential_refresh_required") {
expect(refreshSessionCredential(session, { authorization: "Bearer NEW", projectId: "other" }, event.requestId)).toBeFalse();
expect(refreshSessionCredential(session, { authorization: "Bearer NEW", projectId: "p" }, "stale")).toBeFalse();
refreshSessionCredential(session, { authorization: "Bearer NEW", projectId: "p" }, event.requestId);
} else if (event.type === "credential_refresh_failed") failures += 1;
});
try {
expect((await requestWithSessionCredential(session, async () => { calls += 1; return new Response(null, { status: 401 }); })).status).toBe(401);
await expect(requestWithSessionCredential(session, async () => { calls += 1; return new Response(null, { status: 200 }); })).rejects.toThrow("renewal failed");
expect(calls).toBe(2);
expect(failures).toBe(1);
} finally { unsubscribe(); releaseSessionCredential(session); }
});
it("coalesces concurrent 401 responses, resumes both calls, and retries only once", async () => {
const coordinator = new CredentialRefreshCoordinator(1000);
const old = { authorization: "Bearer TEST_OLD", projectId: "p" };
const renewed = { authorization: "Bearer TEST_NEW", projectId: "p" };
let requests = 0;
coordinator.subscribe("s", (event) => {
if (event.type === "credential_refresh_required") {
requests += 1;
queueMicrotask(() => coordinator.resolve("s", event.requestId, renewed));
}
});
const calls: string[] = [];
const execute = async (credential: typeof old) => {
calls.push(credential.authorization);
return { status: credential.authorization === old.authorization ? 401 : 200 };
};
expect((await Promise.all([runWithCredentialRefresh(coordinator, "s", old, execute), runWithCredentialRefresh(coordinator, "s", old, execute)])).map((result) => result.status)).toEqual([200, 200]);
expect(requests).toBe(1);
expect(calls).toHaveLength(4);
});
it("does not refresh 403 and fails when refresh is unavailable or cancelled", async () => {
const coordinator = new CredentialRefreshCoordinator(1000);
const credential = { authorization: "Bearer TEST", projectId: "p" };
expect((await runWithCredentialRefresh(coordinator, "s", credential, async () => ({ status: 403 }))).status).toBe(403);
await expect(runWithCredentialRefresh(coordinator, "s", credential, async () => ({ status: 401 }))).rejects.toThrow("unavailable");
coordinator.enableBackground("s");
const pending = coordinator.request("s", "access_token_expired");
coordinator.cancelSession("s");
await expect(pending).rejects.toThrow("cancelled");
});
it("renews before near-expiry execution and exposes the same pending request for reconnect", async () => {
const coordinator = new CredentialRefreshCoordinator(1000);
const old = { authorization: "Bearer OLD", projectId: "p", expiresAt: new Date(Date.now() + 1000).toISOString() };
coordinator.subscribe("s", (event) => {
if (event.type === "credential_refresh_required") {
expect(coordinator.getPendingEvent("s")?.requestId).toBe(event.requestId);
coordinator.resolve("s", event.requestId, { authorization: "Bearer NEW", projectId: "p" });
}
});
expect(await runWithCredentialRefresh(coordinator, "s", old, async (credential) => ({ status: 200, authorization: credential.authorization }))).toMatchObject({ authorization: "Bearer NEW" });
});
});