fix: address Copilot cache review

This commit is contained in:
ajnart committed 2026-07-31 13:32:48 +02:00
1 parent cd6c83a7b0
commit 388a74086e
4 files changed
+46 -2

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
// @vitest-environment node
import { describe, expect, test, vi } from "vitest";
vi.mock("./connection", () => ({
createRedisConnection: () => null,
}));
import { createLockChannel } from "./channel";
describe("createLockChannel", () => {
test("degrades gracefully when Redis connections are disabled", async () => {
const lock = createLockChannel("test-lock");
const token = await lock.acquireAsync(60);
expect(token).toEqual(expect.any(String));
if (!token) throw new Error("Expected the fallback lock to return a token");
await expect(lock.releaseAsync(token)).resolves.toBeUndefined();
});
});
+8 -2
View File
@@ -139,11 +139,17 @@ export const createLockChannel = (name: string) => {
return {
acquireAsync: async (ttlSeconds: number) => {
const token = createId();
const result = await getSetClient.set(name, token, "EX", ttlSeconds, "NX");
const client = getSetClient as typeof getSetClient | null;
if (!client) return token;
const result = await client.set(name, token, "EX", ttlSeconds, "NX");
return result === "OK" ? token : null;
},
releaseAsync: async (token: string) => {
await getSetClient.eval(
const client = getSetClient as typeof getSetClient | null;
if (!client) return;
await client.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
1,
name,
@@ -11,10 +11,12 @@ const mocks = vi.hoisted(() => {
const values = new Map<string, StoredValue>();
const locks = new Set<string>();
const listReleases = vi.fn();
const redisOperation = vi.fn();
return {
env: { NO_EXTERNAL_CONNECTION: false },
listReleases,
redisOperation,
values,
locks,
removeFreshResult() {
@@ -28,6 +30,7 @@ vi.mock("@homarr/common/env", () => ({ env: mocks.env }));
vi.mock("@homarr/redis", () => ({
createGetSetChannel: (name: string) => ({
getAsync: async () => {
mocks.redisOperation();
const stored = mocks.values.get(name);
if (!stored) return null;
if (stored.expiresAt !== null && Date.now() >= stored.expiresAt) {
@@ -37,22 +40,26 @@ vi.mock("@homarr/redis", () => ({
return stored.value;
},
setAsync: async (value: unknown, options?: { ttlSeconds?: number }) => {
mocks.redisOperation();
mocks.values.set(name, {
value,
expiresAt: options?.ttlSeconds ? Date.now() + options.ttlSeconds * 1_000 : null,
});
},
removeAsync: async () => {
mocks.redisOperation();
mocks.values.delete(name);
},
}),
createLockChannel: (name: string) => ({
acquireAsync: async () => {
mocks.redisOperation();
if (mocks.locks.has(name)) return null;
mocks.locks.add(name);
return "test-lock-token";
},
releaseAsync: async () => {
mocks.redisOperation();
mocks.locks.delete(name);
},
}),
@@ -94,6 +101,7 @@ describe("persisted update checker cache", () => {
vi.restoreAllMocks();
mocks.env.NO_EXTERNAL_CONNECTION = false;
mocks.listReleases.mockReset();
mocks.redisOperation.mockReset();
mocks.values.clear();
mocks.locks.clear();
});
@@ -202,5 +210,6 @@ describe("persisted update checker cache", () => {
await expect(handler.getDataAsync()).resolves.toMatchObject({ data: { availableUpdates: [] } });
expect(mocks.listReleases).not.toHaveBeenCalled();
expect(mocks.redisOperation).not.toHaveBeenCalled();
});
});
@@ -42,6 +42,14 @@ const waitForConcurrentUpdateCheckAsync = async () => {
};
const getCachedAvailableUpdatesAsync = async (): Promise<UpdateCheckCacheEntry> => {
if (env.NO_EXTERNAL_CONNECTION) {
return {
availableUpdates: [],
attemptedAt: Date.now(),
checkedAt: null,
};
}
const cached = await freshUpdateCheckChannel.getAsync();
if (cached) return cached;