feat(FN-1528): sanitize Kimi 404 url.not_found errors in usage screen

- Add sanitization for Kimi API 404 url.not_found error messages in usage.ts
- Replace technical error codes with user-friendly messages to avoid leaking internal API details
- Add regression test covering error sanitization with mock usage data
This commit is contained in:
gsxdsm
2026-04-10 02:57:32 -07:00
parent e8dd0f2416
commit 46b4d98e47
3 changed files with 73 additions and 3 deletions

View File

@@ -3361,8 +3361,65 @@ describe("usage", () => {
expect(requestedPaths).toEqual(["/v1/coding_plan/usage", "/v1/coding-plan/usage"]);
});
it("returns error when both endpoints return 404", async () => {
// Regression test: when both endpoints return 404, should return error
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) => {

View File

@@ -1482,8 +1482,16 @@ async function fetchKimiUsage(): Promise<ProviderUsage> {
if (res.status === 404) {
if (isLastEndpoint) {
// Last endpoint also returned 404 — return error
// Sanitize known endpoint-not-found errors to avoid leaking raw JSON to users.
// The "url.not_found" error indicates the usage endpoint doesn't exist for this account.
const isEndpointNotFound = res.body.includes('"error":"url.not_found"') ||
res.body.includes('"error":"url_not_found"');
usage.status = "error";
usage.error = `HTTP 404: ${res.body.slice(0, 200)}`;
if (isEndpointNotFound) {
usage.error = "Usage endpoint unavailable — Kimi coding plan may not be active on this account";
} else {
usage.error = `HTTP 404: ${res.body.slice(0, 200)}`;
}
return usage;
}
// Try next endpoint