@dewtech/dare-cli 3.11.0 → 3.13.0
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/README.md +2 -0
- package/dist/__tests__/cli-only-invariants.test.d.ts +2 -0
- package/dist/__tests__/cli-only-invariants.test.d.ts.map +1 -0
- package/dist/__tests__/cli-only-invariants.test.js +100 -0
- package/dist/__tests__/cli-only-invariants.test.js.map +1 -0
- package/dist/__tests__/cli-only-regression.test.d.ts +2 -0
- package/dist/__tests__/cli-only-regression.test.d.ts.map +1 -0
- package/dist/__tests__/cli-only-regression.test.js +29 -0
- package/dist/__tests__/cli-only-regression.test.js.map +1 -0
- package/dist/__tests__/ensure-skills.test.js +5 -0
- package/dist/__tests__/ensure-skills.test.js.map +1 -1
- package/dist/__tests__/ide-command-parity.test.js +1 -0
- package/dist/__tests__/ide-command-parity.test.js.map +1 -1
- package/dist/__tests__/project-generator.test.js +17 -0
- package/dist/__tests__/project-generator.test.js.map +1 -1
- package/dist/__tests__/reverse-facts.test.js +1 -0
- package/dist/__tests__/reverse-facts.test.js.map +1 -1
- package/dist/__tests__/terminal-parity-regression.test.d.ts +2 -0
- package/dist/__tests__/terminal-parity-regression.test.d.ts.map +1 -0
- package/dist/__tests__/terminal-parity-regression.test.js +116 -0
- package/dist/__tests__/terminal-parity-regression.test.js.map +1 -0
- package/dist/__tests__/terminal-parity.test.d.ts +2 -0
- package/dist/__tests__/terminal-parity.test.d.ts.map +1 -0
- package/dist/__tests__/terminal-parity.test.js +81 -0
- package/dist/__tests__/terminal-parity.test.js.map +1 -0
- package/dist/agent/__tests__/antigravity-driver.test.d.ts +2 -0
- package/dist/agent/__tests__/antigravity-driver.test.d.ts.map +1 -0
- package/dist/agent/__tests__/antigravity-driver.test.js +52 -0
- package/dist/agent/__tests__/antigravity-driver.test.js.map +1 -0
- package/dist/agent/__tests__/codex-driver.test.d.ts +2 -0
- package/dist/agent/__tests__/codex-driver.test.d.ts.map +1 -0
- package/dist/agent/__tests__/codex-driver.test.js +68 -0
- package/dist/agent/__tests__/codex-driver.test.js.map +1 -0
- package/dist/agent/__tests__/cursor-driver.test.d.ts +2 -0
- package/dist/agent/__tests__/cursor-driver.test.d.ts.map +1 -0
- package/dist/agent/__tests__/cursor-driver.test.js +52 -0
- package/dist/agent/__tests__/cursor-driver.test.js.map +1 -0
- package/dist/agent/driver.d.ts +1 -1
- package/dist/agent/driver.d.ts.map +1 -1
- package/dist/agent/drivers/antigravity.d.ts +8 -0
- package/dist/agent/drivers/antigravity.d.ts.map +1 -0
- package/dist/agent/drivers/antigravity.js +99 -0
- package/dist/agent/drivers/antigravity.js.map +1 -0
- package/dist/agent/drivers/codex.d.ts +12 -0
- package/dist/agent/drivers/codex.d.ts.map +1 -0
- package/dist/agent/drivers/codex.js +137 -0
- package/dist/agent/drivers/codex.js.map +1 -0
- package/dist/agent/drivers/cursor.d.ts +8 -0
- package/dist/agent/drivers/cursor.d.ts.map +1 -0
- package/dist/agent/drivers/cursor.js +99 -0
- package/dist/agent/drivers/cursor.js.map +1 -0
- package/dist/ai/__tests__/ai-core.test.d.ts +2 -0
- package/dist/ai/__tests__/ai-core.test.d.ts.map +1 -0
- package/dist/ai/__tests__/ai-core.test.js +41 -0
- package/dist/ai/__tests__/ai-core.test.js.map +1 -0
- package/dist/ai/__tests__/parity.test.d.ts +2 -0
- package/dist/ai/__tests__/parity.test.d.ts.map +1 -0
- package/dist/ai/__tests__/parity.test.js +36 -0
- package/dist/ai/__tests__/parity.test.js.map +1 -0
- package/dist/ai/__tests__/pipeline.test.d.ts +2 -0
- package/dist/ai/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/ai/__tests__/pipeline.test.js +147 -0
- package/dist/ai/__tests__/pipeline.test.js.map +1 -0
- package/dist/ai/__tests__/refine-bridge.test.d.ts +2 -0
- package/dist/ai/__tests__/refine-bridge.test.d.ts.map +1 -0
- package/dist/ai/__tests__/refine-bridge.test.js +17 -0
- package/dist/ai/__tests__/refine-bridge.test.js.map +1 -0
- package/dist/ai/__tests__/resolve.test.d.ts +2 -0
- package/dist/ai/__tests__/resolve.test.d.ts.map +1 -0
- package/dist/ai/__tests__/resolve.test.js +42 -0
- package/dist/ai/__tests__/resolve.test.js.map +1 -0
- package/dist/ai/capabilities.d.ts +3 -0
- package/dist/ai/capabilities.d.ts.map +1 -0
- package/dist/ai/capabilities.js +11 -0
- package/dist/ai/capabilities.js.map +1 -0
- package/dist/ai/command-options.d.ts +10 -0
- package/dist/ai/command-options.d.ts.map +1 -0
- package/dist/ai/command-options.js +15 -0
- package/dist/ai/command-options.js.map +1 -0
- package/dist/ai/config.d.ts +27 -0
- package/dist/ai/config.d.ts.map +1 -0
- package/dist/ai/config.js +89 -0
- package/dist/ai/config.js.map +1 -0
- package/dist/ai/parity.d.ts +13 -0
- package/dist/ai/parity.d.ts.map +1 -0
- package/dist/ai/parity.js +87 -0
- package/dist/ai/parity.js.map +1 -0
- package/dist/ai/parse-json-output.d.ts +5 -0
- package/dist/ai/parse-json-output.d.ts.map +1 -0
- package/dist/ai/parse-json-output.js +25 -0
- package/dist/ai/parse-json-output.js.map +1 -0
- package/dist/ai/pipeline.d.ts +20 -0
- package/dist/ai/pipeline.d.ts.map +1 -0
- package/dist/ai/pipeline.js +303 -0
- package/dist/ai/pipeline.js.map +1 -0
- package/dist/ai/prompts.d.ts +6 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +49 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/providers.d.ts +63 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +297 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/ai/refine-bridge.d.ts +5 -0
- package/dist/ai/refine-bridge.d.ts.map +1 -0
- package/dist/ai/refine-bridge.js +14 -0
- package/dist/ai/refine-bridge.js.map +1 -0
- package/dist/ai/registry.d.ts +12 -0
- package/dist/ai/registry.d.ts.map +1 -0
- package/dist/ai/registry.js +43 -0
- package/dist/ai/registry.js.map +1 -0
- package/dist/ai/resolve.d.ts +28 -0
- package/dist/ai/resolve.d.ts.map +1 -0
- package/dist/ai/resolve.js +83 -0
- package/dist/ai/resolve.js.map +1 -0
- package/dist/ai/schemas.d.ts +175 -0
- package/dist/ai/schemas.d.ts.map +1 -0
- package/dist/ai/schemas.js +199 -0
- package/dist/ai/schemas.js.map +1 -0
- package/dist/ai/types.d.ts +52 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +8 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/bin/dare.js +2 -0
- package/dist/bin/dare.js.map +1 -1
- package/dist/commands/__tests__/ai-command.test.d.ts +2 -0
- package/dist/commands/__tests__/ai-command.test.d.ts.map +1 -0
- package/dist/commands/__tests__/ai-command.test.js +68 -0
- package/dist/commands/__tests__/ai-command.test.js.map +1 -0
- package/dist/commands/__tests__/execute-agent.test.js +82 -0
- package/dist/commands/__tests__/execute-agent.test.js.map +1 -1
- package/dist/commands/ai.d.ts +3 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +141 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/blueprint.d.ts.map +1 -1
- package/dist/commands/blueprint.js +17 -3
- package/dist/commands/blueprint.js.map +1 -1
- package/dist/commands/design.d.ts.map +1 -1
- package/dist/commands/design.js +21 -2
- package/dist/commands/design.js.map +1 -1
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +9 -1
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/dna.d.ts.map +1 -1
- package/dist/commands/dna.js +23 -3
- package/dist/commands/dna.js.map +1 -1
- package/dist/commands/execute.d.ts +11 -0
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +111 -4
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +14 -2
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/patterns.d.ts.map +1 -1
- package/dist/commands/patterns.js +14 -2
- package/dist/commands/patterns.js.map +1 -1
- package/dist/commands/refine.d.ts.map +1 -1
- package/dist/commands/refine.js +23 -2
- package/dist/commands/refine.js.map +1 -1
- package/dist/commands/reverse.d.ts.map +1 -1
- package/dist/commands/reverse.js +28 -3
- package/dist/commands/reverse.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +25 -3
- package/dist/commands/review.js.map +1 -1
- package/dist/core/types/project.d.ts +1 -1
- package/dist/core/types/project.d.ts.map +1 -1
- package/dist/dag-runner/run_dag.d.ts +1 -1
- package/dist/dag-runner/run_dag.d.ts.map +1 -1
- package/dist/exec/safe-spawn.d.ts.map +1 -1
- package/dist/exec/safe-spawn.js +6 -1
- package/dist/exec/safe-spawn.js.map +1 -1
- package/dist/skills/bundled.d.ts +5 -0
- package/dist/skills/bundled.d.ts.map +1 -0
- package/dist/skills/bundled.js +34 -0
- package/dist/skills/bundled.js.map +1 -0
- package/dist/skills/commands/add.d.ts +1 -3
- package/dist/skills/commands/add.d.ts.map +1 -1
- package/dist/skills/commands/add.js +20 -3
- package/dist/skills/commands/add.js.map +1 -1
- package/dist/skills/tests/bundled.spec.d.ts +2 -0
- package/dist/skills/tests/bundled.spec.d.ts.map +1 -0
- package/dist/skills/tests/bundled.spec.js +24 -0
- package/dist/skills/tests/bundled.spec.js.map +1 -0
- package/dist/types/UpdateManifest.types.d.ts +1 -1
- package/dist/types/UpdateManifest.types.d.ts.map +1 -1
- package/dist/utils/dag-converter.js +1 -1
- package/dist/utils/dag-converter.js.map +1 -1
- package/dist/utils/project-detector.d.ts +1 -0
- package/dist/utils/project-detector.d.ts.map +1 -1
- package/dist/utils/project-detector.js +8 -0
- package/dist/utils/project-detector.js.map +1 -1
- package/dist/utils/project-generator.d.ts +1 -1
- package/dist/utils/project-generator.d.ts.map +1 -1
- package/dist/utils/project-generator.js +23 -2
- package/dist/utils/project-generator.js.map +1 -1
- package/dist/utils/templates.d.ts +2 -0
- package/dist/utils/templates.d.ts.map +1 -1
- package/dist/utils/templates.js +74 -0
- package/dist/utils/templates.js.map +1 -1
- package/dist/verification/__tests__/safe-spawn.test.js +12 -0
- package/dist/verification/__tests__/safe-spawn.test.js.map +1 -1
- package/package.json +2 -1
- package/skills/dare-ax/generator.ts +325 -0
- package/skills/dare-ax/index.ts +19 -0
- package/skills/dare-ax/metrics.ts +352 -0
- package/skills/dare-ax/package-lock.json +1855 -0
- package/skills/dare-ax/package.json +50 -0
- package/skills/dare-ax/secret-detector.ts +123 -0
- package/skills/dare-ax/skill.yml +19 -0
- package/skills/dare-ax/templates/llms.txt.jinja2 +80 -0
- package/skills/dare-ax/tests/generator.spec.ts +193 -0
- package/skills/dare-ax/tests/metrics.spec.ts +394 -0
- package/skills/dare-ax/tests/validator.spec.ts +298 -0
- package/skills/dare-ax/tsconfig.json +18 -0
- package/skills/dare-ax/types.ts +79 -0
- package/skills/dare-ax/validator.ts +238 -0
- package/skills/dare-frontend-design/generator.ts +616 -0
- package/skills/dare-frontend-design/index.ts +25 -0
- package/skills/dare-frontend-design/linter.ts +227 -0
- package/skills/dare-frontend-design/metrics.ts +82 -0
- package/skills/dare-frontend-design/package-lock.json +1855 -0
- package/skills/dare-frontend-design/package.json +43 -0
- package/skills/dare-frontend-design/skill.yml +20 -0
- package/skills/dare-frontend-design/tests/frontend_design.spec.ts +435 -0
- package/skills/dare-frontend-design/tsconfig.json +18 -0
- package/skills/dare-frontend-design/types.ts +62 -0
- package/skills/dare-layered-design/generator.ts +740 -0
- package/skills/dare-layered-design/index.ts +17 -0
- package/skills/dare-layered-design/linter.ts +462 -0
- package/skills/dare-layered-design/metrics.ts +409 -0
- package/skills/dare-layered-design/package-lock.json +1855 -0
- package/skills/dare-layered-design/package.json +50 -0
- package/skills/dare-layered-design/skill.yml +35 -0
- package/skills/dare-layered-design/tests/generator.spec.ts +156 -0
- package/skills/dare-layered-design/tests/linter.spec.ts +255 -0
- package/skills/dare-layered-design/tests/metrics.spec.ts +286 -0
- package/skills/dare-layered-design/tsconfig.json +18 -0
- package/skills/dare-layered-design/types.ts +48 -0
- package/skills/dare-llm-integration/cache/llm_cache.ts +122 -0
- package/skills/dare-llm-integration/index.ts +49 -0
- package/skills/dare-llm-integration/metrics.ts +107 -0
- package/skills/dare-llm-integration/package-lock.json +1855 -0
- package/skills/dare-llm-integration/package.json +49 -0
- package/skills/dare-llm-integration/prompts/prompt_loader.ts +258 -0
- package/skills/dare-llm-integration/providers/anthropic_provider.ts +159 -0
- package/skills/dare-llm-integration/providers/dummy_provider.ts +113 -0
- package/skills/dare-llm-integration/providers/llm_provider.ts +6 -0
- package/skills/dare-llm-integration/providers/openai_provider.ts +215 -0
- package/skills/dare-llm-integration/rate_limit/token_bucket.ts +86 -0
- package/skills/dare-llm-integration/skill.yml +23 -0
- package/skills/dare-llm-integration/tests/fixtures/greet_v1.jinja2 +1 -0
- package/skills/dare-llm-integration/tests/fixtures/summarize_v1.jinja2 +1 -0
- package/skills/dare-llm-integration/tests/fixtures/summarize_v2.jinja2 +3 -0
- package/skills/dare-llm-integration/tests/llm_integration.spec.ts +657 -0
- package/skills/dare-llm-integration/tsconfig.json +23 -0
- package/skills/dare-llm-integration/types.ts +91 -0
- package/skills/dare-llm-integration/validators/output_validator.ts +200 -0
- package/skills/dare-quality-telemetry/collect.ts +134 -0
- package/skills/dare-quality-telemetry/collectors/dare_ax_collector.ts +301 -0
- package/skills/dare-quality-telemetry/collectors/dare_layered_design_collector.ts +406 -0
- package/skills/dare-quality-telemetry/collectors/index.ts +24 -0
- package/skills/dare-quality-telemetry/github_actions_template.ts +25 -0
- package/skills/dare-quality-telemetry/index.ts +18 -0
- package/skills/dare-quality-telemetry/metrics.ts +137 -0
- package/skills/dare-quality-telemetry/package-lock.json +1855 -0
- package/skills/dare-quality-telemetry/package.json +48 -0
- package/skills/dare-quality-telemetry/regression.ts +60 -0
- package/skills/dare-quality-telemetry/reporter.ts +132 -0
- package/skills/dare-quality-telemetry/skill.yml +18 -0
- package/skills/dare-quality-telemetry/tests/quality_telemetry.spec.ts +885 -0
- package/skills/dare-quality-telemetry/tsconfig.json +19 -0
- package/skills/dare-quality-telemetry/types.ts +41 -0
- package/skills/dare-realtime/event_registry.ts +101 -0
- package/skills/dare-realtime/index.ts +30 -0
- package/skills/dare-realtime/metrics.ts +84 -0
- package/skills/dare-realtime/package-lock.json +1855 -0
- package/skills/dare-realtime/package.json +43 -0
- package/skills/dare-realtime/reconnect_strategy.ts +85 -0
- package/skills/dare-realtime/schema_validator.ts +80 -0
- package/skills/dare-realtime/skill.yml +21 -0
- package/skills/dare-realtime/subscription_manager.ts +106 -0
- package/skills/dare-realtime/tests/realtime.spec.ts +482 -0
- package/skills/dare-realtime/tsconfig.json +18 -0
- package/skills/dare-realtime/types.ts +51 -0
- package/templates/ide/antigravity/.agents/skills/dare-ai/SKILL.md +17 -0
- package/templates/ide/antigravity/.agents/skills/dare-blueprint/SKILL.md +2 -0
- package/templates/ide/antigravity/.agents/skills/dare-design/SKILL.md +2 -0
- package/templates/ide/antigravity/.agents/skills/dare-dna/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-migrate/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-patterns/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-refine/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-reverse/SKILL.md +3 -0
- package/templates/ide/antigravity/.agents/skills/dare-review/SKILL.md +3 -0
- package/templates/ide/claude/.claude/commands/dare-ai.md +17 -0
- package/templates/ide/claude/.claude/commands/dare-blueprint.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-design.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-dna.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-migrate.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-patterns.md +3 -0
- package/templates/ide/claude/.claude/commands/dare-refine.md +3 -0
- package/templates/ide/claude/.claude/commands/dare-reverse.md +2 -0
- package/templates/ide/claude/.claude/commands/dare-review.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-ai.md +17 -0
- package/templates/ide/cursor/.cursor/commands/dare-blueprint.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-design.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-dna.md +2 -0
- package/templates/ide/cursor/.cursor/commands/dare-migrate.md +2 -0
- package/templates/ide/cursor/.cursor/commands/dare-patterns.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-refine.md +3 -0
- package/templates/ide/cursor/.cursor/commands/dare-reverse.md +2 -0
- package/templates/ide/cursor/.cursor/commands/dare-review.md +3 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dare-llm-integration — test suite
|
|
3
|
+
* 50+ tests covering cache, rate limit, providers, prompt loader, validator, metrics.
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
import { LLMCache } from '../cache/llm_cache.js';
|
|
12
|
+
import { TokenBucket } from '../rate_limit/token_bucket.js';
|
|
13
|
+
import { DummyProvider } from '../providers/dummy_provider.js';
|
|
14
|
+
import { PromptLoader } from '../prompts/prompt_loader.js';
|
|
15
|
+
import { OutputValidator } from '../validators/output_validator.js';
|
|
16
|
+
import { collectLLMIntegrationMetrics } from '../metrics.js';
|
|
17
|
+
import type { CompletionResponse } from '../types.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const FIXTURES_DIR = path.join(__dirname, 'fixtures');
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// LLMCache
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
describe('LLMCache', () => {
|
|
27
|
+
let cache: LLMCache;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
cache = new LLMCache();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns null for missing key', () => {
|
|
34
|
+
expect(cache.get('missing')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('stores and retrieves a value', () => {
|
|
38
|
+
const value: CompletionResponse = {
|
|
39
|
+
text: 'hello',
|
|
40
|
+
tokensUsed: { input: 5, output: 10, total: 15 },
|
|
41
|
+
cached: false,
|
|
42
|
+
model: 'gpt-4',
|
|
43
|
+
};
|
|
44
|
+
cache.set('key1', value, 60_000);
|
|
45
|
+
const entry = cache.get('key1');
|
|
46
|
+
expect(entry).not.toBeNull();
|
|
47
|
+
expect((entry!.value as CompletionResponse).text).toBe('hello');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('marks cached:false on stored value (original)', () => {
|
|
51
|
+
const value: CompletionResponse = {
|
|
52
|
+
text: 'test',
|
|
53
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
54
|
+
cached: false,
|
|
55
|
+
model: 'gpt-3.5',
|
|
56
|
+
};
|
|
57
|
+
cache.set('k', value, 60_000);
|
|
58
|
+
const retrieved = cache.get('k');
|
|
59
|
+
expect(retrieved).not.toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns null for expired TTL', async () => {
|
|
63
|
+
const value: CompletionResponse = {
|
|
64
|
+
text: 'x',
|
|
65
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
66
|
+
cached: false,
|
|
67
|
+
model: 'm',
|
|
68
|
+
};
|
|
69
|
+
cache.set('exp', value, 1); // 1ms TTL
|
|
70
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
71
|
+
expect(cache.get('exp')).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('tracks hit count', () => {
|
|
75
|
+
const value: CompletionResponse = {
|
|
76
|
+
text: 'hi',
|
|
77
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
78
|
+
cached: false,
|
|
79
|
+
model: 'm',
|
|
80
|
+
};
|
|
81
|
+
cache.set('k', value, 60_000);
|
|
82
|
+
cache.get('k');
|
|
83
|
+
cache.get('k');
|
|
84
|
+
expect(cache.hits).toBe(2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('tracks miss count', () => {
|
|
88
|
+
cache.get('no');
|
|
89
|
+
cache.get('no2');
|
|
90
|
+
expect(cache.misses).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('calculates hit rate', () => {
|
|
94
|
+
const value: CompletionResponse = {
|
|
95
|
+
text: 'x',
|
|
96
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
97
|
+
cached: false,
|
|
98
|
+
model: 'm',
|
|
99
|
+
};
|
|
100
|
+
cache.set('k', value, 60_000);
|
|
101
|
+
cache.get('k'); // hit
|
|
102
|
+
cache.get('k'); // hit
|
|
103
|
+
cache.get('missing'); // miss
|
|
104
|
+
expect(cache.hitRate).toBeCloseTo(2 / 3);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns 0 hit rate with no requests', () => {
|
|
108
|
+
expect(cache.hitRate).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('invalidates a specific key', () => {
|
|
112
|
+
const value: CompletionResponse = {
|
|
113
|
+
text: 'y',
|
|
114
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
115
|
+
cached: false,
|
|
116
|
+
model: 'm',
|
|
117
|
+
};
|
|
118
|
+
cache.set('k', value, 60_000);
|
|
119
|
+
cache.invalidate('k');
|
|
120
|
+
expect(cache.get('k')).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('clears all entries', () => {
|
|
124
|
+
const value: CompletionResponse = {
|
|
125
|
+
text: 'z',
|
|
126
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
127
|
+
cached: false,
|
|
128
|
+
model: 'm',
|
|
129
|
+
};
|
|
130
|
+
cache.set('k1', value, 60_000);
|
|
131
|
+
cache.set('k2', value, 60_000);
|
|
132
|
+
cache.clear();
|
|
133
|
+
expect(cache.size).toBe(0);
|
|
134
|
+
expect(cache.hits).toBe(0);
|
|
135
|
+
expect(cache.misses).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('purges expired entries', async () => {
|
|
139
|
+
const value: CompletionResponse = {
|
|
140
|
+
text: 'x',
|
|
141
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
142
|
+
cached: false,
|
|
143
|
+
model: 'm',
|
|
144
|
+
};
|
|
145
|
+
cache.set('old', value, 1);
|
|
146
|
+
cache.set('fresh', value, 60_000);
|
|
147
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
148
|
+
const purged = cache.purgeExpired();
|
|
149
|
+
expect(purged).toBe(1);
|
|
150
|
+
expect(cache.size).toBe(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('has() returns false for expired key', async () => {
|
|
154
|
+
const value: CompletionResponse = {
|
|
155
|
+
text: 'x',
|
|
156
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
157
|
+
cached: false,
|
|
158
|
+
model: 'm',
|
|
159
|
+
};
|
|
160
|
+
cache.set('exp', value, 1);
|
|
161
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
162
|
+
expect(cache.has('exp')).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('has() returns true for valid key', () => {
|
|
166
|
+
const value: CompletionResponse = {
|
|
167
|
+
text: 'x',
|
|
168
|
+
tokensUsed: { input: 1, output: 1, total: 2 },
|
|
169
|
+
cached: false,
|
|
170
|
+
model: 'm',
|
|
171
|
+
};
|
|
172
|
+
cache.set('k', value, 60_000);
|
|
173
|
+
expect(cache.has('k')).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// TokenBucket
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
describe('TokenBucket', () => {
|
|
182
|
+
it('throws for rps <= 0', () => {
|
|
183
|
+
expect(() => new TokenBucket({ rps: 0 })).toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('tryAcquire returns true when tokens available', () => {
|
|
187
|
+
const bucket = new TokenBucket({ rps: 10 });
|
|
188
|
+
expect(bucket.tryAcquire()).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('tryAcquire returns false when exhausted', () => {
|
|
192
|
+
const bucket = new TokenBucket({ rps: 1, maxBurst: 1 });
|
|
193
|
+
bucket.tryAcquire(); // consume the 1 token
|
|
194
|
+
expect(bucket.tryAcquire()).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('acquire resolves when token available', async () => {
|
|
198
|
+
const bucket = new TokenBucket({ rps: 100 });
|
|
199
|
+
await expect(bucket.acquire()).resolves.toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('acquire blocks and then resolves after refill', async () => {
|
|
203
|
+
const bucket = new TokenBucket({ rps: 10, maxBurst: 1 });
|
|
204
|
+
bucket.tryAcquire(); // consume
|
|
205
|
+
const start = Date.now();
|
|
206
|
+
await bucket.acquire();
|
|
207
|
+
const elapsed = Date.now() - start;
|
|
208
|
+
expect(elapsed).toBeGreaterThanOrEqual(80); // ~100ms refill for 10 rps
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('reports configured rps', () => {
|
|
212
|
+
const bucket = new TokenBucket({ rps: 5 });
|
|
213
|
+
expect(bucket.requestsPerSecond).toBe(5);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('multiple tryAcquire calls respect burst limit', () => {
|
|
217
|
+
const bucket = new TokenBucket({ rps: 2, maxBurst: 3 });
|
|
218
|
+
let successes = 0;
|
|
219
|
+
for (let i = 0; i < 5; i++) {
|
|
220
|
+
if (bucket.tryAcquire()) successes++;
|
|
221
|
+
}
|
|
222
|
+
expect(successes).toBe(3); // maxBurst = 3
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// DummyProvider
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
describe('DummyProvider', () => {
|
|
231
|
+
it('returns configured completion text', async () => {
|
|
232
|
+
const provider = new DummyProvider({ completionText: 'test answer' });
|
|
233
|
+
const result = await provider.complete({ model: 'gpt-4', prompt: 'hello' });
|
|
234
|
+
expect(result.text).toBe('test answer');
|
|
235
|
+
expect(result.model).toBe('gpt-4');
|
|
236
|
+
expect(result.cached).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns token usage', async () => {
|
|
240
|
+
const provider = new DummyProvider({ tokenCounts: { input: 5, output: 10 } });
|
|
241
|
+
const result = await provider.complete({ model: 'm', prompt: 'q' });
|
|
242
|
+
expect(result.tokensUsed.input).toBe(5);
|
|
243
|
+
expect(result.tokensUsed.output).toBe(10);
|
|
244
|
+
expect(result.tokensUsed.total).toBe(15);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('returns embeddings for single input', async () => {
|
|
248
|
+
const provider = new DummyProvider({ embedding: [0.1, 0.2, 0.3] });
|
|
249
|
+
const result = await provider.embed({ model: 'text-embedding', input: 'hello' });
|
|
250
|
+
expect(result.embeddings).toHaveLength(1);
|
|
251
|
+
expect(result.embeddings[0]).toEqual([0.1, 0.2, 0.3]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns embeddings for multiple inputs', async () => {
|
|
255
|
+
const provider = new DummyProvider({ embedding: [1, 2] });
|
|
256
|
+
const result = await provider.embed({ model: 'text-embedding', input: ['a', 'b', 'c'] });
|
|
257
|
+
expect(result.embeddings).toHaveLength(3);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('throws configured error on complete', async () => {
|
|
261
|
+
const err = new Error('api down');
|
|
262
|
+
const provider = new DummyProvider({ throwError: err });
|
|
263
|
+
await expect(provider.complete({ model: 'm', prompt: 'q' })).rejects.toThrow('api down');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('throws configured error on embed', async () => {
|
|
267
|
+
const err = new Error('embed fail');
|
|
268
|
+
const provider = new DummyProvider({ throwError: err });
|
|
269
|
+
await expect(provider.embed({ model: 'm', input: 'x' })).rejects.toThrow('embed fail');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('records completion history', async () => {
|
|
273
|
+
const provider = new DummyProvider();
|
|
274
|
+
await provider.complete({ model: 'gpt-4', prompt: 'p1' });
|
|
275
|
+
await provider.complete({ model: 'gpt-4', prompt: 'p2' });
|
|
276
|
+
expect(provider.completionHistory).toHaveLength(2);
|
|
277
|
+
expect(provider.completionHistory[0].prompt).toBe('p1');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('calls onComplete spy', async () => {
|
|
281
|
+
const spy = vi.fn();
|
|
282
|
+
const provider = new DummyProvider({ onComplete: spy });
|
|
283
|
+
await provider.complete({ model: 'x', prompt: 'q' });
|
|
284
|
+
expect(spy).toHaveBeenCalledOnce();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('simulates latency', async () => {
|
|
288
|
+
const provider = new DummyProvider({ latencyMs: 50 });
|
|
289
|
+
const start = Date.now();
|
|
290
|
+
await provider.complete({ model: 'm', prompt: 'q' });
|
|
291
|
+
expect(Date.now() - start).toBeGreaterThanOrEqual(45);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// PromptLoader
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
describe('PromptLoader', () => {
|
|
300
|
+
let loader: PromptLoader;
|
|
301
|
+
|
|
302
|
+
beforeEach(() => {
|
|
303
|
+
loader = new PromptLoader({ templatesDir: FIXTURES_DIR });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('loads and renders v1 template', () => {
|
|
307
|
+
const result = loader.load('summarize', 'v1', { text: 'Hello world' });
|
|
308
|
+
expect(result).toContain('Hello world');
|
|
309
|
+
expect(result).toContain('Summarize');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('loads and renders v2 template with different variables', () => {
|
|
313
|
+
const result = loader.load('summarize', 'v2', { max_words: '50', text: 'Test text' });
|
|
314
|
+
expect(result).toContain('50');
|
|
315
|
+
expect(result).toContain('Test text');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('loads greet template', () => {
|
|
319
|
+
const result = loader.load('greet', 'v1', { name: 'Alice', service: 'DARE' });
|
|
320
|
+
expect(result).toContain('Alice');
|
|
321
|
+
expect(result).toContain('DARE');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('throws when template not found', () => {
|
|
325
|
+
expect(() => loader.load('nonexistent', 'v1', {})).toThrow('template not found');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('caches template content after first load', () => {
|
|
329
|
+
loader.load('summarize', 'v1', { text: 'x' });
|
|
330
|
+
loader.load('summarize', 'v1', { text: 'y' }); // should use cache
|
|
331
|
+
// No error = cache working
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('clearCache() forces file re-read', () => {
|
|
335
|
+
loader.load('summarize', 'v1', { text: 'x' });
|
|
336
|
+
loader.clearCache();
|
|
337
|
+
// After clear, should still work by re-reading
|
|
338
|
+
const result = loader.load('summarize', 'v1', { text: 'x' });
|
|
339
|
+
expect(result).toContain('x');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('listTemplates returns all .jinja2 files', () => {
|
|
343
|
+
const templates = loader.listTemplates();
|
|
344
|
+
expect(templates.length).toBeGreaterThanOrEqual(3);
|
|
345
|
+
const names = templates.map((t) => t.name);
|
|
346
|
+
expect(names).toContain('summarize');
|
|
347
|
+
expect(names).toContain('greet');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('render() works with inline template string', () => {
|
|
351
|
+
const result = loader.render('Hello {{ name }}!', { name: 'World' });
|
|
352
|
+
expect(result).toBe('Hello World!');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('handles template with multiple variables', () => {
|
|
356
|
+
const result = loader.render('{{ a }} + {{ b }} = {{ c }}', { a: '1', b: '2', c: '3' });
|
|
357
|
+
expect(result).toBe('1 + 2 = 3');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('handles missing variable gracefully (renders empty)', () => {
|
|
361
|
+
const result = loader.render('Hello {{ missing_var }}!', {});
|
|
362
|
+
expect(result).toBe('Hello !');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// OutputValidator
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
describe('OutputValidator', () => {
|
|
371
|
+
let validator: OutputValidator;
|
|
372
|
+
|
|
373
|
+
beforeEach(() => {
|
|
374
|
+
validator = new OutputValidator();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('validates valid JSON against schema', () => {
|
|
378
|
+
const schema = {
|
|
379
|
+
type: 'object' as const,
|
|
380
|
+
properties: {
|
|
381
|
+
summary: { type: 'string' as const },
|
|
382
|
+
keywords: { type: 'array' as const, items: { type: 'string' as const } },
|
|
383
|
+
},
|
|
384
|
+
required: ['summary', 'keywords'],
|
|
385
|
+
};
|
|
386
|
+
const output = JSON.stringify({ summary: 'Hello', keywords: ['a', 'b'] });
|
|
387
|
+
const result = validator.validate(output, schema);
|
|
388
|
+
expect(result.ok).toBe(true);
|
|
389
|
+
expect(result.data).toEqual({ summary: 'Hello', keywords: ['a', 'b'] });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('fails on invalid JSON', () => {
|
|
393
|
+
const result = validator.validate('not json', {});
|
|
394
|
+
expect(result.ok).toBe(false);
|
|
395
|
+
expect(result.errors[0].field).toBe('$root');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('fails when required field is missing', () => {
|
|
399
|
+
const schema = {
|
|
400
|
+
type: 'object' as const,
|
|
401
|
+
required: ['name', 'email'],
|
|
402
|
+
};
|
|
403
|
+
const result = validator.validate(JSON.stringify({ name: 'Alice' }), schema);
|
|
404
|
+
expect(result.ok).toBe(false);
|
|
405
|
+
expect(result.errors.some((e) => e.message.includes('email'))).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('fails on wrong type', () => {
|
|
409
|
+
const schema = { type: 'string' as const };
|
|
410
|
+
const result = validator.validate('123', schema);
|
|
411
|
+
expect(result.ok).toBe(false);
|
|
412
|
+
expect(result.errors[0].message).toContain('Expected type string');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('passes integer type', () => {
|
|
416
|
+
const schema = { type: 'integer' as const };
|
|
417
|
+
const result = validator.validate('42', schema);
|
|
418
|
+
expect(result.ok).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('fails non-integer for integer type', () => {
|
|
422
|
+
const schema = { type: 'integer' as const };
|
|
423
|
+
const result = validator.validate('42.5', schema);
|
|
424
|
+
expect(result.ok).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('validates string minLength', () => {
|
|
428
|
+
const schema = { type: 'string' as const, minLength: 5 };
|
|
429
|
+
const result = validator.validate('"hi"', schema);
|
|
430
|
+
expect(result.ok).toBe(false);
|
|
431
|
+
expect(result.errors[0].message).toContain('minLength');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('validates string maxLength', () => {
|
|
435
|
+
const schema = { type: 'string' as const, maxLength: 3 };
|
|
436
|
+
const result = validator.validate('"toolong"', schema);
|
|
437
|
+
expect(result.ok).toBe(false);
|
|
438
|
+
expect(result.errors[0].message).toContain('maxLength');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('validates number minimum', () => {
|
|
442
|
+
const schema = { type: 'number' as const, minimum: 10 };
|
|
443
|
+
const result = validator.validate('5', schema);
|
|
444
|
+
expect(result.ok).toBe(false);
|
|
445
|
+
expect(result.errors[0].message).toContain('minimum');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('validates number maximum', () => {
|
|
449
|
+
const schema = { type: 'number' as const, maximum: 100 };
|
|
450
|
+
const result = validator.validate('200', schema);
|
|
451
|
+
expect(result.ok).toBe(false);
|
|
452
|
+
expect(result.errors[0].message).toContain('maximum');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('validates enum values', () => {
|
|
456
|
+
const schema = { enum: ['a', 'b', 'c'] };
|
|
457
|
+
const result = validator.validate('"d"', schema);
|
|
458
|
+
expect(result.ok).toBe(false);
|
|
459
|
+
expect(result.errors[0].message).toContain('enum');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('passes enum with valid value', () => {
|
|
463
|
+
const schema = { enum: ['a', 'b', 'c'] };
|
|
464
|
+
const result = validator.validate('"b"', schema);
|
|
465
|
+
expect(result.ok).toBe(true);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('validateParsed skips JSON parsing', () => {
|
|
469
|
+
const schema = { type: 'string' as const };
|
|
470
|
+
const result = validator.validateParsed('hello', schema);
|
|
471
|
+
expect(result.ok).toBe(true);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('validates nested objects', () => {
|
|
475
|
+
const schema = {
|
|
476
|
+
type: 'object' as const,
|
|
477
|
+
properties: {
|
|
478
|
+
user: {
|
|
479
|
+
type: 'object' as const,
|
|
480
|
+
properties: { name: { type: 'string' as const } },
|
|
481
|
+
required: ['name'],
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
required: ['user'],
|
|
485
|
+
};
|
|
486
|
+
const result = validator.validate(JSON.stringify({ user: { name: 'Alice' } }), schema);
|
|
487
|
+
expect(result.ok).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('fails nested required field', () => {
|
|
491
|
+
const schema = {
|
|
492
|
+
type: 'object' as const,
|
|
493
|
+
properties: {
|
|
494
|
+
user: {
|
|
495
|
+
type: 'object' as const,
|
|
496
|
+
required: ['email'],
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
required: ['user'],
|
|
500
|
+
};
|
|
501
|
+
const result = validator.validate(JSON.stringify({ user: { name: 'Alice' } }), schema);
|
|
502
|
+
expect(result.ok).toBe(false);
|
|
503
|
+
expect(result.errors.some((e) => e.field.includes('email'))).toBe(true);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('validates array items', () => {
|
|
507
|
+
const schema = {
|
|
508
|
+
type: 'array' as const,
|
|
509
|
+
items: { type: 'number' as const },
|
|
510
|
+
};
|
|
511
|
+
const result = validator.validate('[1, 2, "three"]', schema);
|
|
512
|
+
expect(result.ok).toBe(false);
|
|
513
|
+
expect(result.errors.some((e) => e.field.includes('[2]'))).toBe(true);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Metrics
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
describe('collectLLMIntegrationMetrics', () => {
|
|
522
|
+
it('M-01 passes when all calls via provider', () => {
|
|
523
|
+
const results = collectLLMIntegrationMetrics({
|
|
524
|
+
totalCallsViaProvider: 10,
|
|
525
|
+
totalDirectCalls: 0,
|
|
526
|
+
cache: null,
|
|
527
|
+
rateLimiter: null,
|
|
528
|
+
validatedResponseCount: 0,
|
|
529
|
+
unvalidatedResponseCount: 0,
|
|
530
|
+
});
|
|
531
|
+
const m01 = results.find((r) => r.id === 'M-01')!;
|
|
532
|
+
expect(m01.pass).toBe(true);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('M-01 fails when direct calls detected', () => {
|
|
536
|
+
const results = collectLLMIntegrationMetrics({
|
|
537
|
+
totalCallsViaProvider: 10,
|
|
538
|
+
totalDirectCalls: 2,
|
|
539
|
+
cache: null,
|
|
540
|
+
rateLimiter: null,
|
|
541
|
+
validatedResponseCount: 0,
|
|
542
|
+
unvalidatedResponseCount: 0,
|
|
543
|
+
});
|
|
544
|
+
const m01 = results.find((r) => r.id === 'M-01')!;
|
|
545
|
+
expect(m01.pass).toBe(false);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('M-02 passes when cache configured', () => {
|
|
549
|
+
const cache = new LLMCache();
|
|
550
|
+
const results = collectLLMIntegrationMetrics({
|
|
551
|
+
totalCallsViaProvider: 0,
|
|
552
|
+
totalDirectCalls: 0,
|
|
553
|
+
cache,
|
|
554
|
+
rateLimiter: null,
|
|
555
|
+
validatedResponseCount: 0,
|
|
556
|
+
unvalidatedResponseCount: 0,
|
|
557
|
+
});
|
|
558
|
+
const m02 = results.find((r) => r.id === 'M-02')!;
|
|
559
|
+
expect(m02.pass).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('M-02 fails when no cache', () => {
|
|
563
|
+
const results = collectLLMIntegrationMetrics({
|
|
564
|
+
totalCallsViaProvider: 0,
|
|
565
|
+
totalDirectCalls: 0,
|
|
566
|
+
cache: null,
|
|
567
|
+
rateLimiter: null,
|
|
568
|
+
validatedResponseCount: 0,
|
|
569
|
+
unvalidatedResponseCount: 0,
|
|
570
|
+
});
|
|
571
|
+
const m02 = results.find((r) => r.id === 'M-02')!;
|
|
572
|
+
expect(m02.pass).toBe(false);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('M-03 passes when rate limiter configured', () => {
|
|
576
|
+
const rateLimiter = new TokenBucket({ rps: 10 });
|
|
577
|
+
const results = collectLLMIntegrationMetrics({
|
|
578
|
+
totalCallsViaProvider: 0,
|
|
579
|
+
totalDirectCalls: 0,
|
|
580
|
+
cache: null,
|
|
581
|
+
rateLimiter,
|
|
582
|
+
validatedResponseCount: 0,
|
|
583
|
+
unvalidatedResponseCount: 0,
|
|
584
|
+
});
|
|
585
|
+
const m03 = results.find((r) => r.id === 'M-03')!;
|
|
586
|
+
expect(m03.pass).toBe(true);
|
|
587
|
+
expect(m03.detail).toContain('10 req/sec');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('M-03 fails when no rate limiter', () => {
|
|
591
|
+
const results = collectLLMIntegrationMetrics({
|
|
592
|
+
totalCallsViaProvider: 0,
|
|
593
|
+
totalDirectCalls: 0,
|
|
594
|
+
cache: null,
|
|
595
|
+
rateLimiter: null,
|
|
596
|
+
validatedResponseCount: 0,
|
|
597
|
+
unvalidatedResponseCount: 0,
|
|
598
|
+
});
|
|
599
|
+
const m03 = results.find((r) => r.id === 'M-03')!;
|
|
600
|
+
expect(m03.pass).toBe(false);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('M-04 passes when all responses validated', () => {
|
|
604
|
+
const results = collectLLMIntegrationMetrics({
|
|
605
|
+
totalCallsViaProvider: 5,
|
|
606
|
+
totalDirectCalls: 0,
|
|
607
|
+
cache: null,
|
|
608
|
+
rateLimiter: null,
|
|
609
|
+
validatedResponseCount: 5,
|
|
610
|
+
unvalidatedResponseCount: 0,
|
|
611
|
+
});
|
|
612
|
+
const m04 = results.find((r) => r.id === 'M-04')!;
|
|
613
|
+
expect(m04.pass).toBe(true);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('M-04 fails when unvalidated responses exist', () => {
|
|
617
|
+
const results = collectLLMIntegrationMetrics({
|
|
618
|
+
totalCallsViaProvider: 5,
|
|
619
|
+
totalDirectCalls: 0,
|
|
620
|
+
cache: null,
|
|
621
|
+
rateLimiter: null,
|
|
622
|
+
validatedResponseCount: 3,
|
|
623
|
+
unvalidatedResponseCount: 2,
|
|
624
|
+
});
|
|
625
|
+
const m04 = results.find((r) => r.id === 'M-04')!;
|
|
626
|
+
expect(m04.pass).toBe(false);
|
|
627
|
+
expect(m04.detail).toContain('2 response(s) used without validation');
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('returns exactly 4 metrics', () => {
|
|
631
|
+
const results = collectLLMIntegrationMetrics({
|
|
632
|
+
totalCallsViaProvider: 0,
|
|
633
|
+
totalDirectCalls: 0,
|
|
634
|
+
cache: null,
|
|
635
|
+
rateLimiter: null,
|
|
636
|
+
validatedResponseCount: 0,
|
|
637
|
+
unvalidatedResponseCount: 0,
|
|
638
|
+
});
|
|
639
|
+
expect(results).toHaveLength(4);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('all metrics pass with full config', () => {
|
|
643
|
+
const cache = new LLMCache();
|
|
644
|
+
const rateLimiter = new TokenBucket({ rps: 5 });
|
|
645
|
+
const results = collectLLMIntegrationMetrics({
|
|
646
|
+
totalCallsViaProvider: 10,
|
|
647
|
+
totalDirectCalls: 0,
|
|
648
|
+
cache,
|
|
649
|
+
rateLimiter,
|
|
650
|
+
validatedResponseCount: 10,
|
|
651
|
+
unvalidatedResponseCount: 0,
|
|
652
|
+
});
|
|
653
|
+
for (const r of results) {
|
|
654
|
+
expect(r.pass).toBe(true);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": ".",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true
|
|
9
|
+
},
|
|
10
|
+
"include": [
|
|
11
|
+
"*.ts",
|
|
12
|
+
"providers/**/*.ts",
|
|
13
|
+
"cache/**/*.ts",
|
|
14
|
+
"rate_limit/**/*.ts",
|
|
15
|
+
"prompts/**/*.ts",
|
|
16
|
+
"validators/**/*.ts",
|
|
17
|
+
"tests/**/*.ts"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"node_modules",
|
|
21
|
+
"dist"
|
|
22
|
+
]
|
|
23
|
+
}
|