Skip to content

Commit bb3e210

Browse files
committed
Tighten auth-store concurrency tests around the write queue
Replace the fixed 1.4s sleep with await-then-release so the lock-window case cannot flake if the event loop slips past the foreign lock removal, and add a same-process dual-profile save that pins the lost-update net without another timed wait.
1 parent 79e1034 commit bb3e210

1 file changed

Lines changed: 64 additions & 32 deletions

File tree

src/auth/store.test.ts

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,54 @@ describe("createAuthStore", () => {
114114
}
115115
});
116116

117+
test("queues same-process profile writes so neither save is lost", async () => {
118+
const home = await mkdtemp(join(tmpdir(), "oauth-store-same-process-"));
119+
try {
120+
const store = createAuthStore<TestTokens>({
121+
filename: "test-auth.json",
122+
settingsDirName: TEST_SETTINGS_DIR,
123+
isTokens: isTestTokens,
124+
});
125+
126+
await Promise.all([
127+
store.saveProfile(
128+
{
129+
name: "personal",
130+
tokens: { access: "p", refresh: "pr", expiresAt: 1 },
131+
createdAt: 1,
132+
},
133+
home,
134+
),
135+
store.saveProfile(
136+
{
137+
name: "work",
138+
tokens: { access: "w", refresh: "wr", expiresAt: 2 },
139+
createdAt: 2,
140+
},
141+
home,
142+
),
143+
]);
144+
145+
const profiles = await store.listProfiles(home);
146+
expect(profiles.map((profile) => profile.name)).toEqual([
147+
"personal",
148+
"work",
149+
]);
150+
expect(profiles.find((profile) => profile.name === "personal")).toEqual({
151+
name: "personal",
152+
tokens: { access: "p", refresh: "pr", expiresAt: 1 },
153+
createdAt: 1,
154+
});
155+
expect(profiles.find((profile) => profile.name === "work")).toEqual({
156+
name: "work",
157+
tokens: { access: "w", refresh: "wr", expiresAt: 2 },
158+
createdAt: 2,
159+
});
160+
} finally {
161+
await rm(home, { recursive: true, force: true });
162+
}
163+
});
164+
117165
test("gives queued same-process writes their own lock window", async () => {
118166
const home = await mkdtemp(join(tmpdir(), "oauth-store-queue-"));
119167
try {
@@ -131,43 +179,27 @@ describe("createAuthStore", () => {
131179
home,
132180
);
133181

134-
// A foreign process holds the credential lock past the first waiter's
135-
// deadline, then releases; the write queued behind it must still land.
182+
// Hold the lock until the head of the same-process queue times out; the
183+
// queued write must still get its own lock window after we release.
136184
const lockPath = `${store.authPath(home)}.lock`;
137185
await writeFile(lockPath, "foreign", { mode: 0o600 });
138186

139-
const first = store
140-
.updateTokens(
141-
"work",
142-
{ access: "first", refresh: "r1", expiresAt: 2 },
143-
home,
144-
)
145-
.then(
146-
() => "resolved" as const,
147-
(error: unknown) => error,
148-
);
149-
const second = store
150-
.updateTokens(
151-
"work",
152-
{ access: "second", refresh: "r2", expiresAt: 3 },
153-
home,
154-
)
155-
.then(
156-
() => "resolved" as const,
157-
(error: unknown) => error,
158-
);
187+
const first = store.updateTokens(
188+
"work",
189+
{ access: "first", refresh: "r1", expiresAt: 2 },
190+
home,
191+
);
192+
const second = store.updateTokens(
193+
"work",
194+
{ access: "second", refresh: "r2", expiresAt: 3 },
195+
home,
196+
);
159197

160-
await Bun.sleep(1_400);
198+
await expect(first).rejects.toThrow(
199+
"Timed out waiting for OAuth credential lock",
200+
);
161201
await rm(lockPath, { force: true });
162-
163-
const firstResult = await first;
164-
expect(firstResult).toBeInstanceOf(Error);
165-
if (firstResult instanceof Error) {
166-
expect(firstResult.message).toContain(
167-
"Timed out waiting for OAuth credential lock",
168-
);
169-
}
170-
expect(await second).toBe("resolved");
202+
await expect(second).resolves.toBeUndefined();
171203

172204
expect((await store.loadProfile("work", home))?.tokens.access).toBe(
173205
"second",

0 commit comments

Comments
 (0)