mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(ai): close the notes' windows on a fresh Codex sign-in
A save of a person's provider settings and of the operator's admin AI settings forget the health ledger's record and close the status notes' negative windows. The two Codex sign-ins, the operator's central one and a person's own, forgot the ledger record only, so a fixed connection still had every note wait out the window the refused one earned, up to two hours. The central sign-in now closes every record's windows, since every account may fall back on that connection, and a person's own sign-in closes theirs. Proven against the real rows in the Codex sign-in integration suite. Refs #1279
This commit is contained in:
1 parent
8b47d033e7
commit
a172e377c1
3 files changed
+121
-3
No files matched your search
@@ -9,6 +9,7 @@ import { auditLog } from "@/lib/auth/audit";
|
||||
import { getClientIp, apiSuccess, apiError } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { forgetProviderHealth } from "@/lib/ai/provider-health-ledger";
|
||||
import { clearStatusNegativeWindows } from "@/lib/insights/status-negative-window";
|
||||
|
||||
/**
|
||||
* Operator-shared central Codex — device-code poll (admin only).
|
||||
@@ -94,7 +95,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
cookieStore.delete("central_codex_device");
|
||||
// A new central sign-in: every account's ledger rows describe the refused
|
||||
// one, and the central connection is one credential shared by all of them.
|
||||
await forgetProviderHealth({ providerType: "admin-codex" });
|
||||
// #1279 — and every record's note windows close, as on the admin AI
|
||||
// settings save, so no note sits out a window the broken one earned.
|
||||
await Promise.all([
|
||||
forgetProviderHealth({ providerType: "admin-codex" }),
|
||||
clearStatusNegativeWindows(),
|
||||
]);
|
||||
|
||||
await auditLog("admin.central-codex.connected", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getClientIp, apiSuccess, apiError } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { INSIGHTS_CACHE_CLEARED } from "@/lib/insights/cached-text-cipher";
|
||||
import { forgetProviderHealth } from "@/lib/ai/provider-health-ledger";
|
||||
import { clearStatusNegativeWindows } from "@/lib/insights/status-negative-window";
|
||||
|
||||
/**
|
||||
* v1.4.7.1: Device-code flow polling endpoint.
|
||||
@@ -90,8 +91,12 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
cookieStore.delete("codex_device");
|
||||
// A new sign-in: whatever the ledger held against the old one no longer
|
||||
// describes this credential.
|
||||
await forgetProviderHealth({ userId: user.id, providerType: "codex" });
|
||||
// describes this credential, and (#1279) the person's notes stop waiting
|
||||
// out windows it earned, as on a save of their provider settings.
|
||||
await Promise.all([
|
||||
forgetProviderHealth({ userId: user.id, providerType: "codex" }),
|
||||
clearStatusNegativeWindows(user.id),
|
||||
]);
|
||||
|
||||
await auditLog("codex.oauth.connected", {
|
||||
userId: user.id,
|
||||
|
||||
@@ -262,6 +262,113 @@ describe("a refused Codex sign-in", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* #1279 — a fresh sign-in also closes the status notes' negative windows,
|
||||
* as a save of the person's provider settings and of the operator's admin AI
|
||||
* settings do. A window grows with the failures in a row (up to two hours),
|
||||
* so without it a fixed connection still had every note wait out the window
|
||||
* the refused one earned.
|
||||
*/
|
||||
describe("a fresh Codex sign-in and the notes' windows", () => {
|
||||
async function openWindow(userId: string) {
|
||||
await prisma.insightStatusCache.create({
|
||||
data: {
|
||||
userId,
|
||||
metric: "weight",
|
||||
locale: "en",
|
||||
dateKey: "2026-06-15",
|
||||
retryAt: new Date(Date.now() + HOUR_MS),
|
||||
negativeReason: "error",
|
||||
failureStreak: 4,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function windowOf(userId: string) {
|
||||
return prisma.insightStatusCache.findFirstOrThrow({
|
||||
where: { userId },
|
||||
select: { retryAt: true, failureStreak: true },
|
||||
});
|
||||
}
|
||||
|
||||
function signedIn() {
|
||||
vi.mocked(pollDeviceCode).mockResolvedValue({
|
||||
status: "connected",
|
||||
creds: creds("new", 10 * HOUR_MS),
|
||||
} as Awaited<ReturnType<typeof pollDeviceCode>>);
|
||||
}
|
||||
|
||||
it("closes the person's windows on their own sign-in", async () => {
|
||||
const user = await seedCodexUser();
|
||||
await openWindow(user.id);
|
||||
signedIn();
|
||||
cookieJar.set(
|
||||
"codex_device",
|
||||
encrypt(JSON.stringify({ deviceAuthId: "device", userCode: "CODE" })),
|
||||
);
|
||||
const { POST } = await import("@/app/api/auth/codex/device-poll/route");
|
||||
const res = await POST(
|
||||
new NextRequest("http://localhost/api/auth/codex/device-poll", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await windowOf(user.id)).toEqual({
|
||||
retryAt: null,
|
||||
failureStreak: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("closes every account's windows on the operator's central sign-in", async () => {
|
||||
const admin = await prisma.user.create({
|
||||
data: {
|
||||
username: "central-admin",
|
||||
email: "central-admin@example.test",
|
||||
role: "ADMIN",
|
||||
},
|
||||
});
|
||||
const session = await prisma.session.create({
|
||||
data: { userId: admin.id, expiresAt: new Date(Date.now() + 60_000) },
|
||||
});
|
||||
cookieJar.set("healthlog_session", session.id);
|
||||
const member = await prisma.user.create({
|
||||
data: {
|
||||
username: "central-member",
|
||||
email: "central-member@example.test",
|
||||
useCentralCodex: true,
|
||||
},
|
||||
});
|
||||
await openWindow(member.id);
|
||||
await postgresProviderHealthLedger.recordFailure(
|
||||
member.id,
|
||||
"admin-codex",
|
||||
401,
|
||||
);
|
||||
signedIn();
|
||||
cookieJar.set(
|
||||
"central_codex_device",
|
||||
encrypt(JSON.stringify({ deviceAuthId: "device", userCode: "CODE" })),
|
||||
);
|
||||
const { POST } =
|
||||
await import("@/app/api/admin/central-codex/device-poll/route");
|
||||
const res = await POST(
|
||||
new NextRequest("http://localhost/api/admin/central-codex/device-poll", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await windowOf(member.id)).toEqual({
|
||||
retryAt: null,
|
||||
failureStreak: 0,
|
||||
});
|
||||
expect(
|
||||
(await postgresProviderHealthLedger.getSkipHints(member.id)).has(
|
||||
"admin-codex",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("two refreshes of one Codex token", () => {
|
||||
async function codexStatus(userId: string) {
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user