135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
import { redisTest } from "@internal/testcontainers";
|
|
import { subDays } from "date-fns";
|
|
import Redis from "ioredis";
|
|
import { describe, expect, vi } from "vitest";
|
|
import { DevPresence } from "~/presenters/v3/DevPresence.server";
|
|
|
|
vi.setConfig({ testTimeout: 30_000 });
|
|
|
|
let seq = 0;
|
|
function ids() {
|
|
seq += 1;
|
|
return { userId: `user_${seq}`, projectId: `proj_${seq}` };
|
|
}
|
|
const recentKey = (userId: string, projectId: string) => `dev-recent:${userId}:${projectId}`;
|
|
|
|
describe("DevPresence — recency ZSET", () => {
|
|
redisTest(
|
|
"getRecentBranchIds returns an empty map when nothing has pinged",
|
|
async ({ redisOptions }) => {
|
|
const presence = new DevPresence(redisOptions);
|
|
const { userId, projectId } = ids();
|
|
|
|
const result = await presence.getRecentBranchIds(userId, projectId);
|
|
expect(result.size).toBe(0);
|
|
}
|
|
);
|
|
|
|
redisTest("a ping records the branch as recently active", async ({ redisOptions }) => {
|
|
const presence = new DevPresence(redisOptions);
|
|
const { userId, projectId } = ids();
|
|
|
|
await presence.setConnected({ userId, projectId, environmentId: "env_a", ttl: 60 });
|
|
|
|
const result = await presence.getRecentBranchIds(userId, projectId);
|
|
expect([...result.keys()]).toEqual(["env_a"]);
|
|
expect(result.get("env_a")).toBeInstanceOf(Date);
|
|
});
|
|
|
|
redisTest("debounces to at most one ZADD per env per minute", async ({ redisOptions }) => {
|
|
const presence = new DevPresence(redisOptions);
|
|
const redis = new Redis(redisOptions);
|
|
const { userId, projectId } = ids();
|
|
|
|
// First ping records env_a.
|
|
await presence.setConnected({ userId, projectId, environmentId: "env_a", ttl: 60 });
|
|
|
|
// Simulate the entry being removed (e.g. another reader pruned it) while the
|
|
// 60s debounce touch key is still live.
|
|
await redis.zrem(recentKey(userId, projectId), "env_a");
|
|
|
|
// A second ping within the debounce window must NOT re-add it.
|
|
await presence.setConnected({ userId, projectId, environmentId: "env_a", ttl: 60 });
|
|
|
|
const result = await presence.getRecentBranchIds(userId, projectId);
|
|
expect(result.has("env_a")).toBe(false);
|
|
|
|
await redis.quit();
|
|
});
|
|
|
|
redisTest("does not return entries older than the recency window", async ({ redisOptions }) => {
|
|
const presence = new DevPresence(redisOptions);
|
|
const redis = new Redis(redisOptions);
|
|
const { userId, projectId } = ids();
|
|
const key = recentKey(userId, projectId);
|
|
|
|
const fourDaysAgo = subDays(Date.now(), 4).getTime();
|
|
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
await redis.zadd(key, fourDaysAgo, "env_stale", oneHourAgo, "env_fresh");
|
|
|
|
const result = await presence.getRecentBranchIds(userId, projectId);
|
|
expect([...result.keys()]).toEqual(["env_fresh"]);
|
|
|
|
await redis.quit();
|
|
});
|
|
|
|
redisTest("physically prunes stale entries on the next ping", async ({ redisOptions }) => {
|
|
const presence = new DevPresence(redisOptions);
|
|
const redis = new Redis(redisOptions);
|
|
const { userId, projectId } = ids();
|
|
const key = recentKey(userId, projectId);
|
|
|
|
await redis.zadd(key, subDays(Date.now(), 4).getTime(), "env_stale");
|
|
|
|
// A fresh ping triggers the ZREMRANGEBYSCORE cleanup for this user/project.
|
|
await presence.setConnected({ userId, projectId, environmentId: "env_fresh", ttl: 60 });
|
|
|
|
expect(await redis.zscore(key, "env_stale")).toBeNull();
|
|
expect(await redis.zcard(key)).toBe(1);
|
|
|
|
await redis.quit();
|
|
});
|
|
|
|
redisTest(
|
|
"caps cardinality at 50 even under a flood of distinct branches",
|
|
async ({ redisOptions }) => {
|
|
const presence = new DevPresence(redisOptions);
|
|
const redis = new Redis(redisOptions);
|
|
const { userId, projectId } = ids();
|
|
|
|
// 60 distinct envs, each with its own debounce key, so each performs a ZADD.
|
|
for (let i = 0; i < 60; i++) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await presence.setConnected({ userId, projectId, environmentId: `env_${i}`, ttl: 60 });
|
|
}
|
|
|
|
expect(await redis.zcard(recentKey(userId, projectId))).toBe(50);
|
|
|
|
await redis.quit();
|
|
}
|
|
);
|
|
|
|
redisTest("returns recent branches in most-recent-first order", async ({ redisOptions }) => {
|
|
const presence = new DevPresence(redisOptions);
|
|
const redis = new Redis(redisOptions);
|
|
const { userId, projectId } = ids();
|
|
const key = recentKey(userId, projectId);
|
|
|
|
const now = Date.now();
|
|
await redis.zadd(
|
|
key,
|
|
now - 3000,
|
|
"env_oldest",
|
|
now - 2000,
|
|
"env_middle",
|
|
now - 1000,
|
|
"env_newest"
|
|
);
|
|
|
|
const result = await presence.getRecentBranchIds(userId, projectId);
|
|
expect([...result.keys()]).toEqual(["env_newest", "env_middle", "env_oldest"]);
|
|
|
|
await redis.quit();
|
|
});
|
|
});
|