kiku-agent 1.3.0 → 1.3.2
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 +7 -7
- package/package.json +4 -7
- package/server/agent/agentCore.js +2 -2
- package/server/agent/catalogEntityResolver.js +8 -0
- package/server/agent/cli.js +4 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +298 -0
- package/server/agent/eval/modules/evaluation/scoring.js +351 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4796 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -0
- package/server/agent/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/prompts.js +1 -1
- package/server/agent/telemetry/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +127 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +251 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +594 -0
- package/server/agent/tests/cliAutomation.test.js +121 -0
- package/server/agent/tests/cloudProviders.test.js +234 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +417 -0
- package/server/agent/tests/contextIntelligence.test.js +162 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +215 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +312 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +192 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +319 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/modelManager.test.js +109 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +99 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/providerPersistence.test.js +39 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/securityAuditRemediation.test.js +214 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +232 -0
- package/server/agent/tests/sidebarControls.test.js +101 -0
- package/server/agent/tests/statefulMemory.test.js +270 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +126 -0
- package/server/agent/tests/textToolParsing.test.js +139 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +152 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +520 -0
- package/server/agent/tests/webChatInspection.test.js +300 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +261 -0
- package/server/agent/tests/webSources.test.js +247 -0
- package/server/agent/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/apiRouter.js +7 -11
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/tests/connection.test.js +156 -0
- package/server/routes/agent.js +55 -0
- package/server/supabase_schema.sql +63 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { compactToolResultForContext, estimateTextTokens } from '../contextCompactor.js';
|
|
4
|
+
import { sanitizeToolArguments } from '../textToolProtocol.js';
|
|
5
|
+
import { executeTool } from '../tools.js';
|
|
6
|
+
|
|
7
|
+
test('compactToolResultForContext strips SearchUrls and projects essential fields', () => {
|
|
8
|
+
const sampleResult = {
|
|
9
|
+
status: 'success',
|
|
10
|
+
works: [
|
|
11
|
+
{
|
|
12
|
+
id: 'in-blossom',
|
|
13
|
+
titleCn: '花间令',
|
|
14
|
+
titleEn: 'In Blossom',
|
|
15
|
+
year: 2024,
|
|
16
|
+
role: 'Yang Caiwei / Shangguan Zhi',
|
|
17
|
+
youtubeSearchUrl: 'https://youtube.com/results?search_query=fake',
|
|
18
|
+
bilibiliSearchUrl: 'https://bilibili.com/search?keyword=fake',
|
|
19
|
+
qqMusicSearchUrl: 'https://qq.com/search?w=fake',
|
|
20
|
+
unneededMetadata: 'huge payload string here'
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const compacted = compactToolResultForContext(sampleResult, { maxTokens: 800 });
|
|
26
|
+
assert.equal(compacted.works.length, 1);
|
|
27
|
+
const w = compacted.works[0];
|
|
28
|
+
assert.equal(w.titleCn, '花间令');
|
|
29
|
+
assert.equal(w.year, 2024);
|
|
30
|
+
assert.equal(w.youtubeSearchUrl, undefined);
|
|
31
|
+
assert.equal(w.bilibiliSearchUrl, undefined);
|
|
32
|
+
assert.equal(w.qqMusicSearchUrl, undefined);
|
|
33
|
+
assert.equal(w.unneededMetadata, undefined);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('compactToolResultForContext enforces token budget <= 800 tokens', () => {
|
|
37
|
+
const hugeWorks = [];
|
|
38
|
+
for (let i = 0; i < 50; i++) {
|
|
39
|
+
hugeWorks.push({
|
|
40
|
+
id: `work-${i}`,
|
|
41
|
+
titleCn: `作品 ${i}`,
|
|
42
|
+
titleEn: `Title ${i} with long description text repeating over and over again to bloat token count`,
|
|
43
|
+
synopsis: 'A very long storyline synopsis that would normally blow the prompt limit for smaller 4B models when multiple tools are run.',
|
|
44
|
+
year: 2020 + (i % 5),
|
|
45
|
+
role: `Role ${i}`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const compacted = compactToolResultForContext({ status: 'success', works: hugeWorks }, { maxTokens: 800 });
|
|
50
|
+
const serialized = JSON.stringify(compacted);
|
|
51
|
+
const tokens = estimateTextTokens(serialized);
|
|
52
|
+
assert.ok(tokens <= 800, `Expected <= 800 tokens, got ${tokens}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('sanitizeToolArguments prunes secret keys and coerces types', () => {
|
|
56
|
+
const schema = [
|
|
57
|
+
{
|
|
58
|
+
function: {
|
|
59
|
+
name: 'search_works',
|
|
60
|
+
parameters: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
year: { type: 'number' },
|
|
64
|
+
category: { type: 'string', enum: ['historical-drama', 'modern-drama', 'all'] },
|
|
65
|
+
query: { type: 'string' }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const raw = {
|
|
73
|
+
year: '2024',
|
|
74
|
+
category: 'Historical-Drama',
|
|
75
|
+
query: 'In Blossom',
|
|
76
|
+
KIKU_VIP_KEYS: 'secret_leak_attempt'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const clean = sanitizeToolArguments('search_works', raw, schema);
|
|
80
|
+
assert.equal(clean.year, 2024);
|
|
81
|
+
assert.equal(clean.category, 'historical-drama');
|
|
82
|
+
assert.equal(clean.query, 'In Blossom');
|
|
83
|
+
assert.equal(clean.KIKU_VIP_KEYS, undefined);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('get_profile_and_fandom returns targeted cat data when cat query is asked', async () => {
|
|
87
|
+
const res = await executeTool('get_profile_and_fandom', { query: 'แมว' });
|
|
88
|
+
assert.equal(res.status, 'success');
|
|
89
|
+
assert.ok(Array.isArray(res.cats));
|
|
90
|
+
assert.ok(res.cats.length > 0);
|
|
91
|
+
assert.ok(res.petsSummary);
|
|
92
|
+
assert.equal(res.profile, undefined);
|
|
93
|
+
assert.equal(res.talents, undefined);
|
|
94
|
+
assert.equal(res.stats, undefined);
|
|
95
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
HARDWARE_TIERS,
|
|
5
|
+
classifyHardwareTier,
|
|
6
|
+
detectGpuArchitecture,
|
|
7
|
+
calculateDynamicVramBudget
|
|
8
|
+
} from '../telemetry/hardwareTiers.js';
|
|
9
|
+
import { techPowerUpClient } from '../telemetry/techPowerUpClient.js';
|
|
10
|
+
import { resolveSystemHardwareTier } from '../vramService.js';
|
|
11
|
+
|
|
12
|
+
test('Hardware Tiers: classifies RTX 2060 6GB into Tier 1 with Q4_K_M KV cache', () => {
|
|
13
|
+
const tier = classifyHardwareTier({
|
|
14
|
+
name: 'NVIDIA GeForce RTX 2060',
|
|
15
|
+
totalMb: 6144,
|
|
16
|
+
vendor: 'NVIDIA'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
assert.equal(tier.id, 1);
|
|
20
|
+
assert.equal(tier.name, 'Tier 1: Entry AI');
|
|
21
|
+
assert.equal(tier.contextLimit, 8192);
|
|
22
|
+
assert.equal(tier.effectiveKvQuant, 'Q4_K_M');
|
|
23
|
+
assert.equal(tier.architecture.family, 'Turing');
|
|
24
|
+
assert.equal(tier.isConstrained, true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('Hardware Tiers: classifies RTX 4090 24GB into Tier 3 with native FP16', () => {
|
|
28
|
+
const tier = classifyHardwareTier({
|
|
29
|
+
name: 'NVIDIA GeForce RTX 4090',
|
|
30
|
+
totalMb: 24576,
|
|
31
|
+
vendor: 'NVIDIA'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(tier.id, 3);
|
|
35
|
+
assert.equal(tier.name, 'Tier 3: Enthusiast');
|
|
36
|
+
assert.equal(tier.contextLimit, 32768);
|
|
37
|
+
assert.equal(tier.effectiveKvQuant, 'FP16');
|
|
38
|
+
assert.equal(tier.architecture.family, 'Ada Lovelace');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('Hardware Tiers: detects Apple Silicon architecture flags and headroom requirement', () => {
|
|
42
|
+
const arch = detectGpuArchitecture('Apple M3 Max', 'Apple');
|
|
43
|
+
assert.equal(arch.family, 'Apple Silicon');
|
|
44
|
+
assert.equal(arch.requiresReservedHeadroom, true);
|
|
45
|
+
assert.ok(arch.headroomPercent >= 25);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Dynamic VRAM Budgeting: calculates correct KV budget and safe context limits', () => {
|
|
49
|
+
const budget = calculateDynamicVramBudget({
|
|
50
|
+
totalVramMb: 6144,
|
|
51
|
+
modelWeightsMb: 3900,
|
|
52
|
+
osSafetyMb: 700,
|
|
53
|
+
kvQuant: 'Q4_K_M'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.ok(budget.kvBudgetMb > 1000);
|
|
57
|
+
assert.equal(budget.isSafe, true);
|
|
58
|
+
assert.ok(budget.recommendedContextLimit >= 8192);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('TechPowerUp Disambiguation: correctly differentiates RTX 2060 6GB from 12GB variant', () => {
|
|
62
|
+
const spec6G = techPowerUpClient.disambiguateGpu('GeForce RTX 2060', 6.0);
|
|
63
|
+
assert.equal(spec6G.vramGb, 6);
|
|
64
|
+
assert.equal(spec6G.bandwidthGbs, 336.0);
|
|
65
|
+
|
|
66
|
+
const spec12G = techPowerUpClient.disambiguateGpu('GeForce RTX 2060 12GB', 12.0);
|
|
67
|
+
assert.equal(spec12G.vramGb, 12);
|
|
68
|
+
assert.equal(spec12G.bandwidthGbs, 336.0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('TechPowerUp Disambiguation: RTX 4060 Ti 16GB vs 8GB clamshell bus disambiguation', () => {
|
|
72
|
+
const spec16G = techPowerUpClient.disambiguateGpu('NVIDIA GeForce RTX 4060 Ti', 16.0);
|
|
73
|
+
assert.equal(spec16G.vramGb, 16);
|
|
74
|
+
assert.equal(spec16G.busBits, 128);
|
|
75
|
+
assert.equal(spec16G.bandwidthGbs, 288.0);
|
|
76
|
+
assert.equal(spec16G.memoryType, 'GDDR6');
|
|
77
|
+
|
|
78
|
+
const spec8G = techPowerUpClient.disambiguateGpu('NVIDIA GeForce RTX 4060 Ti', 8.0);
|
|
79
|
+
assert.equal(spec8G.vramGb, 8);
|
|
80
|
+
assert.equal(spec8G.busBits, 128);
|
|
81
|
+
assert.equal(spec8G.bandwidthGbs, 288.0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('TechPowerUp Disambiguation: RTX 3060 12GB (192-bit) vs 8GB (128-bit) vs Laptop (192-bit)', () => {
|
|
85
|
+
const spec12G = techPowerUpClient.disambiguateGpu('GeForce RTX 3060', 12.0);
|
|
86
|
+
assert.equal(spec12G.vramGb, 12);
|
|
87
|
+
assert.equal(spec12G.busBits, 192);
|
|
88
|
+
assert.equal(spec12G.bandwidthGbs, 360.0);
|
|
89
|
+
|
|
90
|
+
const spec8G = techPowerUpClient.disambiguateGpu('GeForce RTX 3060', 8.0);
|
|
91
|
+
assert.equal(spec8G.vramGb, 8);
|
|
92
|
+
assert.equal(spec8G.busBits, 128);
|
|
93
|
+
assert.equal(spec8G.bandwidthGbs, 240.0);
|
|
94
|
+
|
|
95
|
+
const specLaptop = techPowerUpClient.disambiguateGpu('GeForce RTX 3060 Laptop', 6.0);
|
|
96
|
+
assert.equal(specLaptop.vramGb, 6);
|
|
97
|
+
assert.equal(specLaptop.busBits, 192);
|
|
98
|
+
assert.equal(specLaptop.bandwidthGbs, 336.0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('TechPowerUp Disambiguation: RTX 3080 12GB (384-bit) vs 10GB (320-bit)', () => {
|
|
102
|
+
const spec12G = techPowerUpClient.disambiguateGpu('NVIDIA GeForce RTX 3080', 12.0);
|
|
103
|
+
assert.equal(spec12G.vramGb, 12);
|
|
104
|
+
assert.equal(spec12G.busBits, 384);
|
|
105
|
+
assert.equal(spec12G.bandwidthGbs, 912.0);
|
|
106
|
+
|
|
107
|
+
const spec10G = techPowerUpClient.disambiguateGpu('NVIDIA GeForce RTX 3080', 10.0);
|
|
108
|
+
assert.equal(spec10G.vramGb, 10);
|
|
109
|
+
assert.equal(spec10G.busBits, 320);
|
|
110
|
+
assert.equal(spec10G.bandwidthGbs, 760.3);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('TechPowerUp Disambiguation: RTX 4070 Ti SUPER 256-bit vs RTX 4070 Ti 192-bit', () => {
|
|
114
|
+
const superSpec = techPowerUpClient.disambiguateGpu('GeForce RTX 4070 Ti SUPER', 16.0);
|
|
115
|
+
assert.equal(superSpec.busBits, 256);
|
|
116
|
+
assert.equal(superSpec.bandwidthGbs, 672.3);
|
|
117
|
+
|
|
118
|
+
const nonSuperSpec = techPowerUpClient.disambiguateGpu('GeForce RTX 4070 Ti', 12.0);
|
|
119
|
+
assert.equal(nonSuperSpec.busBits, 192);
|
|
120
|
+
assert.equal(nonSuperSpec.bandwidthGbs, 504.2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('TechPowerUp Disambiguation: RTX 2080 Ti 22GB AI mod vs 11GB standard', () => {
|
|
124
|
+
const modSpec = techPowerUpClient.disambiguateGpu('GeForce RTX 2080 Ti', 22.0);
|
|
125
|
+
assert.equal(modSpec.vramGb, 22);
|
|
126
|
+
assert.equal(modSpec.busBits, 352);
|
|
127
|
+
assert.equal(modSpec.bandwidthGbs, 616.0);
|
|
128
|
+
|
|
129
|
+
const stdSpec = techPowerUpClient.disambiguateGpu('GeForce RTX 2080 Ti', 11.0);
|
|
130
|
+
assert.equal(stdSpec.vramGb, 11);
|
|
131
|
+
assert.equal(stdSpec.busBits, 352);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('TechPowerUp Disambiguation: Homelab legendary accelerators (Tesla P40 & T4)', () => {
|
|
135
|
+
const p40 = techPowerUpClient.disambiguateGpu('NVIDIA Tesla P40', 24.0);
|
|
136
|
+
assert.equal(p40.vramGb, 24);
|
|
137
|
+
assert.equal(p40.busBits, 384);
|
|
138
|
+
assert.equal(p40.bandwidthGbs, 345.6);
|
|
139
|
+
|
|
140
|
+
const t4 = techPowerUpClient.disambiguateGpu('NVIDIA Tesla T4', 16.0);
|
|
141
|
+
assert.equal(t4.vramGb, 16);
|
|
142
|
+
assert.equal(t4.busBits, 128);
|
|
143
|
+
assert.equal(t4.bandwidthGbs, 320.0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('TechPowerUp Disambiguation: Apple Silicon unified memory bus widths (M3 Max & Ultra)', () => {
|
|
147
|
+
const m3Max36 = techPowerUpClient.disambiguateGpu('Apple M3 Max', 36.0);
|
|
148
|
+
assert.equal(m3Max36.busBits, 384);
|
|
149
|
+
assert.equal(m3Max36.bandwidthGbs, 300.0);
|
|
150
|
+
assert.equal(m3Max36.isUnifiedMemory, true);
|
|
151
|
+
|
|
152
|
+
const m3Max128 = techPowerUpClient.disambiguateGpu('Apple M3 Max', 128.0);
|
|
153
|
+
assert.equal(m3Max128.busBits, 512);
|
|
154
|
+
assert.equal(m3Max128.bandwidthGbs, 400.0);
|
|
155
|
+
|
|
156
|
+
const ultra = techPowerUpClient.disambiguateGpu('Apple M2 Ultra', 192.0);
|
|
157
|
+
assert.equal(ultra.busBits, 1024);
|
|
158
|
+
assert.equal(ultra.bandwidthGbs, 800.0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('TechPowerUp Disambiguation: AMD Radeon & APU hardware (RX 7900 XTX, Radeon VII, Strix Halo)', () => {
|
|
162
|
+
const xtx = techPowerUpClient.disambiguateGpu('AMD Radeon RX 7900 XTX', 24.0);
|
|
163
|
+
assert.equal(xtx.busBits, 384);
|
|
164
|
+
assert.equal(xtx.bandwidthGbs, 960.0);
|
|
165
|
+
|
|
166
|
+
const vii = techPowerUpClient.disambiguateGpu('AMD Radeon VII', 16.0);
|
|
167
|
+
assert.equal(vii.busBits, 4096);
|
|
168
|
+
assert.equal(vii.bandwidthGbs, 1024.0);
|
|
169
|
+
|
|
170
|
+
const halo = techPowerUpClient.disambiguateGpu('AMD Ryzen AI Max+ 395 Strix Halo', 128.0);
|
|
171
|
+
assert.equal(halo.busBits, 256);
|
|
172
|
+
assert.equal(halo.bandwidthGbs, 256.0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('TechPowerUp Disambiguation: Intel Arc A770 16GB vs 8GB and Gaudi 3', () => {
|
|
176
|
+
const a770_16 = techPowerUpClient.disambiguateGpu('Intel Arc A770', 16.0);
|
|
177
|
+
assert.equal(a770_16.vramGb, 16);
|
|
178
|
+
assert.equal(a770_16.bandwidthGbs, 560.0);
|
|
179
|
+
|
|
180
|
+
const a770_8 = techPowerUpClient.disambiguateGpu('Intel Arc A770', 8.0);
|
|
181
|
+
assert.equal(a770_8.vramGb, 8);
|
|
182
|
+
assert.equal(a770_8.bandwidthGbs, 512.0);
|
|
183
|
+
|
|
184
|
+
const gaudi = techPowerUpClient.disambiguateGpu('Intel Gaudi 3', 128.0);
|
|
185
|
+
assert.equal(gaudi.busBits, 8192);
|
|
186
|
+
assert.equal(gaudi.bandwidthGbs, 3700.0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('TechPowerUp Disambiguation: fallback heuristic for uncataloged GPU', () => {
|
|
190
|
+
const uncataloged = techPowerUpClient.disambiguateGpu('Experimental AI Chip', 16.0);
|
|
191
|
+
assert.equal(uncataloged.busBits, 256);
|
|
192
|
+
assert.equal(uncataloged.bandwidthGbs, 672);
|
|
193
|
+
assert.equal(uncataloged.approximate, true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('TechPowerUp Throughput: calculates theoretical max TPS based on memory bandwidth', () => {
|
|
197
|
+
const tps = techPowerUpClient.calculateTheoreticalTps(336.0, 4.0);
|
|
198
|
+
assert.equal(tps, 84.0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('VRAM Service: resolveSystemHardwareTier returns full architecture and tier metadata', async () => {
|
|
202
|
+
const resolved = await resolveSystemHardwareTier();
|
|
203
|
+
assert.ok(resolved.tier);
|
|
204
|
+
assert.ok(resolved.tier.contextLimit > 0);
|
|
205
|
+
assert.ok(resolved.budget.recommendedContextLimit > 0);
|
|
206
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
canonicalizeUrl,
|
|
6
|
+
fuseSearchResults,
|
|
7
|
+
createSearchResult,
|
|
8
|
+
TokenBucketLimiter,
|
|
9
|
+
CircuitBreaker,
|
|
10
|
+
SearchResponseCache,
|
|
11
|
+
assertSafeUrlHostname,
|
|
12
|
+
isPrivateIPv4,
|
|
13
|
+
isPrivateIPv6,
|
|
14
|
+
MetaSearchOrchestrator,
|
|
15
|
+
SearchTelemetry
|
|
16
|
+
} from '../websearch/index.js';
|
|
17
|
+
import { searchWeb } from '../webSearch.js';
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
test('URL Canonicalization strips tracking parameters, www, and trailing slashes', () => {
|
|
22
|
+
const url1 = 'http://www.example.com/drama/?utm_source=twitter&utm_medium=social&fbclid=12345#overview';
|
|
23
|
+
const clean1 = canonicalizeUrl(url1);
|
|
24
|
+
assert.equal(clean1, 'https://example.com/drama');
|
|
25
|
+
|
|
26
|
+
const url2 = 'https://news.example.com/article/?ref=share&gclid=xyz&q=kiku&category=music';
|
|
27
|
+
const clean2 = canonicalizeUrl(url2);
|
|
28
|
+
assert.equal(clean2, 'https://news.example.com/article?category=music&q=kiku');
|
|
29
|
+
|
|
30
|
+
const urlRoot = 'https://www.example.com/';
|
|
31
|
+
assert.equal(canonicalizeUrl(urlRoot), 'https://example.com/');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('Reciprocal Rank Fusion (RRF) correctly scores and deduplicates cross-engine results', () => {
|
|
35
|
+
|
|
36
|
+
const ddgResults = [
|
|
37
|
+
createSearchResult({ link: 'https://example.com/page-a', title: 'Page A (DDG)', snippet: 'Snippet A from DDG', engine: 'duckduckgo' }),
|
|
38
|
+
createSearchResult({ link: 'https://example.com/page-b', title: 'Page B', snippet: 'Snippet B from DDG', engine: 'duckduckgo' })
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const searxResults = [
|
|
43
|
+
createSearchResult({ link: 'https://www.example.com/page-a/', title: 'Page A (SearXNG)', snippet: 'Longer Snippet A from SearXNG', engine: 'searxng' }),
|
|
44
|
+
createSearchResult({ link: 'https://example.com/page-c', title: 'Page C', snippet: 'Snippet C from SearXNG', engine: 'searxng' })
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const fused = fuseSearchResults([ddgResults, searxResults], { k: 60, limit: 10 });
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
assert.equal(fused[0].link, 'https://example.com/page-a');
|
|
51
|
+
assert.equal(fused[0].metadata.engines.length, 2);
|
|
52
|
+
assert.ok(fused[0].score > fused[1].score);
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const expectedScoreA = (1 / 61) + (1 / 61);
|
|
56
|
+
assert.ok(Math.abs(fused[0].score - expectedScoreA) < 0.0001);
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
assert.equal(fused[0].snippet, 'Longer Snippet A from SearXNG');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
test('TokenBucketLimiter enforces rate limiting and burst capacity', async () => {
|
|
65
|
+
const limiter = new TokenBucketLimiter({ name: 'test_limiter', capacity: 2, refillRatePerSec: 10 });
|
|
66
|
+
assert.equal(limiter.tryAcquire(1), true);
|
|
67
|
+
assert.equal(limiter.tryAcquire(1), true);
|
|
68
|
+
|
|
69
|
+
assert.equal(limiter.tryAcquire(1), false);
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const acquired = await limiter.acquire(1, 1000);
|
|
73
|
+
assert.equal(acquired, true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('CircuitBreaker transitions to OPEN on 3 consecutive failures and recovers on cooldown', () => {
|
|
77
|
+
const breaker = new CircuitBreaker({ name: 'test_breaker', failureThreshold: 3, cooldownMs: 100 });
|
|
78
|
+
assert.equal(breaker.canExecute(), true);
|
|
79
|
+
|
|
80
|
+
breaker.recordFailure(new Error('Fail 1'));
|
|
81
|
+
assert.equal(breaker.canExecute(), true);
|
|
82
|
+
|
|
83
|
+
breaker.recordFailure(new Error('Fail 2'));
|
|
84
|
+
assert.equal(breaker.canExecute(), true);
|
|
85
|
+
|
|
86
|
+
breaker.recordFailure(new Error('Fail 3'));
|
|
87
|
+
|
|
88
|
+
assert.equal(breaker.canExecute(), false);
|
|
89
|
+
assert.equal(breaker.state, 'OPEN');
|
|
90
|
+
assert.equal(breaker.tripCount, 1);
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
breaker.reset();
|
|
94
|
+
assert.equal(breaker.canExecute(), true);
|
|
95
|
+
assert.equal(breaker.state, 'CLOSED');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('SearchResponseCache enforces TTL and LRU max size', () => {
|
|
99
|
+
const cache = new SearchResponseCache(2);
|
|
100
|
+
cache.set('key1', { data: 1 }, 10000);
|
|
101
|
+
cache.set('key2', { data: 2 }, 10000);
|
|
102
|
+
|
|
103
|
+
assert.deepEqual(cache.get('key1'), { data: 1 });
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
cache.set('key3', { data: 3 }, 10000);
|
|
107
|
+
assert.equal(cache.has('key1'), true);
|
|
108
|
+
assert.equal(cache.has('key2'), false);
|
|
109
|
+
assert.equal(cache.has('key3'), true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('SSRF Defense rejects private IPs, loopback, and metadata addresses', async () => {
|
|
113
|
+
|
|
114
|
+
assert.equal(isPrivateIPv4('127.0.0.1'), true);
|
|
115
|
+
assert.equal(isPrivateIPv4('10.0.0.5'), true);
|
|
116
|
+
assert.equal(isPrivateIPv4('172.16.50.1'), true);
|
|
117
|
+
assert.equal(isPrivateIPv4('192.168.1.100'), true);
|
|
118
|
+
assert.equal(isPrivateIPv4('169.254.169.254'), true);
|
|
119
|
+
assert.equal(isPrivateIPv4('8.8.8.8'), false);
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
assert.equal(isPrivateIPv6('::1'), true);
|
|
123
|
+
assert.equal(isPrivateIPv6('fe80::1'), true);
|
|
124
|
+
assert.equal(isPrivateIPv6('fc00::1'), true);
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
const loopback = await assertSafeUrlHostname('localhost');
|
|
128
|
+
assert.equal(loopback.safe, false);
|
|
129
|
+
|
|
130
|
+
const internalIp = await assertSafeUrlHostname('192.168.0.1');
|
|
131
|
+
assert.equal(internalIp.safe, false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
test('MetaSearchOrchestrator generates multilingual query fan-out', () => {
|
|
137
|
+
const orchestrator = new MetaSearchOrchestrator();
|
|
138
|
+
const thFanOut = orchestrator.expandQuery('จวีจิ้งอี ผลงาน');
|
|
139
|
+
assert.ok(thFanOut.includes('Ju Jingyi ผลงาน'));
|
|
140
|
+
assert.ok(thFanOut.includes('鞠婧祎 ผลงาน'));
|
|
141
|
+
|
|
142
|
+
const enFanOut = orchestrator.expandQuery('Ju Jingyi drama list');
|
|
143
|
+
assert.ok(enFanOut.includes('鞠婧祎 drama list'));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('MetaSearchOrchestrator handles engine fallbacks and dual consumption modes', async () => {
|
|
147
|
+
const telemetry = new SearchTelemetry();
|
|
148
|
+
const orchestrator = new MetaSearchOrchestrator({
|
|
149
|
+
telemetry,
|
|
150
|
+
fallbackTimeoutMs: 500
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
let callCount = 0;
|
|
155
|
+
orchestrator._searchSingleEngine = async (engine, query, options) => {
|
|
156
|
+
callCount++;
|
|
157
|
+
if (engine === 'searxng') {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
if (engine === 'duckduckgo') {
|
|
161
|
+
return [
|
|
162
|
+
createSearchResult({ link: 'https://example.com/ju-jingyi', title: 'Ju Jingyi Official', snippet: 'Artist bio and works', engine: 'duckduckgo' })
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
return [];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
const bypassRes = await orchestrator.executeSearch('Ju Jingyi bio', { mode: 'bypass', noCache: true });
|
|
170
|
+
assert.equal(bypassRes.status, 'success');
|
|
171
|
+
assert.equal(bypassRes.results.length, 1);
|
|
172
|
+
assert.equal(bypassRes.results[0].link, 'https://example.com/ju-jingyi');
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
const metrics = telemetry.getMetrics();
|
|
176
|
+
assert.equal(metrics.totalSearches, 1);
|
|
177
|
+
assert.ok(metrics.searchLatencyP95Ms >= 0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
test('searchWeb returns backward-compatible format with hybrid meta search', async () => {
|
|
183
|
+
|
|
184
|
+
const errRes = await searchWeb(null);
|
|
185
|
+
assert.equal(errRes.status, 'error');
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
const res = await searchWeb('Ju Jingyi news', 5, { noCache: true });
|
|
189
|
+
assert.ok(['success', 'no_results'].includes(res.status));
|
|
190
|
+
assert.equal(typeof res.count, 'number');
|
|
191
|
+
assert.ok(Array.isArray(res.results));
|
|
192
|
+
});
|