feat(FN-1649): remove Kimi from usage indicator flow
- Remove Kimi provider from usage tracking module - Clean up unused Kimi-related code in usage.ts (225 lines removed) - Remove Kimi-specific tests from usage.test.ts (686 lines removed) - Add tests for usage routes (routes.test.ts) - Add new usage API routes for usage data endpoints - Fix planning resume state mismatch in PlanningModeModal - Add PlanningModeModal tests for state consistency
This commit is contained in:
@@ -71,13 +71,13 @@ describe("usage", () => {
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
|
||||
expect(providers).toHaveLength(6);
|
||||
expect(providers).toHaveLength(5);
|
||||
expect(providers.map((p) => p.name)).toContain("Claude");
|
||||
expect(providers.map((p) => p.name)).toContain("Codex");
|
||||
expect(providers.map((p) => p.name)).toContain("Gemini");
|
||||
expect(providers.map((p) => p.name)).toContain("Minimax");
|
||||
expect(providers.map((p) => p.name)).toContain("Zai");
|
||||
expect(providers.map((p) => p.name)).toContain("Kimi");
|
||||
expect(providers.map((p) => p.name)).not.toContain("Kimi");
|
||||
|
||||
// All should be no-auth status
|
||||
for (const p of providers) {
|
||||
@@ -112,7 +112,7 @@ describe("usage", () => {
|
||||
|
||||
// Should be different array reference
|
||||
expect(second).not.toBe(first);
|
||||
expect(second).toHaveLength(6);
|
||||
expect(second).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2838,686 +2838,6 @@ describe("usage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kimi provider", () => {
|
||||
it("detects no auth when pi auth.json doesn't exist", async () => {
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi");
|
||||
|
||||
expect(kimi).toBeDefined();
|
||||
expect(kimi!.status).toBe("no-auth");
|
||||
expect(kimi!.error).toContain("No Kimi credentials");
|
||||
});
|
||||
|
||||
it("detects no auth when kimi-coding entry has no key", async () => {
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key" /* missing key */ },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi");
|
||||
|
||||
expect(kimi!.status).toBe("no-auth");
|
||||
expect(kimi!.error).toContain("No Kimi credentials");
|
||||
});
|
||||
|
||||
it("detects no auth when kimi-coding entry is missing entirely", async () => {
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({ /* no kimi-coding key */ });
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi");
|
||||
|
||||
expect(kimi!.status).toBe("no-auth");
|
||||
expect(kimi!.error).toContain("No Kimi credentials");
|
||||
});
|
||||
|
||||
it("parses usage data from API response with windows array", async () => {
|
||||
const now = Date.now();
|
||||
const mockResponse = {
|
||||
data: {
|
||||
windows: [
|
||||
{
|
||||
label: "Coding",
|
||||
used: 150,
|
||||
total: 500,
|
||||
reset_time: now + 3 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
label: "MCP",
|
||||
used: 80,
|
||||
total: 200,
|
||||
remaining: 120,
|
||||
reset_time: now + 24 * 60 * 60 * 1000,
|
||||
},
|
||||
],
|
||||
plan: "pro",
|
||||
},
|
||||
};
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
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: 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;
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("ok");
|
||||
expect(kimi.plan).toBe("Pro");
|
||||
expect(kimi.windows).toHaveLength(2);
|
||||
|
||||
const codingWindow = kimi.windows.find((w) => w.label === "Coding")!;
|
||||
expect(codingWindow).toBeDefined();
|
||||
// used=150, total=500 → 150/500*100 = 30%
|
||||
expect(codingWindow.percentUsed).toBe(30);
|
||||
expect(codingWindow.percentLeft).toBe(70);
|
||||
expect(codingWindow.resetText).toContain("resets in");
|
||||
});
|
||||
|
||||
it("returns error on 401 response", async () => {
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "bad-key" },
|
||||
});
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("error");
|
||||
expect(kimi.error).toContain("Auth expired");
|
||||
});
|
||||
|
||||
it("extracts plan information from response", async () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
used: 100,
|
||||
total: 500,
|
||||
reset_time: Date.now() + 60 * 60 * 1000,
|
||||
level: "enterprise",
|
||||
},
|
||||
};
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
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: 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;
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("ok");
|
||||
expect(kimi.plan).toBe("Enterprise");
|
||||
expect(kimi.windows).toHaveLength(1);
|
||||
expect(kimi.windows[0].label).toBe("Coding Plan");
|
||||
});
|
||||
});
|
||||
|
||||
describe("endpoint fallback", () => {
|
||||
it("falls back to hyphen endpoint when underscore endpoint returns 404 url.not_found", async () => {
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
total: 100,
|
||||
used: 25,
|
||||
remaining: 75,
|
||||
reset_time: Date.now() + 3600000,
|
||||
},
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
let statusCode = 200;
|
||||
let responseBody = mockResponse;
|
||||
|
||||
if (pathname === "/v1/coding_plan/usage") {
|
||||
// First endpoint (underscore) returns 404 with url.not_found
|
||||
statusCode = 404;
|
||||
responseBody = { error: "url.not_found" };
|
||||
}
|
||||
|
||||
const mockRes = {
|
||||
statusCode,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify(responseBody)));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("ok");
|
||||
expect(kimi.windows).toHaveLength(1);
|
||||
expect(kimi.windows[0].percentUsed).toBe(25);
|
||||
// Should have tried both endpoints: underscore first, then hyphen fallback
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage", "/v1/coding-plan/usage"]);
|
||||
});
|
||||
|
||||
it("falls back when first endpoint returns 404 url.not_found with extra fields", async () => {
|
||||
// This tests the real failure payload shape from production:
|
||||
// {"code":5,"error":"url.not_found","message":"没找到对象",...}
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
total: 100,
|
||||
used: 25,
|
||||
remaining: 75,
|
||||
reset_time: Date.now() + 3600000,
|
||||
},
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
let statusCode = 200;
|
||||
let responseBody = mockResponse;
|
||||
|
||||
if (pathname === "/v1/coding_plan/usage") {
|
||||
// First endpoint returns 404 with real production payload shape (extra fields)
|
||||
statusCode = 404;
|
||||
responseBody = { code: 5, error: "url.not_found", message: "没找到对象", type: "invalid_request" };
|
||||
}
|
||||
|
||||
const mockRes = {
|
||||
statusCode,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify(responseBody)));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("ok");
|
||||
expect(kimi.windows).toHaveLength(1);
|
||||
expect(kimi.windows[0].percentUsed).toBe(25);
|
||||
// Should fall back to hyphen endpoint when underscore returns url.not_found with extra fields
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage", "/v1/coding-plan/usage"]);
|
||||
});
|
||||
|
||||
it("does not attempt fallback on 401 auth error", async () => {
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
const mockRes = {
|
||||
statusCode: 401,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify({})));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("error");
|
||||
expect(kimi.error).toContain("Auth expired");
|
||||
// Only first endpoint should be attempted (no fallback for auth errors)
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage"]);
|
||||
});
|
||||
|
||||
it("does not attempt fallback on 403 auth error", async () => {
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
const mockRes = {
|
||||
statusCode: 403,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify({})));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("error");
|
||||
expect(kimi.error).toContain("Auth expired");
|
||||
// Only first endpoint should be attempted (no fallback for auth errors)
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage"]);
|
||||
});
|
||||
|
||||
it("falls back to hyphen endpoint when first endpoint returns 404 with non-url.not_found body", async () => {
|
||||
// Regression test: ANY 404 triggers fallback, not just url.not_found
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockFallbackResponse = {
|
||||
data: {
|
||||
total: 100,
|
||||
used: 50,
|
||||
remaining: 50,
|
||||
reset_time: Date.now() + 3600000,
|
||||
},
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
let statusCode = 200;
|
||||
let responseBody = mockFallbackResponse;
|
||||
|
||||
if (pathname === "/v1/coding_plan/usage") {
|
||||
// First endpoint returns 404 with non-url.not_found body
|
||||
statusCode = 404;
|
||||
responseBody = { error: "something_else" };
|
||||
}
|
||||
|
||||
const mockRes = {
|
||||
statusCode,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify(responseBody)));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("ok");
|
||||
expect(kimi.windows).toHaveLength(1);
|
||||
expect(kimi.windows[0].percentUsed).toBe(50);
|
||||
// Should attempt both endpoints: underscore first (404), then hyphen fallback
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage", "/v1/coding-plan/usage"]);
|
||||
});
|
||||
|
||||
it("falls back when first endpoint returns 404 with non-JSON body", async () => {
|
||||
// Regression test: non-JSON 404 body should still trigger fallback
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockFallbackResponse = {
|
||||
data: {
|
||||
total: 100,
|
||||
used: 75,
|
||||
remaining: 25,
|
||||
reset_time: Date.now() + 3600000,
|
||||
},
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
let statusCode = 200;
|
||||
let responseBody = mockFallbackResponse;
|
||||
|
||||
if (pathname === "/v1/coding_plan/usage") {
|
||||
// First endpoint returns 404 with plain text (not JSON)
|
||||
statusCode = 404;
|
||||
responseBody = "Not Found";
|
||||
}
|
||||
|
||||
const mockRes = {
|
||||
statusCode,
|
||||
headers: { "content-type": "text/plain" },
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") {
|
||||
const body = typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody);
|
||||
handler(Buffer.from(body));
|
||||
}
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("ok");
|
||||
expect(kimi.windows).toHaveLength(1);
|
||||
expect(kimi.windows[0].percentUsed).toBe(75);
|
||||
// Should attempt both endpoints
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage", "/v1/coding-plan/usage"]);
|
||||
});
|
||||
|
||||
it("returns sanitized error when both endpoints return 404 url.not_found", async () => {
|
||||
// Regression test: when both endpoints return 404 with url.not_found error,
|
||||
// the error message should be sanitized (no raw JSON leaked to users)
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
// Real production payload shape with url.not_found error
|
||||
const urlNotFoundPayload = JSON.stringify({
|
||||
code: 5,
|
||||
error: "url.not_found",
|
||||
message: "没找到对象",
|
||||
type: "invalid_request",
|
||||
});
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
const mockRes = {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(urlNotFoundPayload));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("error");
|
||||
// Error message should be sanitized — no raw JSON content
|
||||
expect(kimi.error).not.toContain(urlNotFoundPayload);
|
||||
expect(kimi.error).not.toContain("code");
|
||||
expect(kimi.error).not.toContain("url.not_found");
|
||||
expect(kimi.error).not.toContain("没找到对象");
|
||||
// Should contain a clean, actionable message
|
||||
expect(kimi.error).toContain("Usage endpoint unavailable");
|
||||
// Should attempt both endpoints
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage", "/v1/coding-plan/usage"]);
|
||||
});
|
||||
|
||||
it("returns raw error for other 404 body content (not url.not_found)", async () => {
|
||||
// Regression test: when both endpoints return 404 with non-url.not_found body,
|
||||
// the error should still include the raw body for debugging
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
const mockRes = {
|
||||
statusCode: 404,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from("Not Found"));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("error");
|
||||
expect(kimi.error).toContain("HTTP 404");
|
||||
// Should attempt both endpoints
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage", "/v1/coding-plan/usage"]);
|
||||
});
|
||||
|
||||
it("succeeds with single endpoint when first endpoint works", async () => {
|
||||
const requestedPaths: string[] = [];
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
"kimi-coding": { type: "api_key", key: "test-api-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: {
|
||||
total: 100,
|
||||
used: 50,
|
||||
remaining: 50,
|
||||
reset_time: Date.now() + 3600000,
|
||||
},
|
||||
};
|
||||
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
const pathname = new URL(`https://${options.hostname}${options.path}`).pathname;
|
||||
requestedPaths.push(pathname);
|
||||
|
||||
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 { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const kimi = providers.find((p) => p.name === "Kimi")!;
|
||||
|
||||
expect(kimi.status).toBe("ok");
|
||||
expect(kimi.windows).toHaveLength(1);
|
||||
expect(kimi.windows[0].percentUsed).toBe(50);
|
||||
// Should only request first endpoint (underscore)
|
||||
expect(requestedPaths).toEqual(["/v1/coding_plan/usage"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Claude CLI fallback parsing", () => {
|
||||
describe("_stripClaudeAnsi", () => {
|
||||
|
||||
Reference in New Issue
Block a user