Merge remote-tracking branch 'origin/release/v1.46.0' into fix/end-medication-contributor-base

# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
#	src/__tests__/baselines/source-function-lines.json
This commit is contained in:
Marc-André Bombeck committed 2026-10-24 22:39:30 +02:00
commit a35ff5f5ea
47 files changed
+2606 -447

No files matched your search

@@ -67,6 +67,14 @@ vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn(),
}));
vi.mock("@/lib/ai/provider-health-ledger", () => ({
forgetProviderHealth: vi.fn(),
}));
vi.mock("@/lib/insights/status-negative-window", () => ({
clearStatusNegativeWindows: vi.fn(),
}));
import { GET, PATCH } from "../route";
import { prisma } from "@/lib/db";
import { resolveProviderAvailability } from "@/lib/ai/provider";
@@ -77,6 +85,8 @@ import { getAssistantFlags } from "@/lib/feature-flags";
import { providerCredentialPolicy } from "@/lib/sharing/provider-work-authority";
import { hasActiveConsentForSurface } from "@/lib/ai/consent-guard";
import { auditLog } from "@/lib/auth/audit";
import { forgetProviderHealth } from "@/lib/ai/provider-health-ledger";
import { clearStatusNegativeWindows } from "@/lib/insights/status-negative-window";
function patchRequest(body: unknown): Request {
return new Request("http://localhost/api/user/ai-provider", {
@@ -327,6 +337,26 @@ describe("PATCH /api/user/ai-provider — the gateway's own columns (#470)", ()
);
});
it("starts background work afresh after a change to what answers (#1279)", async () => {
// A provider benched for background work and notes backing off would
// otherwise sit out a pause the old setup earned.
vi.mocked(isPublicUrl).mockReturnValue(true);
const res = await patch(
patchRequest({
provider: "OPENAI_COMPATIBLE",
compatModel: "gpt-oss-120b",
}),
);
expect(res.status).toBe(200);
expect(vi.mocked(forgetProviderHealth).mock.calls.map((c) => c[0])).toEqual(
expect.arrayContaining([
{ userId: "u-1", providerType: "openai-compatible" },
{ userId: "u-1", providerType: "local" },
]),
);
expect(clearStatusNegativeWindows).toHaveBeenCalledWith("u-1");
});
it("refuses a private base URL the operator has not allowlisted", async () => {
vi.mocked(isPublicUrl).mockReturnValue(false);
vi.mocked(isLocalAiHostAllowed).mockReturnValue(false);
+31 -1
View File
@@ -23,9 +23,39 @@ import {
serializeProviderChain,
} from "@/lib/ai/provider-chain";
import { reasoningEffortFor } from "@/lib/ai/reasoning-effort";
import { forgetProviderHealth } from "@/lib/ai/provider-health-ledger";
import type { ProviderChainType } from "@/lib/ai/provider-chain";
import { clearStatusNegativeWindows } from "@/lib/insights/status-negative-window";
export const dynamic = "force-dynamic";
/** The chain entries this route configures (Codex signs in elsewhere). */
const USER_SETTINGS_PROVIDERS: readonly ProviderChainType[] = [
"openai",
"anthropic",
"local",
"openai-compatible",
];
/**
* Write the changed provider settings. #1279 — and start background work
* afresh: it skips a provider the health ledger benched, and a status note
* waits out its negative window, so without this a fixed setup would sit out
* a pause the old one earned.
*/
async function saveProviderSettings(
userId: string,
updates: Record<string, unknown>,
): Promise<void> {
await prisma.user.update({ where: { id: userId }, data: updates });
await Promise.all([
...USER_SETTINGS_PROVIDERS.map((providerType) =>
forgetProviderHealth({ userId, providerType }),
),
clearStatusNegativeWindows(userId),
]);
}
export const GET = apiHandler(async () => {
const { user } = await requireAuth();
annotate({ action: { name: "user.ai-provider.get" } });
@@ -389,7 +419,7 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
: null;
if (Object.keys(updates).length > 0) {
await prisma.user.update({ where: { id: user.id }, data: updates });
await saveProviderSettings(user.id, updates);
}
for (const [slot, value, column] of keySlots) {