opencode-swarm-plugin 0.43.0 → 0.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cass.characterization.test.ts +422 -0
- package/bin/swarm.serve.test.ts +6 -4
- package/bin/swarm.test.ts +68 -0
- package/bin/swarm.ts +81 -8
- package/dist/compaction-prompt-scoring.js +139 -0
- package/dist/contributor-tools.d.ts +42 -0
- package/dist/contributor-tools.d.ts.map +1 -0
- package/dist/eval-capture.js +12811 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7728 -62590
- package/dist/plugin.js +23833 -78695
- package/dist/sessions/agent-discovery.d.ts +59 -0
- package/dist/sessions/agent-discovery.d.ts.map +1 -0
- package/dist/sessions/index.d.ts +10 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/package.json +17 -5
- package/.changeset/swarm-insights-data-layer.md +0 -63
- package/.hive/analysis/eval-failure-analysis-2025-12-25.md +0 -331
- package/.hive/analysis/session-data-quality-audit.md +0 -320
- package/.hive/eval-results.json +0 -483
- package/.hive/issues.jsonl +0 -138
- package/.hive/memories.jsonl +0 -729
- package/.opencode/eval-history.jsonl +0 -327
- package/.turbo/turbo-build.log +0 -9
- package/CHANGELOG.md +0 -2255
- package/SCORER-ANALYSIS.md +0 -598
- package/docs/analysis/subagent-coordination-patterns.md +0 -902
- package/docs/analysis-socratic-planner-pattern.md +0 -504
- package/docs/planning/ADR-001-monorepo-structure.md +0 -171
- package/docs/planning/ADR-002-package-extraction.md +0 -393
- package/docs/planning/ADR-003-performance-improvements.md +0 -451
- package/docs/planning/ADR-004-message-queue-features.md +0 -187
- package/docs/planning/ADR-005-devtools-observability.md +0 -202
- package/docs/planning/ADR-007-swarm-enhancements-worktree-review.md +0 -168
- package/docs/planning/ADR-008-worker-handoff-protocol.md +0 -293
- package/docs/planning/ADR-009-oh-my-opencode-patterns.md +0 -353
- package/docs/planning/ROADMAP.md +0 -368
- package/docs/semantic-memory-cli-syntax.md +0 -123
- package/docs/swarm-mail-architecture.md +0 -1147
- package/docs/testing/context-recovery-test.md +0 -470
- package/evals/ARCHITECTURE.md +0 -1189
- package/evals/README.md +0 -768
- package/evals/compaction-prompt.eval.ts +0 -149
- package/evals/compaction-resumption.eval.ts +0 -289
- package/evals/coordinator-behavior.eval.ts +0 -307
- package/evals/coordinator-session.eval.ts +0 -154
- package/evals/evalite.config.ts.bak +0 -15
- package/evals/example.eval.ts +0 -31
- package/evals/fixtures/compaction-cases.ts +0 -350
- package/evals/fixtures/compaction-prompt-cases.ts +0 -311
- package/evals/fixtures/coordinator-sessions.ts +0 -328
- package/evals/fixtures/decomposition-cases.ts +0 -105
- package/evals/lib/compaction-loader.test.ts +0 -248
- package/evals/lib/compaction-loader.ts +0 -320
- package/evals/lib/data-loader.evalite-test.ts +0 -289
- package/evals/lib/data-loader.test.ts +0 -345
- package/evals/lib/data-loader.ts +0 -281
- package/evals/lib/llm.ts +0 -115
- package/evals/scorers/compaction-prompt-scorers.ts +0 -145
- package/evals/scorers/compaction-scorers.ts +0 -305
- package/evals/scorers/coordinator-discipline.evalite-test.ts +0 -539
- package/evals/scorers/coordinator-discipline.ts +0 -325
- package/evals/scorers/index.test.ts +0 -146
- package/evals/scorers/index.ts +0 -328
- package/evals/scorers/outcome-scorers.evalite-test.ts +0 -27
- package/evals/scorers/outcome-scorers.ts +0 -349
- package/evals/swarm-decomposition.eval.ts +0 -121
- package/examples/commands/swarm.md +0 -745
- package/examples/plugin-wrapper-template.ts +0 -2426
- package/examples/skills/hive-workflow/SKILL.md +0 -212
- package/examples/skills/skill-creator/SKILL.md +0 -223
- package/examples/skills/swarm-coordination/SKILL.md +0 -292
- package/global-skills/cli-builder/SKILL.md +0 -344
- package/global-skills/cli-builder/references/advanced-patterns.md +0 -244
- package/global-skills/learning-systems/SKILL.md +0 -644
- package/global-skills/skill-creator/LICENSE.txt +0 -202
- package/global-skills/skill-creator/SKILL.md +0 -352
- package/global-skills/skill-creator/references/output-patterns.md +0 -82
- package/global-skills/skill-creator/references/workflows.md +0 -28
- package/global-skills/swarm-coordination/SKILL.md +0 -995
- package/global-skills/swarm-coordination/references/coordinator-patterns.md +0 -235
- package/global-skills/swarm-coordination/references/strategies.md +0 -138
- package/global-skills/system-design/SKILL.md +0 -213
- package/global-skills/testing-patterns/SKILL.md +0 -430
- package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +0 -586
- package/opencode-swarm-plugin-0.30.7.tgz +0 -0
- package/opencode-swarm-plugin-0.31.0.tgz +0 -0
- package/scripts/cleanup-test-memories.ts +0 -346
- package/scripts/init-skill.ts +0 -222
- package/scripts/migrate-unknown-sessions.ts +0 -349
- package/scripts/validate-skill.ts +0 -204
- package/src/agent-mail.ts +0 -1724
- package/src/anti-patterns.test.ts +0 -1167
- package/src/anti-patterns.ts +0 -448
- package/src/compaction-capture.integration.test.ts +0 -257
- package/src/compaction-hook.test.ts +0 -838
- package/src/compaction-hook.ts +0 -1204
- package/src/compaction-observability.integration.test.ts +0 -139
- package/src/compaction-observability.test.ts +0 -187
- package/src/compaction-observability.ts +0 -324
- package/src/compaction-prompt-scorers.test.ts +0 -475
- package/src/compaction-prompt-scoring.ts +0 -300
- package/src/dashboard.test.ts +0 -611
- package/src/dashboard.ts +0 -462
- package/src/error-enrichment.test.ts +0 -403
- package/src/error-enrichment.ts +0 -219
- package/src/eval-capture.test.ts +0 -1015
- package/src/eval-capture.ts +0 -929
- package/src/eval-gates.test.ts +0 -306
- package/src/eval-gates.ts +0 -218
- package/src/eval-history.test.ts +0 -508
- package/src/eval-history.ts +0 -214
- package/src/eval-learning.test.ts +0 -378
- package/src/eval-learning.ts +0 -360
- package/src/eval-runner.test.ts +0 -223
- package/src/eval-runner.ts +0 -402
- package/src/export-tools.test.ts +0 -476
- package/src/export-tools.ts +0 -257
- package/src/hive.integration.test.ts +0 -2241
- package/src/hive.ts +0 -1628
- package/src/index.ts +0 -935
- package/src/learning.integration.test.ts +0 -1815
- package/src/learning.ts +0 -1079
- package/src/logger.test.ts +0 -189
- package/src/logger.ts +0 -135
- package/src/mandate-promotion.test.ts +0 -473
- package/src/mandate-promotion.ts +0 -239
- package/src/mandate-storage.integration.test.ts +0 -601
- package/src/mandate-storage.test.ts +0 -578
- package/src/mandate-storage.ts +0 -794
- package/src/mandates.ts +0 -540
- package/src/memory-tools.test.ts +0 -195
- package/src/memory-tools.ts +0 -344
- package/src/memory.integration.test.ts +0 -334
- package/src/memory.test.ts +0 -158
- package/src/memory.ts +0 -527
- package/src/model-selection.test.ts +0 -188
- package/src/model-selection.ts +0 -68
- package/src/observability-tools.test.ts +0 -359
- package/src/observability-tools.ts +0 -871
- package/src/output-guardrails.test.ts +0 -438
- package/src/output-guardrails.ts +0 -381
- package/src/pattern-maturity.test.ts +0 -1160
- package/src/pattern-maturity.ts +0 -525
- package/src/planning-guardrails.test.ts +0 -491
- package/src/planning-guardrails.ts +0 -438
- package/src/plugin.ts +0 -23
- package/src/post-compaction-tracker.test.ts +0 -251
- package/src/post-compaction-tracker.ts +0 -237
- package/src/query-tools.test.ts +0 -636
- package/src/query-tools.ts +0 -324
- package/src/rate-limiter.integration.test.ts +0 -466
- package/src/rate-limiter.ts +0 -774
- package/src/replay-tools.test.ts +0 -496
- package/src/replay-tools.ts +0 -240
- package/src/repo-crawl.integration.test.ts +0 -441
- package/src/repo-crawl.ts +0 -610
- package/src/schemas/cell-events.test.ts +0 -347
- package/src/schemas/cell-events.ts +0 -807
- package/src/schemas/cell.ts +0 -257
- package/src/schemas/evaluation.ts +0 -166
- package/src/schemas/index.test.ts +0 -199
- package/src/schemas/index.ts +0 -286
- package/src/schemas/mandate.ts +0 -232
- package/src/schemas/swarm-context.ts +0 -115
- package/src/schemas/task.ts +0 -161
- package/src/schemas/worker-handoff.test.ts +0 -302
- package/src/schemas/worker-handoff.ts +0 -131
- package/src/skills.integration.test.ts +0 -1192
- package/src/skills.test.ts +0 -643
- package/src/skills.ts +0 -1549
- package/src/storage.integration.test.ts +0 -341
- package/src/storage.ts +0 -884
- package/src/structured.integration.test.ts +0 -817
- package/src/structured.test.ts +0 -1046
- package/src/structured.ts +0 -762
- package/src/swarm-decompose.test.ts +0 -188
- package/src/swarm-decompose.ts +0 -1302
- package/src/swarm-deferred.integration.test.ts +0 -157
- package/src/swarm-deferred.test.ts +0 -38
- package/src/swarm-insights.test.ts +0 -214
- package/src/swarm-insights.ts +0 -459
- package/src/swarm-mail.integration.test.ts +0 -970
- package/src/swarm-mail.ts +0 -739
- package/src/swarm-orchestrate.integration.test.ts +0 -282
- package/src/swarm-orchestrate.test.ts +0 -548
- package/src/swarm-orchestrate.ts +0 -3084
- package/src/swarm-prompts.test.ts +0 -1270
- package/src/swarm-prompts.ts +0 -2077
- package/src/swarm-research.integration.test.ts +0 -701
- package/src/swarm-research.test.ts +0 -698
- package/src/swarm-research.ts +0 -472
- package/src/swarm-review.integration.test.ts +0 -285
- package/src/swarm-review.test.ts +0 -879
- package/src/swarm-review.ts +0 -709
- package/src/swarm-strategies.ts +0 -407
- package/src/swarm-worktree.test.ts +0 -501
- package/src/swarm-worktree.ts +0 -575
- package/src/swarm.integration.test.ts +0 -2377
- package/src/swarm.ts +0 -38
- package/src/tool-adapter.integration.test.ts +0 -1221
- package/src/tool-availability.ts +0 -461
- package/tsconfig.json +0 -28
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rate Limiter Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests the rate limiting functionality with both Redis and SQLite backends.
|
|
5
|
-
* Requires Redis to be running for Redis tests (skipped if unavailable).
|
|
6
|
-
*/
|
|
7
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { mkdirSync, rmSync, existsSync } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import {
|
|
12
|
-
createRateLimiter,
|
|
13
|
-
InMemoryRateLimiter,
|
|
14
|
-
SqliteRateLimiter,
|
|
15
|
-
RedisRateLimiter,
|
|
16
|
-
resetFallbackWarning,
|
|
17
|
-
getLimitsForEndpoint,
|
|
18
|
-
DEFAULT_LIMITS,
|
|
19
|
-
type RateLimiter,
|
|
20
|
-
} from "./rate-limiter";
|
|
21
|
-
import Redis from "ioredis";
|
|
22
|
-
|
|
23
|
-
// ============================================================================
|
|
24
|
-
// Test Utilities
|
|
25
|
-
// ============================================================================
|
|
26
|
-
|
|
27
|
-
const TEST_AGENT = "TestAgent";
|
|
28
|
-
const TEST_ENDPOINT = "send";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Create a temporary directory for SQLite tests
|
|
32
|
-
*/
|
|
33
|
-
function createTempDir(): string {
|
|
34
|
-
const dir = join(tmpdir(), `rate-limiter-test-${Date.now()}`);
|
|
35
|
-
mkdirSync(dir, { recursive: true });
|
|
36
|
-
return dir;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Clean up temporary directory
|
|
41
|
-
*/
|
|
42
|
-
function cleanupTempDir(dir: string): void {
|
|
43
|
-
if (existsSync(dir)) {
|
|
44
|
-
rmSync(dir, { recursive: true, force: true });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Check if Redis is available
|
|
50
|
-
*/
|
|
51
|
-
async function isRedisAvailable(): Promise<boolean> {
|
|
52
|
-
try {
|
|
53
|
-
const redis = new Redis({
|
|
54
|
-
connectTimeout: 1000,
|
|
55
|
-
maxRetriesPerRequest: 1,
|
|
56
|
-
retryStrategy: () => null,
|
|
57
|
-
lazyConnect: true,
|
|
58
|
-
});
|
|
59
|
-
await redis.connect();
|
|
60
|
-
await redis.ping();
|
|
61
|
-
await redis.quit();
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ============================================================================
|
|
69
|
-
// InMemoryRateLimiter Tests
|
|
70
|
-
// ============================================================================
|
|
71
|
-
|
|
72
|
-
describe("InMemoryRateLimiter", () => {
|
|
73
|
-
let limiter: InMemoryRateLimiter;
|
|
74
|
-
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
limiter = new InMemoryRateLimiter();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
afterEach(async () => {
|
|
80
|
-
await limiter.close();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("allows requests under limit", async () => {
|
|
84
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
85
|
-
expect(result.allowed).toBe(true);
|
|
86
|
-
expect(result.remaining).toBeGreaterThan(0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("blocks requests over per-minute limit", async () => {
|
|
90
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
91
|
-
|
|
92
|
-
// Record requests up to the limit
|
|
93
|
-
for (let i = 0; i < limits.perMinute; i++) {
|
|
94
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Next request should be blocked
|
|
98
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
99
|
-
expect(result.allowed).toBe(false);
|
|
100
|
-
expect(result.remaining).toBe(0);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("tracks per-agent limits separately", async () => {
|
|
104
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
105
|
-
|
|
106
|
-
// Fill up Agent1's limit
|
|
107
|
-
for (let i = 0; i < limits.perMinute; i++) {
|
|
108
|
-
await limiter.recordRequest("Agent1", TEST_ENDPOINT);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Agent1 should be blocked
|
|
112
|
-
const result1 = await limiter.checkLimit("Agent1", TEST_ENDPOINT);
|
|
113
|
-
expect(result1.allowed).toBe(false);
|
|
114
|
-
|
|
115
|
-
// Agent2 should still be allowed
|
|
116
|
-
const result2 = await limiter.checkLimit("Agent2", TEST_ENDPOINT);
|
|
117
|
-
expect(result2.allowed).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("tracks per-endpoint limits separately", async () => {
|
|
121
|
-
const sendLimits = getLimitsForEndpoint("send");
|
|
122
|
-
|
|
123
|
-
// Fill up send limit
|
|
124
|
-
for (let i = 0; i < sendLimits.perMinute; i++) {
|
|
125
|
-
await limiter.recordRequest(TEST_AGENT, "send");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// send should be blocked
|
|
129
|
-
const sendResult = await limiter.checkLimit(TEST_AGENT, "send");
|
|
130
|
-
expect(sendResult.allowed).toBe(false);
|
|
131
|
-
|
|
132
|
-
// inbox should still be allowed
|
|
133
|
-
const inboxResult = await limiter.checkLimit(TEST_AGENT, "inbox");
|
|
134
|
-
expect(inboxResult.allowed).toBe(true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("reset clears all limits", async () => {
|
|
138
|
-
// Record some requests
|
|
139
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
140
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
141
|
-
|
|
142
|
-
// Reset
|
|
143
|
-
limiter.reset();
|
|
144
|
-
|
|
145
|
-
// Should have full limit available
|
|
146
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
147
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
148
|
-
expect(result.remaining).toBe(limits.perMinute);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("returns correct resetAt timestamp", async () => {
|
|
152
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
153
|
-
const now = Date.now();
|
|
154
|
-
|
|
155
|
-
// Fill up limit
|
|
156
|
-
for (let i = 0; i < limits.perMinute; i++) {
|
|
157
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
161
|
-
expect(result.allowed).toBe(false);
|
|
162
|
-
|
|
163
|
-
// resetAt should be approximately 1 minute from the first request
|
|
164
|
-
expect(result.resetAt).toBeGreaterThan(now);
|
|
165
|
-
expect(result.resetAt).toBeLessThanOrEqual(now + 60_000 + 1000); // Allow 1s tolerance
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// ============================================================================
|
|
170
|
-
// SqliteRateLimiter Tests
|
|
171
|
-
// ============================================================================
|
|
172
|
-
|
|
173
|
-
describe("SqliteRateLimiter", () => {
|
|
174
|
-
let limiter: SqliteRateLimiter;
|
|
175
|
-
let tempDir: string;
|
|
176
|
-
|
|
177
|
-
beforeEach(() => {
|
|
178
|
-
tempDir = createTempDir();
|
|
179
|
-
const dbPath = join(tempDir, "rate-limits.db");
|
|
180
|
-
limiter = new SqliteRateLimiter(dbPath);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
afterEach(async () => {
|
|
184
|
-
await limiter.close();
|
|
185
|
-
cleanupTempDir(tempDir);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("allows requests under limit", async () => {
|
|
189
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
190
|
-
expect(result.allowed).toBe(true);
|
|
191
|
-
expect(result.remaining).toBeGreaterThan(0);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("blocks requests over per-minute limit", async () => {
|
|
195
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
196
|
-
|
|
197
|
-
// Record requests up to the limit
|
|
198
|
-
for (let i = 0; i < limits.perMinute; i++) {
|
|
199
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Next request should be blocked
|
|
203
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
204
|
-
expect(result.allowed).toBe(false);
|
|
205
|
-
expect(result.remaining).toBe(0);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("creates database directory if not exists", () => {
|
|
209
|
-
const nestedDir = join(tempDir, "nested", "path");
|
|
210
|
-
const dbPath = join(nestedDir, "rate-limits.db");
|
|
211
|
-
|
|
212
|
-
// Should not throw
|
|
213
|
-
const nestedLimiter = new SqliteRateLimiter(dbPath);
|
|
214
|
-
expect(existsSync(nestedDir)).toBe(true);
|
|
215
|
-
nestedLimiter.close();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("persists data across instances", async () => {
|
|
219
|
-
const dbPath = join(tempDir, "persistent.db");
|
|
220
|
-
|
|
221
|
-
// First instance - record some requests
|
|
222
|
-
const limiter1 = new SqliteRateLimiter(dbPath);
|
|
223
|
-
await limiter1.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
224
|
-
await limiter1.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
225
|
-
await limiter1.close();
|
|
226
|
-
|
|
227
|
-
// Second instance - should see the recorded requests
|
|
228
|
-
const limiter2 = new SqliteRateLimiter(dbPath);
|
|
229
|
-
const result = await limiter2.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
230
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
231
|
-
expect(result.remaining).toBe(limits.perMinute - 2);
|
|
232
|
-
await limiter2.close();
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ============================================================================
|
|
237
|
-
// RedisRateLimiter Tests (skipped if Redis unavailable)
|
|
238
|
-
// ============================================================================
|
|
239
|
-
|
|
240
|
-
describe("RedisRateLimiter", async () => {
|
|
241
|
-
const redisAvailable = await isRedisAvailable();
|
|
242
|
-
|
|
243
|
-
test.skipIf(!redisAvailable)("allows requests under limit", async () => {
|
|
244
|
-
const redis = new Redis();
|
|
245
|
-
const limiter = new RedisRateLimiter(redis);
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
// Clean up any existing keys
|
|
249
|
-
await redis.del(`ratelimit:${TEST_AGENT}:${TEST_ENDPOINT}:minute`);
|
|
250
|
-
await redis.del(`ratelimit:${TEST_AGENT}:${TEST_ENDPOINT}:hour`);
|
|
251
|
-
|
|
252
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
253
|
-
expect(result.allowed).toBe(true);
|
|
254
|
-
expect(result.remaining).toBeGreaterThan(0);
|
|
255
|
-
} finally {
|
|
256
|
-
await limiter.close();
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test.skipIf(!redisAvailable)(
|
|
261
|
-
"blocks requests over per-minute limit",
|
|
262
|
-
async () => {
|
|
263
|
-
const redis = new Redis();
|
|
264
|
-
const limiter = new RedisRateLimiter(redis);
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
// Clean up any existing keys
|
|
268
|
-
await redis.del(`ratelimit:${TEST_AGENT}:${TEST_ENDPOINT}:minute`);
|
|
269
|
-
await redis.del(`ratelimit:${TEST_AGENT}:${TEST_ENDPOINT}:hour`);
|
|
270
|
-
|
|
271
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
272
|
-
|
|
273
|
-
// Record requests up to the limit
|
|
274
|
-
for (let i = 0; i < limits.perMinute; i++) {
|
|
275
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Next request should be blocked
|
|
279
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
280
|
-
expect(result.allowed).toBe(false);
|
|
281
|
-
expect(result.remaining).toBe(0);
|
|
282
|
-
} finally {
|
|
283
|
-
await limiter.close();
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
test.skipIf(!redisAvailable)("sets TTL on keys", async () => {
|
|
289
|
-
const redis = new Redis();
|
|
290
|
-
const limiter = new RedisRateLimiter(redis);
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
// Clean up any existing keys
|
|
294
|
-
const minuteKey = `ratelimit:${TEST_AGENT}:${TEST_ENDPOINT}:minute`;
|
|
295
|
-
const hourKey = `ratelimit:${TEST_AGENT}:${TEST_ENDPOINT}:hour`;
|
|
296
|
-
await redis.del(minuteKey);
|
|
297
|
-
await redis.del(hourKey);
|
|
298
|
-
|
|
299
|
-
// Record a request
|
|
300
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
301
|
-
|
|
302
|
-
// Check TTL is set
|
|
303
|
-
const minuteTTL = await redis.ttl(minuteKey);
|
|
304
|
-
const hourTTL = await redis.ttl(hourKey);
|
|
305
|
-
|
|
306
|
-
expect(minuteTTL).toBeGreaterThan(0);
|
|
307
|
-
expect(minuteTTL).toBeLessThanOrEqual(120); // 2 minutes
|
|
308
|
-
expect(hourTTL).toBeGreaterThan(0);
|
|
309
|
-
expect(hourTTL).toBeLessThanOrEqual(7200); // 2 hours
|
|
310
|
-
} finally {
|
|
311
|
-
await limiter.close();
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// ============================================================================
|
|
317
|
-
// createRateLimiter Factory Tests
|
|
318
|
-
// ============================================================================
|
|
319
|
-
|
|
320
|
-
describe("createRateLimiter", () => {
|
|
321
|
-
let tempDir: string;
|
|
322
|
-
|
|
323
|
-
beforeEach(() => {
|
|
324
|
-
tempDir = createTempDir();
|
|
325
|
-
resetFallbackWarning();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
afterEach(() => {
|
|
329
|
-
cleanupTempDir(tempDir);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("creates InMemoryRateLimiter when backend is memory", async () => {
|
|
333
|
-
const limiter = await createRateLimiter({ backend: "memory" });
|
|
334
|
-
expect(limiter).toBeInstanceOf(InMemoryRateLimiter);
|
|
335
|
-
await limiter.close();
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("creates SqliteRateLimiter when backend is sqlite", async () => {
|
|
339
|
-
const dbPath = join(tempDir, "test.db");
|
|
340
|
-
const limiter = await createRateLimiter({
|
|
341
|
-
backend: "sqlite",
|
|
342
|
-
sqlitePath: dbPath,
|
|
343
|
-
});
|
|
344
|
-
expect(limiter).toBeInstanceOf(SqliteRateLimiter);
|
|
345
|
-
await limiter.close();
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test("falls back to SQLite when Redis unavailable", async () => {
|
|
349
|
-
const dbPath = join(tempDir, "fallback.db");
|
|
350
|
-
const limiter = await createRateLimiter({
|
|
351
|
-
redisUrl: "redis://localhost:59999", // Non-existent port
|
|
352
|
-
sqlitePath: dbPath,
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// Should fall back to SQLite
|
|
356
|
-
expect(limiter).toBeInstanceOf(SqliteRateLimiter);
|
|
357
|
-
await limiter.close();
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// ============================================================================
|
|
362
|
-
// Configuration Tests
|
|
363
|
-
// ============================================================================
|
|
364
|
-
|
|
365
|
-
describe("Configuration", () => {
|
|
366
|
-
test("DEFAULT_LIMITS has all expected endpoints", () => {
|
|
367
|
-
const expectedEndpoints = [
|
|
368
|
-
"send",
|
|
369
|
-
"reserve",
|
|
370
|
-
"release",
|
|
371
|
-
"ack",
|
|
372
|
-
"inbox",
|
|
373
|
-
"read_message",
|
|
374
|
-
"summarize_thread",
|
|
375
|
-
"search",
|
|
376
|
-
];
|
|
377
|
-
|
|
378
|
-
for (const endpoint of expectedEndpoints) {
|
|
379
|
-
expect(DEFAULT_LIMITS[endpoint]).toBeDefined();
|
|
380
|
-
expect(DEFAULT_LIMITS[endpoint].perMinute).toBeGreaterThan(0);
|
|
381
|
-
expect(DEFAULT_LIMITS[endpoint].perHour).toBeGreaterThan(0);
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
test("getLimitsForEndpoint returns defaults for known endpoints", () => {
|
|
386
|
-
const limits = getLimitsForEndpoint("send");
|
|
387
|
-
expect(limits.perMinute).toBe(DEFAULT_LIMITS.send.perMinute);
|
|
388
|
-
expect(limits.perHour).toBe(DEFAULT_LIMITS.send.perHour);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
test("getLimitsForEndpoint returns fallback for unknown endpoints", () => {
|
|
392
|
-
const limits = getLimitsForEndpoint("unknown_endpoint");
|
|
393
|
-
expect(limits.perMinute).toBe(60);
|
|
394
|
-
expect(limits.perHour).toBe(600);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
test("env vars override default limits", () => {
|
|
398
|
-
const originalMin = process.env.OPENCODE_RATE_LIMIT_SEND_PER_MIN;
|
|
399
|
-
const originalHour = process.env.OPENCODE_RATE_LIMIT_SEND_PER_HOUR;
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
process.env.OPENCODE_RATE_LIMIT_SEND_PER_MIN = "100";
|
|
403
|
-
process.env.OPENCODE_RATE_LIMIT_SEND_PER_HOUR = "1000";
|
|
404
|
-
|
|
405
|
-
const limits = getLimitsForEndpoint("send");
|
|
406
|
-
expect(limits.perMinute).toBe(100);
|
|
407
|
-
expect(limits.perHour).toBe(1000);
|
|
408
|
-
} finally {
|
|
409
|
-
// Restore original values
|
|
410
|
-
if (originalMin !== undefined) {
|
|
411
|
-
process.env.OPENCODE_RATE_LIMIT_SEND_PER_MIN = originalMin;
|
|
412
|
-
} else {
|
|
413
|
-
delete process.env.OPENCODE_RATE_LIMIT_SEND_PER_MIN;
|
|
414
|
-
}
|
|
415
|
-
if (originalHour !== undefined) {
|
|
416
|
-
process.env.OPENCODE_RATE_LIMIT_SEND_PER_HOUR = originalHour;
|
|
417
|
-
} else {
|
|
418
|
-
delete process.env.OPENCODE_RATE_LIMIT_SEND_PER_HOUR;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// ============================================================================
|
|
425
|
-
// Dual Window Tests
|
|
426
|
-
// ============================================================================
|
|
427
|
-
|
|
428
|
-
describe("Dual Window Enforcement", () => {
|
|
429
|
-
let limiter: InMemoryRateLimiter;
|
|
430
|
-
|
|
431
|
-
beforeEach(() => {
|
|
432
|
-
limiter = new InMemoryRateLimiter();
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
afterEach(async () => {
|
|
436
|
-
await limiter.close();
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
test("enforces both minute and hour limits", async () => {
|
|
440
|
-
// Use an endpoint with low limits for testing
|
|
441
|
-
// inbox has 60/min, 600/hour
|
|
442
|
-
const endpoint = "inbox";
|
|
443
|
-
const limits = getLimitsForEndpoint(endpoint);
|
|
444
|
-
|
|
445
|
-
// Record requests up to minute limit
|
|
446
|
-
for (let i = 0; i < limits.perMinute; i++) {
|
|
447
|
-
await limiter.recordRequest(TEST_AGENT, endpoint);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Should be blocked by minute limit
|
|
451
|
-
const result = await limiter.checkLimit(TEST_AGENT, endpoint);
|
|
452
|
-
expect(result.allowed).toBe(false);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
test("returns most restrictive remaining count", async () => {
|
|
456
|
-
// Record a few requests
|
|
457
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
458
|
-
await limiter.recordRequest(TEST_AGENT, TEST_ENDPOINT);
|
|
459
|
-
|
|
460
|
-
const result = await limiter.checkLimit(TEST_AGENT, TEST_ENDPOINT);
|
|
461
|
-
const limits = getLimitsForEndpoint(TEST_ENDPOINT);
|
|
462
|
-
|
|
463
|
-
// Remaining should be based on minute window (more restrictive)
|
|
464
|
-
expect(result.remaining).toBe(limits.perMinute - 2);
|
|
465
|
-
});
|
|
466
|
-
});
|