+2
-2
compose.yaml
+2
-2
compose.yaml
···
72
72
- ./prometheus.yml:/etc/prometheus/prometheus.yml
73
73
- prometheus-data:/prometheus
74
74
command:
75
-
- '--config.file=/etc/prometheus/prometheus.yml'
76
-
- '--storage.tsdb.path=/prometheus'
75
+
- "--config.file=/etc/prometheus/prometheus.yml"
76
+
- "--storage.tsdb.path=/prometheus"
77
77
networks:
78
78
- skywatch-network
79
79
depends_on:
+38
-27
eslint.config.mjs
+38
-27
eslint.config.mjs
···
1
1
import eslint from "@eslint/js";
2
2
import stylistic from "@stylistic/eslint-plugin";
3
+
import { defineConfig } from "eslint/config";
3
4
import prettier from "eslint-config-prettier";
4
5
import importPlugin from "eslint-plugin-import";
5
6
import tseslint from "typescript-eslint";
6
7
7
-
export default tseslint.config(
8
+
export default defineConfig(
8
9
eslint.configs.recommended,
9
10
...tseslint.configs.strictTypeChecked,
10
11
...tseslint.configs.stylisticTypeChecked,
···
25
26
rules: {
26
27
// TypeScript specific rules
27
28
"@typescript-eslint/no-unused-vars": [
28
-
"error",
29
+
"warn",
29
30
{ argsIgnorePattern: "^_" },
30
31
],
31
-
"@typescript-eslint/no-explicit-any": "error",
32
+
"@typescript-eslint/no-explicit-any": "warn",
32
33
"@typescript-eslint/no-unsafe-assignment": "error",
33
34
"@typescript-eslint/no-unsafe-member-access": "error",
34
35
"@typescript-eslint/no-unsafe-call": "error",
35
36
"@typescript-eslint/no-unsafe-return": "error",
36
37
"@typescript-eslint/no-unsafe-argument": "error",
37
-
"@typescript-eslint/prefer-nullish-coalescing": "error",
38
-
"@typescript-eslint/prefer-optional-chain": "error",
38
+
"@typescript-eslint/prefer-nullish-coalescing": "warn",
39
+
"@typescript-eslint/prefer-optional-chain": "warn",
39
40
"@typescript-eslint/no-non-null-assertion": "error",
40
41
"@typescript-eslint/consistent-type-imports": "error",
41
42
"@typescript-eslint/consistent-type-exports": "error",
···
45
46
"no-console": "warn",
46
47
"no-debugger": "error",
47
48
"no-var": "error",
48
-
"prefer-const": "error",
49
-
"prefer-template": "error",
50
-
"object-shorthand": "error",
51
-
"prefer-destructuring": ["error", { object: true, array: false }],
49
+
"prefer-const": "warn",
50
+
"prefer-template": "warn",
51
+
"object-shorthand": "warn",
52
+
"prefer-destructuring": ["warn", { object: true, array: false }],
52
53
53
54
// Import rules
54
55
"import/order": [
55
-
"error",
56
+
"warn",
56
57
{
57
58
groups: [
58
59
"builtin",
···
62
63
"sibling",
63
64
"index",
64
65
],
65
-
"newlines-between": "always",
66
+
pathGroups: [
67
+
{
68
+
pattern: "@atproto/**",
69
+
group: "external",
70
+
position: "after",
71
+
},
72
+
{
73
+
pattern: "@skyware/**",
74
+
group: "external",
75
+
position: "after",
76
+
},
77
+
{
78
+
pattern: "@clavata/**",
79
+
group: "external",
80
+
position: "after",
81
+
},
82
+
],
83
+
pathGroupsExcludedImportTypes: ["builtin"],
84
+
"newlines-between": "never",
66
85
alphabetize: { order: "asc", caseInsensitive: true },
67
86
},
68
87
],
69
-
"import/no-duplicates": "error",
88
+
"import/no-duplicates": "warn",
70
89
"import/no-unresolved": "off", // TypeScript handles this
71
90
72
91
// Security-focused rules
···
81
100
"no-unreachable": "error",
82
101
"no-unreachable-loop": "error",
83
102
84
-
// Style preferences
85
-
"@stylistic/indent": ["error", 2],
86
-
"@stylistic/quotes": ["error", "double"],
87
-
"@stylistic/semi": ["error", "always"],
88
-
//"@stylistic/comma-dangle": ["error", "es5"],
89
-
"@stylistic/object-curly-spacing": ["error", "always"],
90
-
"@stylistic/array-bracket-spacing": ["error", "never"],
91
-
"@stylistic/space-before-function-paren": [
92
-
"error",
93
-
{
94
-
anonymous: "always",
95
-
named: "never",
96
-
asyncArrow: "always",
97
-
},
98
-
],
103
+
// Style preferences (prettier handles these)
104
+
"@stylistic/indent": "off",
105
+
"@stylistic/quotes": "off",
106
+
"@stylistic/semi": "off",
107
+
"@stylistic/object-curly-spacing": "off",
108
+
"@stylistic/array-bracket-spacing": "off",
109
+
"@stylistic/space-before-function-paren": "off",
99
110
},
100
111
},
101
112
{
+3
-3
prometheus.yml
+3
-3
prometheus.yml
+5
-1
src/accountThreshold.ts
+5
-1
src/accountThreshold.ts
···
133
133
const shouldLabel = config.toLabel !== false;
134
134
135
135
if (shouldLabel) {
136
-
await createAccountLabel(did, config.accountLabel, config.accountComment);
136
+
await createAccountLabel(
137
+
did,
138
+
config.accountLabel,
139
+
config.accountComment,
140
+
);
137
141
accountLabelsThresholdAppliedCounter.inc({
138
142
account_label: config.accountLabel,
139
143
action: "label",
+4
-2
src/agent.ts
+4
-2
src/agent.ts
···
1
1
import { Agent, setGlobalDispatcher } from "undici";
2
2
import { AtpAgent } from "@atproto/api";
3
3
import { BSKY_HANDLE, BSKY_PASSWORD, OZONE_PDS } from "./config.js";
4
-
import { loadSession, saveSession, type SessionData } from "./session.js";
5
4
import { updateRateLimitState } from "./limits.js";
6
5
import { logger } from "./logger.js";
6
+
import { type SessionData, loadSession, saveSession } from "./session.js";
7
7
8
8
setGlobalDispatcher(
9
9
new Agent({
···
64
64
}
65
65
66
66
const refreshIn = JWT_LIFETIME_MS * REFRESH_AT_PERCENT;
67
-
logger.debug(`Scheduling session refresh in ${(refreshIn / 1000 / 60).toFixed(1)} minutes`);
67
+
logger.debug(
68
+
`Scheduling session refresh in ${(refreshIn / 1000 / 60).toFixed(1)} minutes`,
69
+
);
68
70
69
71
refreshTimer = setTimeout(() => {
70
72
refreshSession().catch((error) => {
+3
-3
src/limits.ts
+3
-3
src/limits.ts
···
1
1
import { pRateLimit } from "p-ratelimit";
2
-
import { logger } from "./logger.js";
3
2
import { Counter, Gauge, Histogram } from "prom-client";
3
+
import { logger } from "./logger.js";
4
4
5
5
interface RateLimitState {
6
6
limit: number;
···
76
76
remaining: rateLimitState.remaining,
77
77
resetIn: rateLimitState.reset - Math.floor(Date.now() / 1000),
78
78
},
79
-
"Rate limit state updated"
79
+
"Rate limit state updated",
80
80
);
81
81
}
82
82
···
93
93
94
94
if (delayMs > 0) {
95
95
logger.warn(
96
-
`Rate limit critical (${state.remaining}/${state.limit} remaining). Waiting ${delaySeconds}s until reset...`
96
+
`Rate limit critical (${state.remaining}/${state.limit} remaining). Waiting ${delaySeconds}s until reset...`,
97
97
);
98
98
99
99
const waitStart = Date.now();
-2
src/moderation.ts
-2
src/moderation.ts
+6
-3
src/rules/account/age.ts
+6
-3
src/rules/account/age.ts
···
1
+
import { ACCOUNT_AGE_CHECKS } from "../../../rules/accountAge.js";
2
+
import { GLOBAL_ALLOW } from "../../../rules/constants.js";
3
+
import {
4
+
checkAccountLabels,
5
+
createAccountLabel,
6
+
} from "../../accountModeration.js";
1
7
import { agent, isLoggedIn } from "../../agent.js";
2
8
import { PLC_URL } from "../../config.js";
3
-
import { GLOBAL_ALLOW } from "../../../rules/constants.js";
4
9
import { logger } from "../../logger.js";
5
-
import { checkAccountLabels, createAccountLabel } from "../../accountModeration.js";
6
-
import { ACCOUNT_AGE_CHECKS } from "../../../rules/accountAge.js";
7
10
8
11
interface InteractionContext {
9
12
// For replies
+1
-1
src/rules/account/countStarterPacks.ts
+1
-1
src/rules/account/countStarterPacks.ts
···
1
+
import { createAccountLabel } from "../../accountModeration.js";
1
2
import { agent, isLoggedIn } from "../../agent.js";
2
3
import { limit } from "../../limits.js";
3
4
import { logger } from "../../logger.js";
4
-
import { createAccountLabel } from "../../accountModeration.js";
5
5
6
6
const ALLOWED_DIDS = ["did:plc:gpunjjgvlyb4racypz3yfiq4"];
7
7
+6
-3
src/rules/account/tests/age.test.ts
+6
-3
src/rules/account/tests/age.test.ts
···
1
1
import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
import { ACCOUNT_AGE_CHECKS } from "../../../../rules/accountAge.js";
3
+
import { GLOBAL_ALLOW } from "../../../../rules/constants.js";
4
+
import {
5
+
checkAccountLabels,
6
+
createAccountLabel,
7
+
} from "../../../accountModeration.js";
2
8
import { agent } from "../../../agent.js";
3
-
import { GLOBAL_ALLOW } from "../../../../rules/constants.js";
4
9
import { logger } from "../../../logger.js";
5
-
import { checkAccountLabels, createAccountLabel } from "../../../accountModeration.js";
6
10
import {
7
11
calculateAccountAge,
8
12
checkAccountAge,
9
13
getAccountCreationDate,
10
14
} from "../age.js";
11
-
import { ACCOUNT_AGE_CHECKS } from "../../../../rules/accountAge.js";
12
15
13
16
// Mock dependencies
14
17
vi.mock("../../../agent.js", () => ({
+1
-1
src/rules/account/tests/countStarterPacks.test.ts
+1
-1
src/rules/account/tests/countStarterPacks.test.ts
···
1
1
import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
import { createAccountLabel } from "../../../accountModeration.js";
2
3
import { agent } from "../../../agent.js";
3
4
import { limit } from "../../../limits.js";
4
5
import { logger } from "../../../logger.js";
5
-
import { createAccountLabel } from "../../../accountModeration.js";
6
6
import { countStarterPacks } from "../countStarterPacks.js";
7
7
8
8
// Mock dependencies
+1
-1
src/rules/facets/facets.ts
+1
-1
src/rules/facets/facets.ts
+1
-1
src/rules/facets/tests/facets.test.ts
+1
-1
src/rules/facets/tests/facets.test.ts
···
1
1
import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
import { logger } from "../../../logger.js";
3
2
import { createAccountLabel } from "../../../accountModeration.js";
3
+
import { logger } from "../../../logger.js";
4
4
import { Facet } from "../../../types.js";
5
5
import {
6
6
FACET_SPAM_ALLOWLIST,
+2
-2
src/rules/handles/checkHandles.ts
+2
-2
src/rules/handles/checkHandles.ts
···
1
1
import { GLOBAL_ALLOW } from "../../../rules/constants.js";
2
-
import { logger } from "../../logger.js";
2
+
import { HANDLE_CHECKS } from "../../../rules/handles.js";
3
3
import {
4
4
createAccountComment,
5
5
createAccountLabel,
6
6
createAccountReport,
7
7
} from "../../accountModeration.js";
8
-
import { HANDLE_CHECKS } from "../../../rules/handles.js";
8
+
import { logger } from "../../logger.js";
9
9
10
10
export const checkHandle = async (
11
11
did: string,
+2
-2
src/rules/profiles/checkProfiles.ts
+2
-2
src/rules/profiles/checkProfiles.ts
···
1
1
import { GLOBAL_ALLOW } from "../../../rules/constants.js";
2
-
import { logger } from "../../logger.js";
2
+
import { PROFILE_CHECKS } from "../../../rules/profiles.js";
3
3
import {
4
4
createAccountComment,
5
5
createAccountLabel,
6
6
createAccountReport,
7
7
} from "../../accountModeration.js";
8
-
import { PROFILE_CHECKS } from "../../../rules/profiles.js";
8
+
import { logger } from "../../logger.js";
9
9
import { getLanguage } from "../../utils/getLanguage.js";
10
10
11
11
export const checkDescription = async (
+1
-2
src/rules/profiles/tests/checkProfiles.test.ts
+1
-2
src/rules/profiles/tests/checkProfiles.test.ts
···
1
1
import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
import { logger } from "../../../logger.js";
3
2
import {
4
3
createAccountComment,
5
4
createAccountLabel,
6
5
createAccountReport,
7
6
} from "../../../accountModeration.js";
7
+
import { logger } from "../../../logger.js";
8
8
import { getLanguage } from "../../../utils/getLanguage.js";
9
9
import { checkDescription, checkDisplayName } from "../checkProfiles.js";
10
10
···
357
357
expect.any(String),
358
358
);
359
359
});
360
-
361
360
});
362
361
});
363
362
+11
-2
src/session.ts
+11
-2
src/session.ts
···
1
-
import { readFileSync, writeFileSync, unlinkSync, chmodSync, existsSync } from "node:fs";
1
+
import {
2
+
chmodSync,
3
+
existsSync,
4
+
readFileSync,
5
+
unlinkSync,
6
+
writeFileSync,
7
+
} from "node:fs";
2
8
import { join } from "node:path";
3
9
import { logger } from "./logger.js";
4
10
···
34
40
logger.info("Loaded existing session from file");
35
41
return session;
36
42
} catch (error) {
37
-
logger.error({ error }, "Failed to load session file, will authenticate fresh");
43
+
logger.error(
44
+
{ error },
45
+
"Failed to load session file, will authenticate fresh",
46
+
);
38
47
return null;
39
48
}
40
49
}
+19
-20
src/tests/accountThreshold.test.ts
+19
-20
src/tests/accountThreshold.test.ts
···
1
1
import { afterEach, describe, expect, it, vi } from "vitest";
2
+
import {
3
+
createAccountComment,
4
+
createAccountLabel,
5
+
createAccountReport,
6
+
} from "../accountModeration.js";
7
+
import {
8
+
checkAccountThreshold,
9
+
loadThresholdConfigs,
10
+
} from "../accountThreshold.js";
11
+
import { logger } from "../logger.js";
12
+
import {
13
+
accountLabelsThresholdAppliedCounter,
14
+
accountThresholdChecksCounter,
15
+
accountThresholdMetCounter,
16
+
} from "../metrics.js";
17
+
import {
18
+
getPostLabelCountInWindow,
19
+
trackPostLabelForAccount,
20
+
} from "../redis.js";
2
21
3
22
vi.mock("../logger.js", () => ({
4
23
logger: {
···
76
95
inc: vi.fn(),
77
96
},
78
97
}));
79
-
80
-
import {
81
-
checkAccountThreshold,
82
-
loadThresholdConfigs,
83
-
} from "../accountThreshold.js";
84
-
import { logger } from "../logger.js";
85
-
import {
86
-
accountLabelsThresholdAppliedCounter,
87
-
accountThresholdChecksCounter,
88
-
accountThresholdMetCounter,
89
-
} from "../metrics.js";
90
-
import {
91
-
createAccountComment,
92
-
createAccountLabel,
93
-
createAccountReport,
94
-
} from "../accountModeration.js";
95
-
import {
96
-
getPostLabelCountInWindow,
97
-
trackPostLabelForAccount,
98
-
} from "../redis.js";
99
98
100
99
describe("Account Threshold Logic", () => {
101
100
afterEach(() => {
+55
-27
src/tests/moderation.test.ts
+55
-27
src/tests/moderation.test.ts
···
1
1
import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
// --- Imports Second ---
3
+
import { checkAccountLabels } from "../accountModeration.js";
4
+
import { agent } from "../agent.js";
5
+
import { logger } from "../logger.js";
6
+
import { createPostLabel } from "../moderation.js";
7
+
import { tryClaimPostLabel } from "../redis.js";
2
8
3
9
// --- Mocks First ---
4
10
···
39
45
limit: vi.fn((fn) => fn()),
40
46
}));
41
47
42
-
// --- Imports Second ---
43
-
44
-
import { checkAccountLabels } from "../accountModeration.js";
45
-
import { agent } from "../agent.js";
46
-
import { logger } from "../logger.js";
47
-
import { createPostLabel } from "../moderation.js";
48
-
import { tryClaimPostLabel } from "../redis.js";
49
-
50
48
describe("Moderation Logic", () => {
51
49
beforeEach(() => {
52
50
vi.clearAllMocks();
···
57
55
vi.mocked(agent.tools.ozone.moderation.getRepo).mockResolvedValueOnce({
58
56
data: {
59
57
labels: [
60
-
{ val: "spam", src: "did:plc:test", uri: "at://test", cts: "2024-01-01T00:00:00Z" },
61
-
{ val: "window-reply", src: "did:plc:test", uri: "at://test", cts: "2024-01-01T00:00:00Z" }
62
-
]
58
+
{
59
+
val: "spam",
60
+
src: "did:plc:test",
61
+
uri: "at://test",
62
+
cts: "2024-01-01T00:00:00Z",
63
+
},
64
+
{
65
+
val: "window-reply",
66
+
src: "did:plc:test",
67
+
uri: "at://test",
68
+
cts: "2024-01-01T00:00:00Z",
69
+
},
70
+
],
63
71
},
64
72
} as any);
65
-
const result = await checkAccountLabels("did:plc:test123", "window-reply");
73
+
const result = await checkAccountLabels(
74
+
"did:plc:test123",
75
+
"window-reply",
76
+
);
66
77
expect(result).toBe(true);
67
78
});
68
79
});
···
79
90
await createPostLabel(URI, CID, LABEL, COMMENT, undefined);
80
91
81
92
expect(vi.mocked(tryClaimPostLabel)).toHaveBeenCalledWith(URI, LABEL);
82
-
expect(vi.mocked(agent.tools.ozone.moderation.getRecord)).not.toHaveBeenCalled();
83
-
expect(vi.mocked(agent.tools.ozone.moderation.emitEvent)).not.toHaveBeenCalled();
93
+
expect(
94
+
vi.mocked(agent.tools.ozone.moderation.getRecord),
95
+
).not.toHaveBeenCalled();
96
+
expect(
97
+
vi.mocked(agent.tools.ozone.moderation.emitEvent),
98
+
).not.toHaveBeenCalled();
84
99
});
85
100
86
101
it("should skip event if claimed but already labeled via API", async () => {
87
102
vi.mocked(tryClaimPostLabel).mockResolvedValue(true);
88
103
vi.mocked(agent.tools.ozone.moderation.getRecord).mockResolvedValue({
89
-
data: { labels: [{ val: LABEL, src: "did:plc:test", uri: URI, cts: "2024-01-01T00:00:00Z" }] },
104
+
data: {
105
+
labels: [
106
+
{
107
+
val: LABEL,
108
+
src: "did:plc:test",
109
+
uri: URI,
110
+
cts: "2024-01-01T00:00:00Z",
111
+
},
112
+
],
113
+
},
90
114
} as any);
91
115
92
116
await createPostLabel(URI, CID, LABEL, COMMENT, undefined);
93
117
94
118
expect(vi.mocked(tryClaimPostLabel)).toHaveBeenCalledWith(URI, LABEL);
95
-
expect(vi.mocked(agent.tools.ozone.moderation.getRecord)).toHaveBeenCalledWith(
96
-
{ uri: URI },
97
-
expect.any(Object),
98
-
);
99
-
expect(vi.mocked(agent.tools.ozone.moderation.emitEvent)).not.toHaveBeenCalled();
119
+
expect(
120
+
vi.mocked(agent.tools.ozone.moderation.getRecord),
121
+
).toHaveBeenCalledWith({ uri: URI }, expect.any(Object));
122
+
expect(
123
+
vi.mocked(agent.tools.ozone.moderation.emitEvent),
124
+
).not.toHaveBeenCalled();
100
125
});
101
126
102
127
it("should emit event if claimed and not labeled anywhere", async () => {
···
104
129
vi.mocked(agent.tools.ozone.moderation.getRecord).mockResolvedValue({
105
130
data: { labels: [] },
106
131
} as any);
107
-
vi.mocked(agent.tools.ozone.moderation.emitEvent).mockResolvedValue({ success: true } as any);
132
+
vi.mocked(agent.tools.ozone.moderation.emitEvent).mockResolvedValue({
133
+
success: true,
134
+
} as any);
108
135
109
136
await createPostLabel(URI, CID, LABEL, COMMENT, undefined);
110
137
111
138
expect(vi.mocked(tryClaimPostLabel)).toHaveBeenCalledWith(URI, LABEL);
112
-
expect(vi.mocked(agent.tools.ozone.moderation.getRecord)).toHaveBeenCalledWith(
113
-
{ uri: URI },
114
-
expect.any(Object),
115
-
);
116
-
expect(vi.mocked(agent.tools.ozone.moderation.emitEvent)).toHaveBeenCalled();
139
+
expect(
140
+
vi.mocked(agent.tools.ozone.moderation.getRecord),
141
+
).toHaveBeenCalledWith({ uri: URI }, expect.any(Object));
142
+
expect(
143
+
vi.mocked(agent.tools.ozone.moderation.emitEvent),
144
+
).toHaveBeenCalled();
117
145
});
118
146
});
119
-
});
147
+
});
+88
-74
src/tests/redis.test.ts
+88
-74
src/tests/redis.test.ts
···
1
-
import { afterEach, describe, expect, it, vi } from 'vitest';
1
+
// Import the mocked redis first to get a reference to the mock client
2
+
import { createClient } from "redis";
3
+
import { afterEach, describe, expect, it, vi } from "vitest";
4
+
import { logger } from "../logger.js";
5
+
// Import the modules to be tested
6
+
import {
7
+
connectRedis,
8
+
disconnectRedis,
9
+
getPostLabelCountInWindow,
10
+
trackPostLabelForAccount,
11
+
tryClaimAccountLabel,
12
+
tryClaimPostLabel,
13
+
} from "../redis.js";
2
14
3
15
// Mock the 'redis' module in a way that avoids hoisting issues.
4
16
// The mock implementation is self-contained.
5
-
vi.mock('redis', () => {
17
+
vi.mock("redis", () => {
6
18
const mockClient = {
7
19
on: vi.fn(),
8
20
connect: vi.fn(),
···
19
31
};
20
32
});
21
33
22
-
// Import the mocked redis first to get a reference to the mock client
23
-
import { createClient } from 'redis';
24
34
const mockRedisClient = createClient();
25
35
26
-
// Import the modules to be tested
27
-
import {
28
-
tryClaimPostLabel,
29
-
tryClaimAccountLabel,
30
-
connectRedis,
31
-
disconnectRedis,
32
-
trackPostLabelForAccount,
33
-
getPostLabelCountInWindow,
34
-
} from '../redis.js';
35
-
import { logger } from '../logger.js';
36
-
37
36
// Suppress logger output during tests
38
-
vi.mock('../logger.js', () => ({
37
+
vi.mock("../logger.js", () => ({
39
38
logger: {
40
39
info: vi.fn(),
41
40
warn: vi.fn(),
···
44
43
},
45
44
}));
46
45
47
-
describe('Redis Cache Logic', () => {
46
+
describe("Redis Cache Logic", () => {
48
47
afterEach(() => {
49
48
vi.clearAllMocks();
50
49
});
51
50
52
-
describe('Connection', () => {
53
-
it('should call redisClient.connect on connectRedis', async () => {
51
+
describe("Connection", () => {
52
+
it("should call redisClient.connect on connectRedis", async () => {
54
53
await connectRedis();
55
54
expect(mockRedisClient.connect).toHaveBeenCalled();
56
55
});
57
56
58
-
it('should call redisClient.quit on disconnectRedis', async () => {
57
+
it("should call redisClient.quit on disconnectRedis", async () => {
59
58
await disconnectRedis();
60
59
expect(mockRedisClient.quit).toHaveBeenCalled();
61
60
});
62
61
});
63
62
64
-
describe('tryClaimPostLabel', () => {
65
-
it('should return true and set key if key does not exist', async () => {
66
-
vi.mocked(mockRedisClient.set).mockResolvedValue('OK');
67
-
const result = await tryClaimPostLabel('at://uri', 'test-label');
63
+
describe("tryClaimPostLabel", () => {
64
+
it("should return true and set key if key does not exist", async () => {
65
+
vi.mocked(mockRedisClient.set).mockResolvedValue("OK");
66
+
const result = await tryClaimPostLabel("at://uri", "test-label");
68
67
expect(result).toBe(true);
69
68
expect(mockRedisClient.set).toHaveBeenCalledWith(
70
-
'post-label:at://uri:test-label',
71
-
'1',
72
-
{ NX: true, EX: 60 * 60 * 24 * 7 }
69
+
"post-label:at://uri:test-label",
70
+
"1",
71
+
{ NX: true, EX: 60 * 60 * 24 * 7 },
73
72
);
74
73
});
75
74
76
-
it('should return false if key already exists', async () => {
75
+
it("should return false if key already exists", async () => {
77
76
vi.mocked(mockRedisClient.set).mockResolvedValue(null);
78
-
const result = await tryClaimPostLabel('at://uri', 'test-label');
77
+
const result = await tryClaimPostLabel("at://uri", "test-label");
79
78
expect(result).toBe(false);
80
79
});
81
80
82
-
it('should return true and log warning on Redis error', async () => {
83
-
const redisError = new Error('Redis down');
81
+
it("should return true and log warning on Redis error", async () => {
82
+
const redisError = new Error("Redis down");
84
83
vi.mocked(mockRedisClient.set).mockRejectedValue(redisError);
85
-
const result = await tryClaimPostLabel('at://uri', 'test-label');
84
+
const result = await tryClaimPostLabel("at://uri", "test-label");
86
85
expect(result).toBe(true);
87
86
expect(logger.warn).toHaveBeenCalledWith(
88
-
{ err: redisError, atURI: 'at://uri', label: 'test-label' },
89
-
'Error claiming post label in Redis, allowing through'
87
+
{ err: redisError, atURI: "at://uri", label: "test-label" },
88
+
"Error claiming post label in Redis, allowing through",
90
89
);
91
90
});
92
91
});
93
92
94
-
describe('tryClaimAccountLabel', () => {
95
-
it('should return true and set key if key does not exist', async () => {
96
-
vi.mocked(mockRedisClient.set).mockResolvedValue('OK');
97
-
const result = await tryClaimAccountLabel('did:plc:123', 'test-label');
93
+
describe("tryClaimAccountLabel", () => {
94
+
it("should return true and set key if key does not exist", async () => {
95
+
vi.mocked(mockRedisClient.set).mockResolvedValue("OK");
96
+
const result = await tryClaimAccountLabel("did:plc:123", "test-label");
98
97
expect(result).toBe(true);
99
98
expect(mockRedisClient.set).toHaveBeenCalledWith(
100
-
'account-label:did:plc:123:test-label',
101
-
'1',
102
-
{ NX: true, EX: 60 * 60 * 24 * 7 }
99
+
"account-label:did:plc:123:test-label",
100
+
"1",
101
+
{ NX: true, EX: 60 * 60 * 24 * 7 },
103
102
);
104
103
});
105
104
106
-
it('should return false if key already exists', async () => {
105
+
it("should return false if key already exists", async () => {
107
106
vi.mocked(mockRedisClient.set).mockResolvedValue(null);
108
-
const result = await tryClaimAccountLabel('did:plc:123', 'test-label');
107
+
const result = await tryClaimAccountLabel("did:plc:123", "test-label");
109
108
expect(result).toBe(false);
110
109
});
111
110
});
112
111
113
-
describe('trackPostLabelForAccount', () => {
114
-
it('should track post label with correct timestamp and TTL', async () => {
112
+
describe("trackPostLabelForAccount", () => {
113
+
it("should track post label with correct timestamp and TTL", async () => {
115
114
vi.mocked(mockRedisClient.zRemRangeByScore).mockResolvedValue(0);
116
115
vi.mocked(mockRedisClient.zAdd).mockResolvedValue(1);
117
116
vi.mocked(mockRedisClient.expire).mockResolvedValue(true);
···
119
118
const timestamp = 1640000000000000; // microseconds
120
119
const windowDays = 5;
121
120
122
-
await trackPostLabelForAccount('did:plc:123', 'test-label', timestamp, windowDays);
121
+
await trackPostLabelForAccount(
122
+
"did:plc:123",
123
+
"test-label",
124
+
timestamp,
125
+
windowDays,
126
+
);
123
127
124
-
const expectedKey = 'account-post-labels:did:plc:123:test-label:5';
128
+
const expectedKey = "account-post-labels:did:plc:123:test-label:5";
125
129
const windowStartTime = timestamp - windowDays * 24 * 60 * 60 * 1000000;
126
130
127
131
expect(mockRedisClient.zRemRangeByScore).toHaveBeenCalledWith(
128
132
expectedKey,
129
-
'-inf',
130
-
windowStartTime
133
+
"-inf",
134
+
windowStartTime,
131
135
);
132
136
expect(mockRedisClient.zAdd).toHaveBeenCalledWith(expectedKey, {
133
137
score: timestamp,
···
135
139
});
136
140
expect(mockRedisClient.expire).toHaveBeenCalledWith(
137
141
expectedKey,
138
-
(windowDays + 1) * 24 * 60 * 60
142
+
(windowDays + 1) * 24 * 60 * 60,
139
143
);
140
144
});
141
145
142
-
it('should throw error on Redis failure', async () => {
143
-
const redisError = new Error('Redis down');
146
+
it("should throw error on Redis failure", async () => {
147
+
const redisError = new Error("Redis down");
144
148
vi.mocked(mockRedisClient.zRemRangeByScore).mockRejectedValue(redisError);
145
149
146
150
await expect(
147
-
trackPostLabelForAccount('did:plc:123', 'test-label', 1640000000000000, 5)
148
-
).rejects.toThrow('Redis down');
151
+
trackPostLabelForAccount(
152
+
"did:plc:123",
153
+
"test-label",
154
+
1640000000000000,
155
+
5,
156
+
),
157
+
).rejects.toThrow("Redis down");
149
158
150
159
expect(logger.error).toHaveBeenCalled();
151
160
});
152
161
});
153
162
154
-
describe('getPostLabelCountInWindow', () => {
155
-
it('should count posts for single label', async () => {
163
+
describe("getPostLabelCountInWindow", () => {
164
+
it("should count posts for single label", async () => {
156
165
vi.mocked(mockRedisClient.zCount).mockResolvedValue(3);
157
166
158
167
const currentTime = 1640000000000000;
159
168
const windowDays = 5;
160
169
const count = await getPostLabelCountInWindow(
161
-
'did:plc:123',
162
-
['test-label'],
170
+
"did:plc:123",
171
+
["test-label"],
163
172
windowDays,
164
-
currentTime
173
+
currentTime,
165
174
);
166
175
167
176
expect(count).toBe(3);
168
177
const windowStartTime = currentTime - windowDays * 24 * 60 * 60 * 1000000;
169
178
expect(mockRedisClient.zCount).toHaveBeenCalledWith(
170
-
'account-post-labels:did:plc:123:test-label:5',
179
+
"account-post-labels:did:plc:123:test-label:5",
171
180
windowStartTime,
172
-
'+inf'
181
+
"+inf",
173
182
);
174
183
});
175
184
176
-
it('should sum counts for multiple labels (OR logic)', async () => {
185
+
it("should sum counts for multiple labels (OR logic)", async () => {
177
186
vi.mocked(mockRedisClient.zCount)
178
187
.mockResolvedValueOnce(3)
179
188
.mockResolvedValueOnce(2)
···
182
191
const currentTime = 1640000000000000;
183
192
const windowDays = 5;
184
193
const count = await getPostLabelCountInWindow(
185
-
'did:plc:123',
186
-
['label-1', 'label-2', 'label-3'],
194
+
"did:plc:123",
195
+
["label-1", "label-2", "label-3"],
187
196
windowDays,
188
-
currentTime
197
+
currentTime,
189
198
);
190
199
191
200
expect(count).toBe(6);
192
201
expect(mockRedisClient.zCount).toHaveBeenCalledTimes(3);
193
202
});
194
203
195
-
it('should return 0 when no posts in window', async () => {
204
+
it("should return 0 when no posts in window", async () => {
196
205
vi.mocked(mockRedisClient.zCount).mockResolvedValue(0);
197
206
198
207
const count = await getPostLabelCountInWindow(
199
-
'did:plc:123',
200
-
['test-label'],
208
+
"did:plc:123",
209
+
["test-label"],
201
210
5,
202
-
1640000000000000
211
+
1640000000000000,
203
212
);
204
213
205
214
expect(count).toBe(0);
206
215
});
207
216
208
-
it('should throw error on Redis failure', async () => {
209
-
const redisError = new Error('Redis down');
217
+
it("should throw error on Redis failure", async () => {
218
+
const redisError = new Error("Redis down");
210
219
vi.mocked(mockRedisClient.zCount).mockRejectedValue(redisError);
211
220
212
221
await expect(
213
-
getPostLabelCountInWindow('did:plc:123', ['test-label'], 5, 1640000000000000)
214
-
).rejects.toThrow('Redis down');
222
+
getPostLabelCountInWindow(
223
+
"did:plc:123",
224
+
["test-label"],
225
+
5,
226
+
1640000000000000,
227
+
),
228
+
).rejects.toThrow("Redis down");
215
229
216
230
expect(logger.error).toHaveBeenCalled();
217
231
});
218
232
});
219
-
});
233
+
});