+20
-1
src/rules/account/age.ts
+20
-1
src/rules/account/age.ts
···
1
1
import { agent, isLoggedIn } from "../../agent.js";
2
2
import { logger } from "../../logger.js";
3
-
import { createAccountLabel } from "../../moderation.js";
3
+
import { createAccountLabel, checkAccountLabels } from "../../moderation.js";
4
4
import { ACCOUNT_AGE_CHECKS } from "./ageConstants.js";
5
5
import { PLC_URL } from "../../config.js";
6
6
import { GLOBAL_ALLOW } from "../../constants.js";
···
156
156
157
157
// Check if account was created within the window
158
158
if (creationDate >= windowStart && creationDate <= windowEnd) {
159
+
// Check if the label already exists to prevent duplicates
160
+
const labelExists = await checkAccountLabels(
161
+
context.replyingDid,
162
+
check.label,
163
+
);
164
+
165
+
if (labelExists) {
166
+
logger.debug(
167
+
{
168
+
process: "ACCOUNT_AGE",
169
+
replyingDid: context.replyingDid,
170
+
label: check.label,
171
+
},
172
+
"Label already exists, skipping duplicate",
173
+
);
174
+
// Only apply one label per reply
175
+
return;
176
+
}
177
+
159
178
logger.info(
160
179
{
161
180
process: "ACCOUNT_AGE",
+83
-1
src/rules/account/tests/age.test.ts
+83
-1
src/rules/account/tests/age.test.ts
···
25
25
26
26
vi.mock("../../../moderation.js", () => ({
27
27
createAccountLabel: vi.fn(),
28
+
checkAccountLabels: vi.fn(),
28
29
}));
29
30
30
31
vi.mock("../../../constants.js", () => ({
···
36
37
37
38
import { agent } from "../../../agent.js";
38
39
import { logger } from "../../../logger.js";
39
-
import { createAccountLabel } from "../../../moderation.js";
40
+
import {
41
+
createAccountLabel,
42
+
checkAccountLabels,
43
+
} from "../../../moderation.js";
40
44
import { GLOBAL_ALLOW } from "../../../constants.js";
41
45
42
46
describe("Account Age Module", () => {
···
362
366
"did:plc:newaccount",
363
367
"label1",
364
368
expect.any(String),
369
+
);
370
+
});
371
+
372
+
it("should skip labeling if label already exists on account", async () => {
373
+
ACCOUNT_AGE_CHECKS.push({
374
+
monitoredDIDs: ["did:plc:monitored"],
375
+
anchorDate: "2025-10-15",
376
+
maxAgeDays: 7,
377
+
label: "window-reply",
378
+
comment: "Account created in window",
379
+
});
380
+
381
+
// Mock account created within window
382
+
const mockDidDoc = [{ createdAt: "2025-10-18T12:00:00.000Z" }];
383
+
(global.fetch as any).mockResolvedValueOnce({
384
+
ok: true,
385
+
json: async () => mockDidDoc,
386
+
});
387
+
388
+
// Mock that label already exists
389
+
(checkAccountLabels as any).mockResolvedValueOnce(true);
390
+
391
+
await checkAccountAge({
392
+
replyToDid: "did:plc:monitored",
393
+
replyingDid: "did:plc:alreadylabeled",
394
+
atURI: TEST_REPLY_URI,
395
+
time: TEST_TIME,
396
+
});
397
+
398
+
expect(checkAccountLabels).toHaveBeenCalledWith(
399
+
"did:plc:alreadylabeled",
400
+
"window-reply",
401
+
);
402
+
expect(createAccountLabel).not.toHaveBeenCalled();
403
+
expect(logger.debug).toHaveBeenCalledWith(
404
+
{
405
+
process: "ACCOUNT_AGE",
406
+
replyingDid: "did:plc:alreadylabeled",
407
+
label: "window-reply",
408
+
},
409
+
"Label already exists, skipping duplicate",
410
+
);
411
+
});
412
+
413
+
it("should create label if it does not already exist", async () => {
414
+
ACCOUNT_AGE_CHECKS.push({
415
+
monitoredDIDs: ["did:plc:monitored"],
416
+
anchorDate: "2025-10-15",
417
+
maxAgeDays: 7,
418
+
label: "window-reply",
419
+
comment: "Account created in window",
420
+
});
421
+
422
+
// Mock account created within window
423
+
const mockDidDoc = [{ createdAt: "2025-10-18T12:00:00.000Z" }];
424
+
(global.fetch as any).mockResolvedValueOnce({
425
+
ok: true,
426
+
json: async () => mockDidDoc,
427
+
});
428
+
429
+
// Mock that label does NOT exist
430
+
(checkAccountLabels as any).mockResolvedValueOnce(false);
431
+
432
+
await checkAccountAge({
433
+
replyToDid: "did:plc:monitored",
434
+
replyingDid: "did:plc:newlabel",
435
+
atURI: TEST_REPLY_URI,
436
+
time: TEST_TIME,
437
+
});
438
+
439
+
expect(checkAccountLabels).toHaveBeenCalledWith(
440
+
"did:plc:newlabel",
441
+
"window-reply",
442
+
);
443
+
expect(createAccountLabel).toHaveBeenCalledWith(
444
+
"did:plc:newlabel",
445
+
"window-reply",
446
+
expect.stringContaining("Account created within monitored range"),
365
447
);
366
448
});
367
449
+161
src/tests/moderation.test.ts
+161
src/tests/moderation.test.ts
···
1
+
import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
import { checkAccountLabels } from "../moderation.js";
3
+
4
+
// Mock dependencies
5
+
vi.mock("../agent.js", () => ({
6
+
agent: {
7
+
tools: {
8
+
ozone: {
9
+
moderation: {
10
+
getRepo: vi.fn(),
11
+
},
12
+
},
13
+
},
14
+
},
15
+
isLoggedIn: Promise.resolve(true),
16
+
}));
17
+
18
+
vi.mock("../logger.js", () => ({
19
+
logger: {
20
+
info: vi.fn(),
21
+
debug: vi.fn(),
22
+
warn: vi.fn(),
23
+
error: vi.fn(),
24
+
},
25
+
}));
26
+
27
+
vi.mock("../config.js", () => ({
28
+
MOD_DID: "did:plc:moderator123",
29
+
}));
30
+
31
+
vi.mock("../limits.js", () => ({
32
+
limit: vi.fn((fn) => fn()),
33
+
}));
34
+
35
+
import { agent } from "../agent.js";
36
+
import { logger } from "../logger.js";
37
+
38
+
describe("checkAccountLabels", () => {
39
+
beforeEach(() => {
40
+
vi.clearAllMocks();
41
+
});
42
+
43
+
it("should return true if label exists on account", async () => {
44
+
(agent.tools.ozone.moderation.getRepo as any).mockResolvedValueOnce({
45
+
data: {
46
+
labels: [
47
+
{ val: "spam" },
48
+
{ val: "harassment" },
49
+
{ val: "window-reply" },
50
+
],
51
+
},
52
+
});
53
+
54
+
const result = await checkAccountLabels(
55
+
"did:plc:test123",
56
+
"window-reply",
57
+
);
58
+
59
+
expect(result).toBe(true);
60
+
expect(agent.tools.ozone.moderation.getRepo).toHaveBeenCalledWith(
61
+
{ did: "did:plc:test123" },
62
+
{
63
+
headers: {
64
+
"atproto-proxy": "did:plc:moderator123#atproto_labeler",
65
+
"atproto-accept-labelers":
66
+
"did:plc:ar7c4by46qjdydhdevvrndac;redact",
67
+
},
68
+
},
69
+
);
70
+
});
71
+
72
+
it("should return false if label does not exist on account", async () => {
73
+
(agent.tools.ozone.moderation.getRepo as any).mockResolvedValueOnce({
74
+
data: {
75
+
labels: [{ val: "spam" }, { val: "harassment" }],
76
+
},
77
+
});
78
+
79
+
const result = await checkAccountLabels(
80
+
"did:plc:test123",
81
+
"window-reply",
82
+
);
83
+
84
+
expect(result).toBe(false);
85
+
});
86
+
87
+
it("should return false if account has no labels", async () => {
88
+
(agent.tools.ozone.moderation.getRepo as any).mockResolvedValueOnce({
89
+
data: {
90
+
labels: [],
91
+
},
92
+
});
93
+
94
+
const result = await checkAccountLabels(
95
+
"did:plc:test123",
96
+
"window-reply",
97
+
);
98
+
99
+
expect(result).toBe(false);
100
+
});
101
+
102
+
it("should return false if labels property is undefined", async () => {
103
+
(agent.tools.ozone.moderation.getRepo as any).mockResolvedValueOnce({
104
+
data: {},
105
+
});
106
+
107
+
const result = await checkAccountLabels(
108
+
"did:plc:test123",
109
+
"window-reply",
110
+
);
111
+
112
+
expect(result).toBe(false);
113
+
});
114
+
115
+
it("should handle API errors gracefully", async () => {
116
+
(agent.tools.ozone.moderation.getRepo as any).mockRejectedValueOnce(
117
+
new Error("API Error"),
118
+
);
119
+
120
+
const result = await checkAccountLabels(
121
+
"did:plc:test123",
122
+
"window-reply",
123
+
);
124
+
125
+
expect(result).toBe(false);
126
+
expect(logger.error).toHaveBeenCalledWith(
127
+
{
128
+
process: "MODERATION",
129
+
did: "did:plc:test123",
130
+
error: expect.any(Error),
131
+
},
132
+
"Failed to check account labels",
133
+
);
134
+
});
135
+
136
+
it("should perform case-sensitive label matching", async () => {
137
+
(agent.tools.ozone.moderation.getRepo as any).mockResolvedValueOnce({
138
+
data: {
139
+
labels: [{ val: "window-reply" }],
140
+
},
141
+
});
142
+
143
+
const resultLower = await checkAccountLabels(
144
+
"did:plc:test123",
145
+
"window-reply",
146
+
);
147
+
expect(resultLower).toBe(true);
148
+
149
+
(agent.tools.ozone.moderation.getRepo as any).mockResolvedValueOnce({
150
+
data: {
151
+
labels: [{ val: "window-reply" }],
152
+
},
153
+
});
154
+
155
+
const resultUpper = await checkAccountLabels(
156
+
"did:plc:test123",
157
+
"Window-Reply",
158
+
);
159
+
expect(resultUpper).toBe(false);
160
+
});
161
+
});