Spaces:
Paused
Paused
File size: 5,733 Bytes
0c8b3c0 b72575f 0c8b3c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | import { describe, it, expect, vi, beforeEach } from "vitest";
import type { AccountEntry, AccountsFile } from "../types.js";
// Must use vi.hoisted() for mock variables referenced in vi.mock factories
const mockFs = vi.hoisted(() => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(() => ""),
writeFileSync: vi.fn(),
renameSync: vi.fn(),
mkdirSync: vi.fn(),
}));
vi.mock("fs", () => mockFs);
vi.mock("../../paths.js", () => ({
getDataDir: vi.fn(() => "/tmp/test-persistence"),
}));
vi.mock("../jwt-utils.js", () => ({
extractChatGptAccountId: vi.fn((token: string) => `acct-${token}`),
extractUserProfile: vi.fn(() => null),
isTokenExpired: vi.fn(() => false),
}));
import { createFsPersistence } from "../account-persistence.js";
function makeEntry(id: string): AccountEntry {
return {
id,
token: `tok-${id}`,
refreshToken: null,
email: `${id}@test.com`,
accountId: `acct-${id}`,
userId: `user-${id}`,
planType: "free",
proxyApiKey: `key-${id}`,
status: "active",
usage: {
request_count: 0,
input_tokens: 0,
output_tokens: 0,
empty_response_count: 0,
last_used: null,
rate_limit_until: null,
window_request_count: 0,
window_input_tokens: 0,
window_output_tokens: 0,
window_counters_reset_at: null,
limit_window_seconds: null,
},
addedAt: new Date().toISOString(),
cachedQuota: null,
quotaFetchedAt: null,
};
}
describe("account-persistence", () => {
beforeEach(() => {
vi.clearAllMocks();
mockFs.existsSync.mockReturnValue(false);
});
describe("load", () => {
it("returns empty entries when no files exist", () => {
const p = createFsPersistence();
const result = p.load();
expect(result.entries).toEqual([]);
expect(result.needsPersist).toBe(false);
});
it("loads from accounts.json", () => {
const entry = makeEntry("a");
const data: AccountsFile = { accounts: [entry] };
mockFs.existsSync.mockImplementation(((path: string) =>
path.includes("accounts.json")) as () => boolean,
);
mockFs.readFileSync.mockReturnValue(JSON.stringify(data));
const p = createFsPersistence();
const result = p.load();
expect(result.entries).toHaveLength(1);
expect(result.entries[0].id).toBe("a");
});
it("skips entries without id or token", () => {
const data = { accounts: [{ id: "", token: "x" }, { id: "b" }] };
mockFs.existsSync.mockImplementation(((path: string) =>
path.includes("accounts.json")) as () => boolean,
);
mockFs.readFileSync.mockReturnValue(JSON.stringify(data));
const p = createFsPersistence();
expect(p.load().entries).toEqual([]);
});
it("backfills missing empty_response_count and auto-persists", () => {
const entry = makeEntry("a");
(entry.usage as unknown as Record<string, unknown>).empty_response_count = undefined;
const data: AccountsFile = { accounts: [entry] };
mockFs.existsSync.mockImplementation(((path: string) =>
path.includes("accounts.json")) as () => boolean,
);
mockFs.readFileSync.mockReturnValue(JSON.stringify(data));
const p = createFsPersistence();
const result = p.load();
expect(result.entries[0].usage.empty_response_count).toBe(0);
expect(result.needsPersist).toBe(true);
// Verify auto-persist was triggered (write + rename for atomic save)
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(1);
expect(mockFs.renameSync).toHaveBeenCalledTimes(1);
});
});
describe("save", () => {
it("writes atomically via tmp file + rename", () => {
mockFs.existsSync.mockReturnValue(true);
const p = createFsPersistence();
const entry = makeEntry("a");
p.save([entry]);
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(1);
const writtenPath = mockFs.writeFileSync.mock.calls[0][0] as string;
expect(writtenPath).toMatch(/accounts\.json\.tmp$/);
expect(mockFs.renameSync).toHaveBeenCalledTimes(1);
});
it("creates directory if missing", () => {
mockFs.existsSync.mockReturnValue(false);
const p = createFsPersistence();
p.save([makeEntry("a")]);
expect(mockFs.mkdirSync).toHaveBeenCalledWith(
expect.any(String),
{ recursive: true },
);
});
it("serializes accounts as JSON", () => {
mockFs.existsSync.mockReturnValue(true);
const p = createFsPersistence();
const entries = [makeEntry("a"), makeEntry("b")];
p.save(entries);
const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1] as string) as AccountsFile;
expect(written.accounts).toHaveLength(2);
expect(written.accounts[0].id).toBe("a");
expect(written.accounts[1].id).toBe("b");
});
});
describe("legacy migration", () => {
it("migrates from auth.json when accounts.json does not exist", () => {
const legacyData = {
token: "legacy-token",
proxyApiKey: "old-key",
userInfo: { email: "old@test.com", planType: "free" },
};
mockFs.existsSync.mockImplementation(((path: string) => {
if (path.includes("accounts.json")) return false;
if (path.includes("auth.json")) return true;
return false;
}) as () => boolean);
mockFs.readFileSync.mockReturnValue(JSON.stringify(legacyData));
const p = createFsPersistence();
const result = p.load();
expect(result.entries).toHaveLength(1);
expect(result.entries[0].token).toBe("legacy-token");
// Should rename old file
expect(mockFs.renameSync).toHaveBeenCalled();
});
});
});
|