feature/IO-3701-Harness-Replacement - Implement

This commit is contained in:
Dave
2026-05-20 14:41:24 -04:00
parent 84ec68f142
commit deb2fc28ce
100 changed files with 4813 additions and 773 deletions

View File

@@ -0,0 +1,103 @@
import { describe, expect, it, vi } from "vitest";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { applyRedisHelpers } = require("./redisHelpers");
/**
* Creates an in-memory Redis-like test double for feature flag cache helper tests.
*/
const createRedis = () => {
const values = new Map();
const expirations = new Map();
return {
del: vi.fn(async (...keys) => {
let deleted = 0;
for (const key of keys) {
if (values.delete(key)) deleted += 1;
}
return deleted;
}),
expire: vi.fn(async (key, ttl) => {
expirations.set(key, ttl);
return 1;
}),
get: vi.fn(async (key) => values.get(key) ?? null),
incr: vi.fn(async (key) => {
const nextValue = Number(values.get(key) || 0) + 1;
values.set(key, String(nextValue));
return nextValue;
}),
set: vi.fn(async (key, value) => {
values.set(key, String(value));
return "OK";
}),
setnx: vi.fn(async (key, value) => {
if (values.has(key)) return 0;
values.set(key, String(value));
return 1;
}),
values,
expirations
};
};
/**
* Applies Redis helpers to the in-memory test double and returns the mounted API.
*/
const createHelpers = () => {
const pubClient = createRedis();
const app = { use: vi.fn() };
const logger = { log: vi.fn() };
const helpers = applyRedisHelpers({ pubClient, app, logger });
return { app, helpers, logger, pubClient };
};
describe("feature flag Redis cache helpers", () => {
it("stores and reads bodyshop feature flags under the current cache version", async () => {
const { helpers, pubClient } = createHelpers();
await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "on" } } });
expect(await helpers.getBodyshopFeatureFlagsCacheVersion()).toBe("1");
expect(await helpers.getBodyshopFeatureFlagsFromRedis("shop-1")).toEqual({
flags: {
Demo: {
treatment: "on"
}
}
});
expect(pubClient.values.has("bodyshop-feature-flags:v1:shop-1")).toBe(true);
expect(pubClient.expirations.get("bodyshop-feature-flags:v1:shop-1")).toBe(3600);
});
it("global invalidation bumps the cache version instead of deleting old versioned keys", async () => {
const { helpers, pubClient } = createHelpers();
await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "on" } } });
const nextVersion = await helpers.invalidateAllBodyshopFeatureFlagsInRedis();
expect(nextVersion).toBe(2);
expect(pubClient.del).not.toHaveBeenCalledWith("bodyshop-feature-flags:v1:shop-1");
expect(await helpers.getBodyshopFeatureFlagsFromRedis("shop-1")).toBeNull();
expect(pubClient.values.has("bodyshop-feature-flags:v1:shop-1")).toBe(true);
});
it("bodyshop invalidation deletes only the current version cache key for that shop", async () => {
const { helpers, pubClient } = createHelpers();
await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "on" } } });
await helpers.setBodyshopFeatureFlagsInRedis("shop-2", { flags: { Demo: { treatment: "on" } } });
await helpers.invalidateAllBodyshopFeatureFlagsInRedis();
await helpers.setBodyshopFeatureFlagsInRedis("shop-1", { flags: { Demo: { treatment: "off" } } });
await helpers.setBodyshopFeatureFlagsInRedis("shop-2", { flags: { Demo: { treatment: "on" } } });
await helpers.invalidateBodyshopFeatureFlagsInRedis("shop-1");
expect(pubClient.values.has("bodyshop-feature-flags:v1:shop-1")).toBe(true);
expect(pubClient.values.has("bodyshop-feature-flags:v2:shop-1")).toBe(false);
expect(pubClient.values.has("bodyshop-feature-flags:v2:shop-2")).toBe(true);
});
});

View File

@@ -7,6 +7,8 @@ const client = require("../graphql-client/graphql-client").client;
* @type {number}
*/
const BODYSHOP_CACHE_TTL = 3600; // 1 hour
const FEATURE_FLAGS_CACHE_TTL = 3600; // 1 hour
const FEATURE_FLAGS_CACHE_VERSION_KEY = "bodyshop-feature-flags:version";
/**
* Chatter API token cache TTL in seconds
@@ -20,6 +22,7 @@ const CHATTER_TOKEN_CACHE_TTL = 3600; // 1 hour
* @returns {`bodyshop-cache:${string}`}
*/
const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`;
const getBodyshopFeatureFlagsCacheKey = (bodyshopId, version = "1") => `bodyshop-feature-flags:v${version}:${bodyshopId}`;
/**
* Generate a cache key for a Chatter API token
@@ -418,6 +421,92 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
}
};
/**
* Reads or initializes the global feature flag cache version used by per-shop keys.
*/
const getBodyshopFeatureFlagsCacheVersion = async () => {
try {
const version = await pubClient.get(FEATURE_FLAGS_CACHE_VERSION_KEY);
if (version) return version;
await pubClient.setnx(FEATURE_FLAGS_CACHE_VERSION_KEY, "1");
return "1";
} catch (error) {
logger.log("get-bodyshop-feature-flags-cache-version", "ERROR", "redis", null, {
error: error.message
});
return "1";
}
};
/**
* Reads a bodyshop feature flag payload from the current or supplied cache version.
*/
const getBodyshopFeatureFlagsFromRedis = async (bodyshopId, version) => {
const cacheVersion = version || (await getBodyshopFeatureFlagsCacheVersion());
const key = getBodyshopFeatureFlagsCacheKey(bodyshopId, cacheVersion);
try {
const cachedData = await pubClient.get(key);
return cachedData ? JSON.parse(cachedData) : null;
} catch (error) {
logger.log("get-bodyshop-feature-flags-from-redis", "ERROR", "redis", null, {
bodyshopId,
error: error.message
});
return null;
}
};
/**
* Stores a bodyshop feature flag payload under a versioned Redis key.
*/
const setBodyshopFeatureFlagsInRedis = async (bodyshopId, value, version) => {
const cacheVersion = version || (await getBodyshopFeatureFlagsCacheVersion());
const key = getBodyshopFeatureFlagsCacheKey(bodyshopId, cacheVersion);
try {
await pubClient.set(key, toRedisJson(value));
await pubClient.expire(key, FEATURE_FLAGS_CACHE_TTL);
} catch (error) {
logger.log("set-bodyshop-feature-flags-in-redis", "ERROR", "redis", null, {
bodyshopId,
error: error.message
});
}
};
/**
* Deletes one bodyshop's feature flag cache entry for the current or supplied version.
*/
const invalidateBodyshopFeatureFlagsInRedis = async (bodyshopId, version) => {
const cacheVersion = version || (await getBodyshopFeatureFlagsCacheVersion());
const key = getBodyshopFeatureFlagsCacheKey(bodyshopId, cacheVersion);
try {
await pubClient.del(key);
} catch (error) {
logger.log("invalidate-bodyshop-feature-flags-in-redis", "ERROR", "redis", null, {
bodyshopId,
error: error.message
});
}
};
/**
* Invalidates all bodyshop feature flag caches by incrementing the global version.
*/
const invalidateAllBodyshopFeatureFlagsInRedis = async () => {
try {
return await pubClient.incr(FEATURE_FLAGS_CACHE_VERSION_KEY);
} catch (error) {
logger.log("invalidate-all-bodyshop-feature-flags-in-redis", "ERROR", "redis", null, {
error: error.message
});
return 0;
}
};
/**
* Set provider cache data
* @param ns
@@ -482,6 +571,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
const api = {
getUserSocketMappingKey,
getBodyshopCacheKey,
getBodyshopFeatureFlagsCacheKey,
getChatterTokenCacheKey,
setSessionData,
getSessionData,
@@ -493,6 +583,11 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
refreshUserSocketTTL,
getBodyshopFromRedis,
updateOrInvalidateBodyshopFromRedis,
getBodyshopFeatureFlagsCacheVersion,
getBodyshopFeatureFlagsFromRedis,
setBodyshopFeatureFlagsInRedis,
invalidateBodyshopFeatureFlagsInRedis,
invalidateAllBodyshopFeatureFlagsInRedis,
setSessionTransactionData,
getSessionTransactionData,
clearSessionTransactionData,