bluera-knowledge 0.9.38 → 0.9.40
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/.env.example +12 -0
- package/CHANGELOG.md +53 -0
- package/CLAUDE.md +6 -3
- package/README.md +33 -5
- package/dist/{chunk-XJFV7AJW.js → chunk-HUEWT6U5.js} +90 -15
- package/dist/chunk-HUEWT6U5.js.map +1 -0
- package/dist/{chunk-ZAWIPEYX.js → chunk-IZWOEBFM.js} +2 -2
- package/dist/{chunk-36IFANFI.js → chunk-TIGPI3BE.js} +15 -6
- package/dist/chunk-TIGPI3BE.js.map +1 -0
- package/dist/index.js +22 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/package.json +7 -6
- package/plugin.json +1 -1
- package/src/cli/commands/search.ts +22 -4
- package/src/mcp/handlers/search.handler.ts +7 -2
- package/src/mcp/schemas/index.ts +5 -0
- package/src/mcp/server.ts +5 -0
- package/src/services/search.service.test.ts +191 -3
- package/src/services/search.service.ts +121 -18
- package/src/types/search.ts +8 -0
- package/dist/chunk-36IFANFI.js.map +0 -1
- package/dist/chunk-XJFV7AJW.js.map +0 -1
- /package/dist/{chunk-ZAWIPEYX.js.map → chunk-IZWOEBFM.js.map} +0 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
IntelligentCrawler
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-IZWOEBFM.js";
|
|
5
5
|
import {
|
|
6
6
|
JobService,
|
|
7
7
|
createDocumentId,
|
|
8
8
|
createServices,
|
|
9
9
|
createStoreId
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-HUEWT6U5.js";
|
|
11
11
|
import "../chunk-6FHWC36B.js";
|
|
12
12
|
|
|
13
13
|
// src/workers/background-worker.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bluera-knowledge",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.40",
|
|
4
4
|
"description": "CLI tool for managing knowledge stores with semantic search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"lint:quiet": "eslint src/ --quiet && echo '✓ Lint passed' || { echo '❌ ESLint errors'; exit 1; }",
|
|
29
29
|
"lint:deadcode:quiet": "knip --no-exit-code --quiet 2>&1 | grep -E '^(Unused|Configuration)' || echo '✓ No unused code'",
|
|
30
30
|
"typecheck:quiet": "tsc --noEmit --pretty false && echo '✓ Type check passed'",
|
|
31
|
-
"test:changed:quiet": "vitest run --changed --reporter=dot --silent 2>&1 | tail -3",
|
|
32
|
-
"test:coverage:quiet": "vitest run --coverage --reporter=dot 2>&1 | grep -E
|
|
31
|
+
"test:changed:quiet": "bash -c 'set -o pipefail && vitest run --changed --reporter=dot --silent 2>&1 | tail -3'",
|
|
32
|
+
"test:coverage:quiet": "bash -c 'set -o pipefail && vitest run --coverage --reporter=dot 2>&1 | grep -E \"(FAIL|Test Files|passed|Coverage|threshold)\" | tail -10'",
|
|
33
33
|
"build:quiet": "tsup --silent && echo '✓ Build complete'",
|
|
34
|
+
"prerelease": "bun run format:check:quiet && bun run lint:quiet && bun run lint:deadcode:quiet && bun run typecheck:quiet && bun run test:coverage:quiet && bun run build:quiet",
|
|
34
35
|
"prepare": "husky",
|
|
35
|
-
"version:major": "commit-and-tag-version --release-as major --skip.commit --skip.tag",
|
|
36
|
-
"version:minor": "commit-and-tag-version --release-as minor --skip.commit --skip.tag",
|
|
37
|
-
"version:patch": "commit-and-tag-version --release-as patch --skip.commit --skip.tag",
|
|
36
|
+
"version:major": "bun run prerelease && commit-and-tag-version --release-as major --skip.commit --skip.tag",
|
|
37
|
+
"version:minor": "bun run prerelease && commit-and-tag-version --release-as minor --skip.commit --skip.tag",
|
|
38
|
+
"version:patch": "bun run prerelease && commit-and-tag-version --release-as patch --skip.commit --skip.tag",
|
|
38
39
|
"release:patch": "commit-and-tag-version --release-as patch && git push --follow-tags",
|
|
39
40
|
"release:minor": "commit-and-tag-version --release-as minor && git push --follow-tags",
|
|
40
41
|
"release:major": "commit-and-tag-version --release-as major && git push --follow-tags"
|
package/plugin.json
CHANGED
|
@@ -18,6 +18,10 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
|
|
|
18
18
|
)
|
|
19
19
|
.option('-n, --limit <count>', 'Maximum results to return (default: 10)', '10')
|
|
20
20
|
.option('-t, --threshold <score>', 'Minimum score 0-1; omit low-relevance results')
|
|
21
|
+
.option(
|
|
22
|
+
'--min-relevance <score>',
|
|
23
|
+
'Minimum raw cosine similarity 0-1; returns empty if no results meet threshold'
|
|
24
|
+
)
|
|
21
25
|
.option('--include-content', 'Show full document content, not just preview snippet')
|
|
22
26
|
.option(
|
|
23
27
|
'--detail <level>',
|
|
@@ -32,6 +36,7 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
|
|
|
32
36
|
mode?: SearchMode;
|
|
33
37
|
limit?: string;
|
|
34
38
|
threshold?: string;
|
|
39
|
+
minRelevance?: string;
|
|
35
40
|
includeContent?: boolean;
|
|
36
41
|
detail?: DetailLevel;
|
|
37
42
|
}
|
|
@@ -82,6 +87,8 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
|
|
|
82
87
|
limit: parseInt(options.limit ?? '10', 10),
|
|
83
88
|
threshold:
|
|
84
89
|
options.threshold !== undefined ? parseFloat(options.threshold) : undefined,
|
|
90
|
+
minRelevance:
|
|
91
|
+
options.minRelevance !== undefined ? parseFloat(options.minRelevance) : undefined,
|
|
85
92
|
includeContent: options.includeContent,
|
|
86
93
|
detail: options.detail ?? 'minimal',
|
|
87
94
|
});
|
|
@@ -96,12 +103,23 @@ export function createSearchCommand(getOptions: () => GlobalOptions): Command {
|
|
|
96
103
|
}
|
|
97
104
|
} else {
|
|
98
105
|
console.log(`\nSearch: "${query}"`);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
106
|
+
|
|
107
|
+
// Build status line with optional confidence info
|
|
108
|
+
let statusLine = `Mode: ${results.mode} | Detail: ${String(options.detail)} | Stores: ${String(results.stores.length)} | Results: ${String(results.totalResults)} | Time: ${String(results.timeMs)}ms`;
|
|
109
|
+
if (results.confidence !== undefined) {
|
|
110
|
+
statusLine += ` | Confidence: ${results.confidence}`;
|
|
111
|
+
}
|
|
112
|
+
if (results.maxRawScore !== undefined) {
|
|
113
|
+
statusLine += ` | MaxRaw: ${results.maxRawScore.toFixed(3)}`;
|
|
114
|
+
}
|
|
115
|
+
console.log(`${statusLine}\n`);
|
|
102
116
|
|
|
103
117
|
if (results.results.length === 0) {
|
|
104
|
-
|
|
118
|
+
if (results.confidence === 'low') {
|
|
119
|
+
console.log('No sufficiently relevant results found.\n');
|
|
120
|
+
} else {
|
|
121
|
+
console.log('No results found.\n');
|
|
122
|
+
}
|
|
105
123
|
} else {
|
|
106
124
|
for (let i = 0; i < results.results.length; i++) {
|
|
107
125
|
const r = results.results[i];
|
|
@@ -72,6 +72,7 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
72
72
|
mode: 'hybrid',
|
|
73
73
|
limit: validated.limit,
|
|
74
74
|
detail: validated.detail,
|
|
75
|
+
minRelevance: validated.minRelevance,
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
const results = await services.search.search(searchQuery);
|
|
@@ -107,6 +108,8 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
107
108
|
totalResults: results.totalResults,
|
|
108
109
|
mode: results.mode,
|
|
109
110
|
timeMs: results.timeMs,
|
|
111
|
+
confidence: results.confidence,
|
|
112
|
+
maxRawScore: results.maxRawScore,
|
|
110
113
|
},
|
|
111
114
|
null,
|
|
112
115
|
2
|
|
@@ -115,8 +118,10 @@ export const handleSearch: ToolHandler<SearchArgs> = async (
|
|
|
115
118
|
// Calculate actual token estimate based on response content
|
|
116
119
|
const responseTokens = estimateTokens(responseJson);
|
|
117
120
|
|
|
118
|
-
// Create visible header with token usage
|
|
119
|
-
const
|
|
121
|
+
// Create visible header with token usage and confidence
|
|
122
|
+
const confidenceInfo =
|
|
123
|
+
results.confidence !== undefined ? ` | Confidence: ${results.confidence}` : '';
|
|
124
|
+
const header = `Search: "${validated.query}" | Results: ${String(results.totalResults)} | ${formatTokenCount(responseTokens)} tokens | ${String(results.timeMs)}ms${confidenceInfo}\n\n`;
|
|
120
125
|
|
|
121
126
|
// Log the complete MCP response that will be sent to Claude Code
|
|
122
127
|
logger.info(
|
package/src/mcp/schemas/index.ts
CHANGED
|
@@ -28,6 +28,11 @@ export const SearchArgsSchema = z.object({
|
|
|
28
28
|
detail: z.enum(['minimal', 'contextual', 'full']).default('minimal'),
|
|
29
29
|
limit: z.number().int().positive().default(10),
|
|
30
30
|
stores: z.array(z.string()).optional(),
|
|
31
|
+
minRelevance: z
|
|
32
|
+
.number()
|
|
33
|
+
.min(0, 'minRelevance must be between 0 and 1')
|
|
34
|
+
.max(1, 'minRelevance must be between 0 and 1')
|
|
35
|
+
.optional(),
|
|
31
36
|
});
|
|
32
37
|
|
|
33
38
|
export type SearchArgs = z.infer<typeof SearchArgsSchema>;
|
package/src/mcp/server.ts
CHANGED
|
@@ -70,6 +70,11 @@ export function createMCPServer(options: MCPServerOptions): Server {
|
|
|
70
70
|
items: { type: 'string' },
|
|
71
71
|
description: 'Specific store IDs to search (optional)',
|
|
72
72
|
},
|
|
73
|
+
minRelevance: {
|
|
74
|
+
type: 'number',
|
|
75
|
+
description:
|
|
76
|
+
'Minimum raw cosine similarity (0-1). Returns empty if no results meet threshold. Use to filter irrelevant results.',
|
|
77
|
+
},
|
|
73
78
|
},
|
|
74
79
|
required: ['query'],
|
|
75
80
|
},
|
|
@@ -1161,6 +1161,7 @@ describe('SearchService - Edge Cases', () => {
|
|
|
1161
1161
|
});
|
|
1162
1162
|
|
|
1163
1163
|
it('handles threshold parameter for vector search', async () => {
|
|
1164
|
+
// Setup: multiple results with varying scores
|
|
1164
1165
|
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
1165
1166
|
{
|
|
1166
1167
|
id: createDocumentId('doc1'),
|
|
@@ -1168,22 +1169,41 @@ describe('SearchService - Edge Cases', () => {
|
|
|
1168
1169
|
content: 'high score',
|
|
1169
1170
|
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1170
1171
|
},
|
|
1172
|
+
{
|
|
1173
|
+
id: createDocumentId('doc2'),
|
|
1174
|
+
score: 0.5,
|
|
1175
|
+
content: 'medium score',
|
|
1176
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
id: createDocumentId('doc3'),
|
|
1180
|
+
score: 0.3,
|
|
1181
|
+
content: 'low score',
|
|
1182
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1183
|
+
},
|
|
1171
1184
|
]);
|
|
1172
1185
|
|
|
1186
|
+
// Search with high threshold
|
|
1173
1187
|
const results = await searchService.search({
|
|
1174
1188
|
query: 'test',
|
|
1175
1189
|
stores: [storeId],
|
|
1176
1190
|
mode: 'vector',
|
|
1177
1191
|
limit: 10,
|
|
1178
|
-
threshold: 0.
|
|
1192
|
+
threshold: 0.8,
|
|
1179
1193
|
});
|
|
1180
1194
|
|
|
1195
|
+
// Verify lanceStore.search was called (without checking threshold param - it's unused in LanceStore)
|
|
1181
1196
|
expect(vi.mocked(mockLanceStore.search)).toHaveBeenCalledWith(
|
|
1182
1197
|
storeId,
|
|
1183
1198
|
expect.anything(),
|
|
1184
|
-
expect.anything()
|
|
1185
|
-
0.9
|
|
1199
|
+
expect.anything()
|
|
1186
1200
|
);
|
|
1201
|
+
|
|
1202
|
+
// Verify threshold filtering works: only high-score result should pass
|
|
1203
|
+
// After normalization, the top result has score 1.0, and threshold 0.8 filters out lower ones
|
|
1204
|
+
// The normalized scores are: doc1=1.0, doc2=0.31, doc3=0.0 (relative to max 0.95)
|
|
1205
|
+
expect(results.results.length).toBe(1);
|
|
1206
|
+
expect(results.results[0]?.id).toBe('doc1');
|
|
1187
1207
|
});
|
|
1188
1208
|
});
|
|
1189
1209
|
|
|
@@ -1992,3 +2012,171 @@ describe('SearchService - Threshold Filtering', () => {
|
|
|
1992
2012
|
expect(results.query).toBe('test query');
|
|
1993
2013
|
});
|
|
1994
2014
|
});
|
|
2015
|
+
|
|
2016
|
+
describe('SearchService - Raw Score and Confidence', () => {
|
|
2017
|
+
let mockLanceStore: LanceStore;
|
|
2018
|
+
let mockEmbeddingEngine: EmbeddingEngine;
|
|
2019
|
+
let searchService: SearchService;
|
|
2020
|
+
const storeId = createStoreId('test-store');
|
|
2021
|
+
|
|
2022
|
+
beforeEach(() => {
|
|
2023
|
+
mockLanceStore = {
|
|
2024
|
+
search: vi.fn(),
|
|
2025
|
+
fullTextSearch: vi.fn(),
|
|
2026
|
+
} as unknown as LanceStore;
|
|
2027
|
+
|
|
2028
|
+
mockEmbeddingEngine = {
|
|
2029
|
+
embed: vi.fn().mockResolvedValue([0.1, 0.2, 0.3]),
|
|
2030
|
+
} as unknown as EmbeddingEngine;
|
|
2031
|
+
|
|
2032
|
+
searchService = new SearchService(mockLanceStore, mockEmbeddingEngine);
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
it('exposes rawVectorScore in rankingMetadata for hybrid search', async () => {
|
|
2036
|
+
// Mock results with known raw vector scores
|
|
2037
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
2038
|
+
{
|
|
2039
|
+
id: createDocumentId('doc1'),
|
|
2040
|
+
score: 0.85, // Raw cosine similarity
|
|
2041
|
+
content: 'vector result',
|
|
2042
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
id: createDocumentId('doc2'),
|
|
2046
|
+
score: 0.65,
|
|
2047
|
+
content: 'another vector result',
|
|
2048
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
2049
|
+
},
|
|
2050
|
+
]);
|
|
2051
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
2052
|
+
|
|
2053
|
+
const results = await searchService.search({
|
|
2054
|
+
query: 'test query',
|
|
2055
|
+
stores: [storeId],
|
|
2056
|
+
mode: 'hybrid',
|
|
2057
|
+
limit: 10,
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
// Verify results have rawVectorScore in rankingMetadata
|
|
2061
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
2062
|
+
const firstResult = results.results[0];
|
|
2063
|
+
expect(firstResult?.rankingMetadata).toBeDefined();
|
|
2064
|
+
expect(firstResult?.rankingMetadata?.rawVectorScore).toBeDefined();
|
|
2065
|
+
expect(firstResult?.rankingMetadata?.rawVectorScore).toBe(0.85);
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
it('returns confidence level based on maxRawScore', async () => {
|
|
2069
|
+
// Mock results with high raw vector score (>= 0.5)
|
|
2070
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
2071
|
+
{
|
|
2072
|
+
id: createDocumentId('doc1'),
|
|
2073
|
+
score: 0.6, // High confidence threshold
|
|
2074
|
+
content: 'high score result',
|
|
2075
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
2076
|
+
},
|
|
2077
|
+
]);
|
|
2078
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
2079
|
+
|
|
2080
|
+
const results = await searchService.search({
|
|
2081
|
+
query: 'test query',
|
|
2082
|
+
stores: [storeId],
|
|
2083
|
+
mode: 'hybrid',
|
|
2084
|
+
limit: 10,
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
expect(results.confidence).toBe('high');
|
|
2088
|
+
expect(results.maxRawScore).toBe(0.6);
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
it('returns medium confidence for scores between 0.3 and 0.5', async () => {
|
|
2092
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
2093
|
+
{
|
|
2094
|
+
id: createDocumentId('doc1'),
|
|
2095
|
+
score: 0.4, // Medium confidence
|
|
2096
|
+
content: 'medium score result',
|
|
2097
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
2098
|
+
},
|
|
2099
|
+
]);
|
|
2100
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
2101
|
+
|
|
2102
|
+
const results = await searchService.search({
|
|
2103
|
+
query: 'test query',
|
|
2104
|
+
stores: [storeId],
|
|
2105
|
+
mode: 'hybrid',
|
|
2106
|
+
limit: 10,
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
expect(results.confidence).toBe('medium');
|
|
2110
|
+
expect(results.maxRawScore).toBe(0.4);
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
it('returns low confidence for scores below 0.3', async () => {
|
|
2114
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
2115
|
+
{
|
|
2116
|
+
id: createDocumentId('doc1'),
|
|
2117
|
+
score: 0.2, // Low confidence
|
|
2118
|
+
content: 'low score result',
|
|
2119
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
2120
|
+
},
|
|
2121
|
+
]);
|
|
2122
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
2123
|
+
|
|
2124
|
+
const results = await searchService.search({
|
|
2125
|
+
query: 'test query',
|
|
2126
|
+
stores: [storeId],
|
|
2127
|
+
mode: 'hybrid',
|
|
2128
|
+
limit: 10,
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
expect(results.confidence).toBe('low');
|
|
2132
|
+
expect(results.maxRawScore).toBe(0.2);
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
it('filters results with minRelevance based on raw score', async () => {
|
|
2136
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
2137
|
+
{
|
|
2138
|
+
id: createDocumentId('doc1'),
|
|
2139
|
+
score: 0.25, // Below minRelevance threshold
|
|
2140
|
+
content: 'low relevance result',
|
|
2141
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
2142
|
+
},
|
|
2143
|
+
]);
|
|
2144
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
2145
|
+
|
|
2146
|
+
const results = await searchService.search({
|
|
2147
|
+
query: 'irrelevant query',
|
|
2148
|
+
stores: [storeId],
|
|
2149
|
+
mode: 'hybrid',
|
|
2150
|
+
limit: 10,
|
|
2151
|
+
minRelevance: 0.4, // Filter out results below this raw score
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
// Should return empty since max raw score (0.25) < minRelevance (0.4)
|
|
2155
|
+
expect(results.results.length).toBe(0);
|
|
2156
|
+
expect(results.confidence).toBe('low');
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
it('returns results when maxRawScore meets minRelevance', async () => {
|
|
2160
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
2161
|
+
{
|
|
2162
|
+
id: createDocumentId('doc1'),
|
|
2163
|
+
score: 0.5, // Above minRelevance threshold
|
|
2164
|
+
content: 'relevant result',
|
|
2165
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
2166
|
+
},
|
|
2167
|
+
]);
|
|
2168
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
2169
|
+
|
|
2170
|
+
const results = await searchService.search({
|
|
2171
|
+
query: 'relevant query',
|
|
2172
|
+
stores: [storeId],
|
|
2173
|
+
mode: 'hybrid',
|
|
2174
|
+
limit: 10,
|
|
2175
|
+
minRelevance: 0.4,
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
// Should return results since max raw score (0.5) >= minRelevance (0.4)
|
|
2179
|
+
expect(results.results.length).toBe(1);
|
|
2180
|
+
expect(results.confidence).toBe('high');
|
|
2181
|
+
});
|
|
2182
|
+
});
|