chore: resolve merge conflicts
This commit is contained in:
@@ -5372,6 +5372,75 @@ describe("PUT /settings", () => {
|
||||
expect(res.status).toBe(500);
|
||||
expect(res.body.error).toContain("Write failed");
|
||||
});
|
||||
|
||||
it("updates planning and validator model settings via store.updateSettings", async () => {
|
||||
const updatedSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4-5",
|
||||
validatorProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
};
|
||||
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
"PUT",
|
||||
"/api/settings",
|
||||
JSON.stringify({
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4-5",
|
||||
validatorProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateSettings).toHaveBeenCalledWith({
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4-5",
|
||||
validatorProvider: "openai",
|
||||
validatorModelId: "gpt-4o",
|
||||
});
|
||||
});
|
||||
|
||||
it("persists planning/validator settings and returns them via GET /settings", async () => {
|
||||
// First, update the settings
|
||||
const updatedSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-opus-4",
|
||||
validatorProvider: "openai",
|
||||
validatorModelId: "gpt-4-turbo",
|
||||
};
|
||||
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
|
||||
|
||||
const updateRes = await REQUEST(
|
||||
buildApp(),
|
||||
"PUT",
|
||||
"/api/settings",
|
||||
JSON.stringify({
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-opus-4",
|
||||
validatorProvider: "openai",
|
||||
validatorModelId: "gpt-4-turbo",
|
||||
}),
|
||||
{ "Content-Type": "application/json" },
|
||||
);
|
||||
|
||||
expect(updateRes.status).toBe(200);
|
||||
|
||||
// Then, verify GET /settings returns the persisted values
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
|
||||
const getRes = await GET(buildApp(), "/api/settings");
|
||||
|
||||
expect(getRes.status).toBe(200);
|
||||
expect(getRes.body.planningProvider).toBe("anthropic");
|
||||
expect(getRes.body.planningModelId).toBe("claude-opus-4");
|
||||
expect(getRes.body.validatorProvider).toBe("openai");
|
||||
expect(getRes.body.validatorModelId).toBe("gpt-4-turbo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /settings/global", () => {
|
||||
|
||||
@@ -6,10 +6,13 @@ import {
|
||||
calculatePace,
|
||||
_setSleepFn,
|
||||
_resetSleepFn,
|
||||
<<<<<<< ours
|
||||
_stripClaudeAnsi,
|
||||
_parseClaudePercentLine,
|
||||
_parseClaudeResetLine,
|
||||
_parseClaudeResetText,
|
||||
=======
|
||||
>>>>>>> theirs
|
||||
} from "./usage.js";
|
||||
|
||||
// Mock the https module
|
||||
@@ -401,6 +404,7 @@ describe("usage", () => {
|
||||
expect(claude.error).toContain("Auth expired");
|
||||
});
|
||||
|
||||
<<<<<<< ours
|
||||
it("handles 403 auth error", async () => {
|
||||
setupClaudeMocks({
|
||||
credFileContent: {
|
||||
@@ -417,6 +421,104 @@ describe("usage", () => {
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from('{"error": "forbidden"}'));
|
||||
if (event === "end") handler();
|
||||
=======
|
||||
it("does not send anthropic-beta header in requests", async () => {
|
||||
const mockResponse = {
|
||||
five_hour: { utilization: 10.0 },
|
||||
};
|
||||
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("claude")) {
|
||||
return JSON.stringify({
|
||||
accessToken: "test-token",
|
||||
scopes: ["user:profile"],
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
let capturedHeaders: Record<string, string> = {};
|
||||
const mockReq = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
capturedHeaders = options.headers || {};
|
||||
const mockRes = {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
handler(Buffer.from(JSON.stringify(mockResponse)));
|
||||
}
|
||||
if (event === "end") {
|
||||
handler();
|
||||
}
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
|
||||
await fetchAllProviderUsage();
|
||||
|
||||
// Verify no anthropic-beta header is sent
|
||||
expect(capturedHeaders).not.toHaveProperty("anthropic-beta");
|
||||
});
|
||||
|
||||
it("retries on 429 and succeeds after transient rate limit", async () => {
|
||||
const noopSleep = vi.fn().mockResolvedValue(undefined);
|
||||
_setSleepFn(noopSleep);
|
||||
|
||||
const mockResponse = {
|
||||
five_hour: {
|
||||
utilization: 20.0,
|
||||
resets_at: new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("claude")) {
|
||||
return JSON.stringify({
|
||||
accessToken: "test-token",
|
||||
scopes: ["user:profile"],
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
let callCount = 0;
|
||||
const mockReq = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
callCount++;
|
||||
const is429 = callCount <= 2; // First 2 calls return 429, third succeeds
|
||||
const mockRes = {
|
||||
statusCode: is429 ? 429 : 200,
|
||||
headers: is429 ? { "retry-after": "1" } : {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
const body = is429
|
||||
? '{"error":"rate_limited"}'
|
||||
: JSON.stringify(mockResponse);
|
||||
handler(Buffer.from(body));
|
||||
}
|
||||
if (event === "end") {
|
||||
handler();
|
||||
}
|
||||
>>>>>>> theirs
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
@@ -426,6 +528,7 @@ describe("usage", () => {
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const claude = providers.find((p) => p.name === "Claude")!;
|
||||
|
||||
<<<<<<< ours
|
||||
expect(claude.status).toBe("error");
|
||||
expect(claude.error).toContain("Auth expired");
|
||||
});
|
||||
@@ -446,6 +549,53 @@ describe("usage", () => {
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from("not valid json {{{"));
|
||||
if (event === "end") handler();
|
||||
=======
|
||||
expect(claude.status).toBe("ok");
|
||||
expect(claude.windows).toHaveLength(1);
|
||||
expect(claude.windows[0].percentUsed).toBe(20);
|
||||
|
||||
// Verify sleep was called for retries (2 retry sleeps)
|
||||
expect(noopSleep).toHaveBeenCalledTimes(2);
|
||||
|
||||
_resetSleepFn();
|
||||
});
|
||||
|
||||
it("reports rate limited after all retries exhausted on 429", async () => {
|
||||
const noopSleep = vi.fn().mockResolvedValue(undefined);
|
||||
_setSleepFn(noopSleep);
|
||||
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("claude")) {
|
||||
return JSON.stringify({
|
||||
accessToken: "test-token",
|
||||
scopes: ["user:profile"],
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
// Always return 429
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const mockRes = {
|
||||
statusCode: 429,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
handler(Buffer.from('{"error":"rate_limited"}'));
|
||||
}
|
||||
if (event === "end") {
|
||||
handler();
|
||||
}
|
||||
>>>>>>> theirs
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
@@ -456,6 +606,7 @@ describe("usage", () => {
|
||||
const claude = providers.find((p) => p.name === "Claude")!;
|
||||
|
||||
expect(claude.status).toBe("error");
|
||||
<<<<<<< ours
|
||||
expect(claude.error).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -488,10 +639,162 @@ describe("usage", () => {
|
||||
});
|
||||
|
||||
setupClaudeApiResponse({ five_hour: { utilization: 10 } });
|
||||
=======
|
||||
expect(claude.error).toBe("Rate limited — try again later");
|
||||
|
||||
// Verify retries happened (2 sleeps for 3 attempts)
|
||||
expect(noopSleep).toHaveBeenCalledTimes(2);
|
||||
|
||||
_resetSleepFn();
|
||||
});
|
||||
|
||||
it("uses exponential backoff delays when retry-after header is absent", async () => {
|
||||
const noopSleep = vi.fn().mockResolvedValue(undefined);
|
||||
_setSleepFn(noopSleep);
|
||||
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("claude")) {
|
||||
return JSON.stringify({
|
||||
accessToken: "test-token",
|
||||
scopes: ["user:profile"],
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
// Always return 429 without retry-after
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const mockRes = {
|
||||
statusCode: 429,
|
||||
headers: {}, // No retry-after header
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
handler(Buffer.from('{"error":"rate_limited"}'));
|
||||
}
|
||||
if (event === "end") {
|
||||
handler();
|
||||
}
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
|
||||
await fetchAllProviderUsage();
|
||||
|
||||
// Exponential backoff: 1000ms * 2^0 = 1000, 1000ms * 2^1 = 2000
|
||||
expect(noopSleep).toHaveBeenCalledTimes(2);
|
||||
expect(noopSleep).toHaveBeenNthCalledWith(1, 1000);
|
||||
expect(noopSleep).toHaveBeenNthCalledWith(2, 2000);
|
||||
|
||||
_resetSleepFn();
|
||||
});
|
||||
|
||||
it("respects retry-after header value for delay", async () => {
|
||||
const noopSleep = vi.fn().mockResolvedValue(undefined);
|
||||
_setSleepFn(noopSleep);
|
||||
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("claude")) {
|
||||
return JSON.stringify({
|
||||
accessToken: "test-token",
|
||||
scopes: ["user:profile"],
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
// 429 with retry-after: 5 seconds
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const mockRes = {
|
||||
statusCode: 429,
|
||||
headers: { "retry-after": "5" },
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
handler(Buffer.from('{"error":"rate_limited"}'));
|
||||
}
|
||||
if (event === "end") {
|
||||
handler();
|
||||
}
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
|
||||
await fetchAllProviderUsage();
|
||||
|
||||
// Should use retry-after value (5s = 5000ms) for both retries
|
||||
expect(noopSleep).toHaveBeenCalledTimes(2);
|
||||
expect(noopSleep).toHaveBeenNthCalledWith(1, 5000);
|
||||
expect(noopSleep).toHaveBeenNthCalledWith(2, 5000);
|
||||
|
||||
_resetSleepFn();
|
||||
});
|
||||
|
||||
it("does not retry on 401 auth errors", async () => {
|
||||
const noopSleep = vi.fn().mockResolvedValue(undefined);
|
||||
_setSleepFn(noopSleep);
|
||||
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("claude")) {
|
||||
return JSON.stringify({
|
||||
accessToken: "expired-token",
|
||||
scopes: ["user:profile"],
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const mockRes = {
|
||||
statusCode: 401,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
handler(Buffer.from('{"error": "unauthorized"}'));
|
||||
}
|
||||
if (event === "end") {
|
||||
handler();
|
||||
}
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
>>>>>>> theirs
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const claude = providers.find((p) => p.name === "Claude")!;
|
||||
|
||||
<<<<<<< ours
|
||||
expect(claude.plan).toBe("Team");
|
||||
});
|
||||
|
||||
@@ -507,11 +810,69 @@ describe("usage", () => {
|
||||
});
|
||||
|
||||
setupClaudeApiResponse({ five_hour: { utilization: 10 } });
|
||||
=======
|
||||
expect(claude.status).toBe("error");
|
||||
expect(claude.error).toContain("Auth expired");
|
||||
// No retries should happen for auth errors
|
||||
expect(noopSleep).not.toHaveBeenCalled();
|
||||
|
||||
_resetSleepFn();
|
||||
});
|
||||
|
||||
it("does not retry on 403 auth errors", async () => {
|
||||
const noopSleep = vi.fn().mockResolvedValue(undefined);
|
||||
_setSleepFn(noopSleep);
|
||||
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("claude")) {
|
||||
return JSON.stringify({
|
||||
accessToken: "forbidden-token",
|
||||
scopes: ["user:profile"],
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = {
|
||||
on: vi.fn(),
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const mockRes = {
|
||||
statusCode: 403,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
handler(Buffer.from('{"error": "forbidden"}'));
|
||||
}
|
||||
if (event === "end") {
|
||||
handler();
|
||||
}
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
>>>>>>> theirs
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const claude = providers.find((p) => p.name === "Claude")!;
|
||||
|
||||
<<<<<<< ours
|
||||
expect(claude.plan).toBe("Pro");
|
||||
=======
|
||||
expect(claude.status).toBe("error");
|
||||
expect(claude.error).toContain("Auth expired");
|
||||
// No retries should happen for auth errors
|
||||
expect(noopSleep).not.toHaveBeenCalled();
|
||||
|
||||
_resetSleepFn();
|
||||
>>>>>>> theirs
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user