thatgfsj-code 3.0.4 → 3.0.7
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 +64 -21
- package/dist/app/index.d.ts +93 -22
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +232 -40
- package/dist/app/index.js.map +1 -1
- package/dist/cmd/index.d.ts +12 -20
- package/dist/cmd/index.d.ts.map +1 -1
- package/dist/cmd/index.js +192 -146
- package/dist/cmd/index.js.map +1 -1
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +1 -1
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +116 -84
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/gemini.d.ts +27 -3
- package/dist/llm/gemini.d.ts.map +1 -1
- package/dist/llm/gemini.js +196 -73
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/index.d.ts +20 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +66 -6
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +5 -1
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +126 -66
- package/dist/llm/openai.js.map +1 -1
- package/dist/mcp/client.d.ts +84 -34
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +291 -126
- package/dist/mcp/client.js.map +1 -1
- package/dist/session/compactor.d.ts +27 -7
- package/dist/session/compactor.d.ts.map +1 -1
- package/dist/session/compactor.js +78 -23
- package/dist/session/compactor.js.map +1 -1
- package/dist/session/index.d.ts +96 -33
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +278 -60
- package/dist/session/index.js.map +1 -1
- package/dist/tools/file.d.ts +10 -2
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +54 -5
- package/dist/tools/file.js.map +1 -1
- package/dist/tools/git.d.ts +16 -2
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +61 -18
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/nwt.d.ts.map +1 -1
- package/dist/tools/nwt.js +8 -1
- package/dist/tools/nwt.js.map +1 -1
- package/dist/tools/search.d.ts +8 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +105 -34
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/shell.d.ts +0 -4
- package/dist/tools/shell.d.ts.map +1 -1
- package/dist/tools/shell.js +11 -26
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js.map +1 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +90 -18
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/ChatMessage.d.ts.map +1 -1
- package/dist/tui/components/ChatMessage.js +9 -2
- package/dist/tui/components/ChatMessage.js.map +1 -1
- package/dist/tui/components/ConfirmPrompt.d.ts +21 -0
- package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
- package/dist/tui/components/ConfirmPrompt.js +45 -0
- package/dist/tui/components/ConfirmPrompt.js.map +1 -0
- package/dist/tui/components/Header.d.ts +10 -5
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +13 -5
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/StatusBar.d.ts +7 -0
- package/dist/tui/components/StatusBar.d.ts.map +1 -1
- package/dist/tui/components/StatusBar.js +8 -3
- package/dist/tui/components/StatusBar.js.map +1 -1
- package/dist/tui/components/Thinking.d.ts +5 -0
- package/dist/tui/components/Thinking.d.ts.map +1 -1
- package/dist/tui/components/Thinking.js +24 -2
- package/dist/tui/components/Thinking.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts +6 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +50 -22
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/components/UserInput.d.ts +5 -0
- package/dist/tui/components/UserInput.d.ts.map +1 -1
- package/dist/tui/components/UserInput.js +9 -13
- package/dist/tui/components/UserInput.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts +1 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +36 -16
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts +7 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +77 -41
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/dist/tui/theme.d.ts +34 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +34 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +9 -7
- package/dist/tui/welcome.js.map +1 -1
- package/dist/types.d.ts +16 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/diff.d.ts +5 -0
- package/dist/utils/diff.d.ts.map +1 -1
- package/dist/utils/diff.js +15 -0
- package/dist/utils/diff.js.map +1 -1
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +29 -0
- package/dist/version.js.map +1 -0
- package/package.json +19 -7
- package/CHANGELOG.md +0 -192
- package/DEVELOPMENT.md +0 -286
- package/ROADMAP.md +0 -91
- package/docs/API_KEY_GUIDE.md +0 -236
- package/docs/FAQ.md +0 -182
- package/install.bat +0 -63
- package/install.ps1 +0 -238
- package/install.sh +0 -113
- package/src/app/index.ts +0 -197
- package/src/cache/fingerprint.ts +0 -101
- package/src/cache/index.ts +0 -21
- package/src/cache/smartModel.ts +0 -133
- package/src/cache/stats.ts +0 -199
- package/src/cache/volatile.ts +0 -47
- package/src/cmd/index.tsx +0 -288
- package/src/config/index.ts +0 -156
- package/src/config/providers.ts +0 -234
- package/src/config/types.ts +0 -71
- package/src/hooks/index.ts +0 -111
- package/src/llm/anthropic.ts +0 -413
- package/src/llm/gemini.ts +0 -169
- package/src/llm/index.ts +0 -303
- package/src/llm/openai.ts +0 -243
- package/src/llm/provider.ts +0 -71
- package/src/mcp/client.ts +0 -330
- package/src/prompts/index.ts +0 -260
- package/src/session/compactor.ts +0 -103
- package/src/session/index.ts +0 -181
- package/src/session/message.ts +0 -42
- package/src/skills/brainstorming.ts +0 -43
- package/src/skills/code-review.ts +0 -45
- package/src/skills/executing-plans.ts +0 -27
- package/src/skills/frontend-design.ts +0 -35
- package/src/skills/git-workflow.ts +0 -36
- package/src/skills/improve-architecture.ts +0 -38
- package/src/skills/index.ts +0 -136
- package/src/skills/neuroweave.ts +0 -47
- package/src/skills/playwright.ts +0 -72
- package/src/skills/prototype.ts +0 -30
- package/src/skills/subagent.ts +0 -28
- package/src/skills/supabase.ts +0 -44
- package/src/skills/systematic-debugging.ts +0 -44
- package/src/skills/tdd.ts +0 -39
- package/src/skills/triage.ts +0 -35
- package/src/skills/verification.ts +0 -31
- package/src/skills/writing-plans.ts +0 -42
- package/src/tools/file.ts +0 -143
- package/src/tools/git.ts +0 -132
- package/src/tools/index.ts +0 -111
- package/src/tools/nwt.ts +0 -598
- package/src/tools/search.ts +0 -263
- package/src/tools/shell.ts +0 -136
- package/src/tools/types.ts +0 -122
- package/src/tui/app.tsx +0 -159
- package/src/tui/components/ChatList.tsx +0 -41
- package/src/tui/components/ChatMessage.tsx +0 -54
- package/src/tui/components/Header.tsx +0 -66
- package/src/tui/components/InitWizard.tsx +0 -247
- package/src/tui/components/Markdown.tsx +0 -35
- package/src/tui/components/ModelSelector.tsx +0 -107
- package/src/tui/components/StatusBar.tsx +0 -30
- package/src/tui/components/Thinking.tsx +0 -17
- package/src/tui/components/ToolCall.tsx +0 -102
- package/src/tui/components/UserInput.tsx +0 -151
- package/src/tui/hooks/useChat.ts +0 -287
- package/src/tui/hooks/useCommands.ts +0 -271
- package/src/tui/index.ts +0 -6
- package/src/tui/welcome.ts +0 -177
- package/src/types.ts +0 -104
- package/src/utils/diff.ts +0 -71
- package/src/utils/project.ts +0 -99
- package/src/utils/stableStringify.ts +0 -47
- package/src/utils/thinking.ts +0 -119
- package/tests/cache/fingerprint.test.ts +0 -75
- package/tests/cache/providerCatalog.test.ts +0 -31
- package/tests/cache/smartModel.test.ts +0 -100
- package/tests/cache/stableStringify.test.ts +0 -43
- package/tests/cache/stats.test.ts +0 -146
- package/tests/cache/volatile.test.ts +0 -75
- package/tests/smoke-pollution.mjs +0 -78
- package/tests/smoke-thinking.mjs +0 -110
- package/tests/smoke-tool-stream.mjs +0 -69
- package/tests/smoke-tool.mjs +0 -117
- package/tsconfig.json +0 -21
package/src/utils/project.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project Context - Detect project type and gather info
|
|
3
|
-
* Migrated from old src/utils/project-context.ts + getProjectContext() from index.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
|
|
9
|
-
export interface ProjectInfo {
|
|
10
|
-
type: string;
|
|
11
|
-
name: string;
|
|
12
|
-
path: string;
|
|
13
|
-
fileCount: number;
|
|
14
|
-
dependencies: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface PackageJson {
|
|
18
|
-
name?: string;
|
|
19
|
-
version?: string;
|
|
20
|
-
dependencies?: Record<string, string>;
|
|
21
|
-
devDependencies?: Record<string, string>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class ProjectContext {
|
|
25
|
-
/**
|
|
26
|
-
* Get project context string for display
|
|
27
|
-
*/
|
|
28
|
-
static getSummary(cwd: string = process.cwd()): string {
|
|
29
|
-
const info: string[] = [];
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const pkgPath = join(cwd, 'package.json');
|
|
33
|
-
if (existsSync(pkgPath)) {
|
|
34
|
-
const pkg: PackageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
35
|
-
info.push(`📦 Project: ${cwd}`);
|
|
36
|
-
info.push(` Name: ${pkg.name || 'unknown'}`);
|
|
37
|
-
const depCount = Object.keys(pkg.dependencies || {}).length;
|
|
38
|
-
if (depCount > 0) info.push(` Deps: ${depCount} packages`);
|
|
39
|
-
} else {
|
|
40
|
-
info.push(`📁 Working dir: ${cwd}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const fileCount = ProjectContext.countCodeFiles(cwd);
|
|
44
|
-
if (fileCount > 0) {
|
|
45
|
-
info.push(` Files: ${fileCount} code files`);
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
info.push(`📁 Working dir: ${cwd}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return info.join('\n');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Count code files in directory (skipping node_modules, .git, etc.)
|
|
56
|
-
*/
|
|
57
|
-
static countCodeFiles(dir: string, maxDepth = 3): number {
|
|
58
|
-
let count = 0;
|
|
59
|
-
const codeExts = /\.(ts|js|py|go|rs|java|cpp|c|h|tsx|jsx|vue|svelte)$/;
|
|
60
|
-
|
|
61
|
-
const walk = (d: string, depth: number) => {
|
|
62
|
-
if (depth > maxDepth) return;
|
|
63
|
-
try {
|
|
64
|
-
const items = readdirSync(d);
|
|
65
|
-
for (const item of items) {
|
|
66
|
-
if (['node_modules', '.git', 'dist', 'build', '__pycache__'].includes(item)) continue;
|
|
67
|
-
const fullPath = join(d, item);
|
|
68
|
-
try {
|
|
69
|
-
const stat = statSync(fullPath);
|
|
70
|
-
if (stat.isDirectory()) {
|
|
71
|
-
walk(fullPath, depth + 1);
|
|
72
|
-
} else if (codeExts.test(item)) {
|
|
73
|
-
count++;
|
|
74
|
-
}
|
|
75
|
-
} catch {
|
|
76
|
-
// Skip inaccessible files
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
// Skip inaccessible directories
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
walk(dir, 0);
|
|
85
|
-
return count;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Detect project type from files
|
|
90
|
-
*/
|
|
91
|
-
static detectType(cwd: string = process.cwd()): string {
|
|
92
|
-
if (existsSync(join(cwd, 'package.json'))) return 'node';
|
|
93
|
-
if (existsSync(join(cwd, 'Cargo.toml'))) return 'rust';
|
|
94
|
-
if (existsSync(join(cwd, 'go.mod'))) return 'go';
|
|
95
|
-
if (existsSync(join(cwd, 'pyproject.toml')) || existsSync(join(cwd, 'requirements.txt'))) return 'python';
|
|
96
|
-
if (existsSync(join(cwd, 'build.gradle'))) return 'java';
|
|
97
|
-
return 'unknown';
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic JSON serializer.
|
|
3
|
-
*
|
|
4
|
-
* JSON.stringify in V8 iterates keys in insertion order, which means objects
|
|
5
|
-
* built with literal `{a, b, c}` are stable, but anything built via spread,
|
|
6
|
-
* Object.fromEntries, or merged maps can produce different byte sequences
|
|
7
|
-
* between requests. That byte-sequence instability is fatal for prompt-cache
|
|
8
|
-
* hit-rate: a single reordered key invalidates the cache fingerprint.
|
|
9
|
-
*
|
|
10
|
-
* stableStringify sorts object keys lexicographically before recursing, so
|
|
11
|
-
* any pair of structurally equal objects produces identical JSON text.
|
|
12
|
-
*
|
|
13
|
-
* Performance note: O(n log n) on key counts per object. For typical request
|
|
14
|
-
* bodies (<200 KB) the overhead is negligible (<5 ms). The main hot path is
|
|
15
|
-
* the tool/parameters tree which is small and stable in practice.
|
|
16
|
-
*
|
|
17
|
-
* Reference: Reasonix-style "stableStringify" requirement for prefix-cache
|
|
18
|
-
* byte-level stability (see DeepSeek Reasonix, Section 3).
|
|
19
|
-
*/
|
|
20
|
-
export function stableStringify(value: unknown): string {
|
|
21
|
-
return stringify(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function stringify(value: unknown): string {
|
|
25
|
-
if (value === null) return 'null';
|
|
26
|
-
if (value === undefined) return 'null'; // OpenAI wire format does not carry undefined
|
|
27
|
-
const t = typeof value;
|
|
28
|
-
if (t === 'string') return JSON.stringify(value);
|
|
29
|
-
if (t === 'number') return Number.isFinite(value as number) ? String(value) : 'null';
|
|
30
|
-
if (t === 'boolean') return value ? 'true' : 'false';
|
|
31
|
-
if (Array.isArray(value)) {
|
|
32
|
-
return '[' + value.map((v) => stringify(v)).join(',') + ']';
|
|
33
|
-
}
|
|
34
|
-
if (t === 'object') {
|
|
35
|
-
const obj = value as Record<string, unknown>;
|
|
36
|
-
const keys = Object.keys(obj).sort();
|
|
37
|
-
const parts: string[] = [];
|
|
38
|
-
for (const k of keys) {
|
|
39
|
-
const v = obj[k];
|
|
40
|
-
if (v === undefined) continue; // omit undefined to match OpenAI/Anthropic convention
|
|
41
|
-
parts.push(JSON.stringify(k) + ':' + stringify(v));
|
|
42
|
-
}
|
|
43
|
-
return '{' + parts.join(',') + '}';
|
|
44
|
-
}
|
|
45
|
-
// functions / symbols / bigints: drop
|
|
46
|
-
return 'null';
|
|
47
|
-
}
|
package/src/utils/thinking.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thinking block extraction + compression
|
|
3
|
-
*
|
|
4
|
-
* Models that aren't truly reasoning-capable (Qwen, DeepSeek-V3 chat,
|
|
5
|
-
* Kimi, etc.) still emit `<think>...</think>` blocks as plain text
|
|
6
|
-
* inside their response. These blocks can be hundreds of lines of
|
|
7
|
-
* internal monologue that the user doesn't want to see scroll by.
|
|
8
|
-
*
|
|
9
|
-
* opencode handles this by separating reasoning into its own stream
|
|
10
|
-
* part at the protocol level. We don't have that luxury — the model
|
|
11
|
-
* is emitting everything as a single `content` field. So we use the
|
|
12
|
-
* same regex-strip approach opencode uses internally (see
|
|
13
|
-
* packages/opencode/src/session/prompt.ts line 244).
|
|
14
|
-
*
|
|
15
|
-
* Three block delimiters are supported:
|
|
16
|
-
* 1. <think>...</think> — DeepSeek / Qwen / Kimi
|
|
17
|
-
* 2. <reasoning>...</reasoning> — OpenRouter-style
|
|
18
|
-
* 3. [THINK]...[/THINK] — some custom fine-tunes
|
|
19
|
-
*
|
|
20
|
-
* Usage:
|
|
21
|
-
* const { thinking, conclusion } = splitThinking(fullContent);
|
|
22
|
-
* if (thinking && compress) {
|
|
23
|
-
* render(`💭 ${summarize(thinking)}\n${conclusion}`);
|
|
24
|
-
* } else {
|
|
25
|
-
* render(fullContent);
|
|
26
|
-
* }
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
export interface ThinkingSplit {
|
|
30
|
-
/** Raw `<think>...` content, or empty if none. */
|
|
31
|
-
thinking: string;
|
|
32
|
-
/** Content with thinking blocks stripped + trimmed. */
|
|
33
|
-
conclusion: string;
|
|
34
|
-
/** How many lines the thinking block spanned. */
|
|
35
|
-
thinkingLines: number;
|
|
36
|
-
/** First non-empty line of the thinking block (heuristic summary). */
|
|
37
|
-
thinkingHint: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* All known thinking-block delimiters, in priority order. The regex
|
|
42
|
-
* flags `gi` (global, case-insensitive) so `[THINK]` and `<think>`
|
|
43
|
-
* are both caught, and so multiple blocks collapse cleanly.
|
|
44
|
-
*/
|
|
45
|
-
const THINKING_PATTERNS: RegExp[] = [
|
|
46
|
-
/<think>[\s\S]*?<\/think>/gi,
|
|
47
|
-
/<reasoning>[\s\S]*?<\/reasoning>/gi,
|
|
48
|
-
/\[THINK\][\s\S]*?\[\/THINK\]/gi,
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
export function splitThinking(content: string): ThinkingSplit {
|
|
52
|
-
if (!content) {
|
|
53
|
-
return { thinking: '', conclusion: '', thinkingLines: 0, thinkingHint: '' };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let thinking = '';
|
|
57
|
-
let remaining = content;
|
|
58
|
-
|
|
59
|
-
for (const pat of THINKING_PATTERNS) {
|
|
60
|
-
const matches = remaining.match(pat);
|
|
61
|
-
if (matches) {
|
|
62
|
-
thinking += matches.join('\n');
|
|
63
|
-
remaining = remaining.replace(pat, '');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const conclusion = remaining.trim();
|
|
68
|
-
const thinkingLines = thinking ? thinking.split('\n').length : 0;
|
|
69
|
-
const thinkingHint = firstNonEmptyLine(thinking);
|
|
70
|
-
|
|
71
|
-
return { thinking, conclusion, thinkingLines, thinkingHint };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Build a short, single-line summary of a thinking block, suitable
|
|
76
|
-
* for collapsed display. Returns something like:
|
|
77
|
-
* "💭 thought for 24 lines: The user is asking about Win+E..."
|
|
78
|
-
* Falls back to "(no hint)" if the block is empty.
|
|
79
|
-
*/
|
|
80
|
-
export function summarizeThinking(split: ThinkingSplit): string {
|
|
81
|
-
if (!split.thinking) return '';
|
|
82
|
-
const hint = split.thinkingHint.length > 60
|
|
83
|
-
? split.thinkingHint.slice(0, 57) + '...'
|
|
84
|
-
: split.thinkingHint;
|
|
85
|
-
return `💭 thought for ${split.thinkingLines} line${split.thinkingLines === 1 ? '' : 's'}${hint ? `: ${hint}` : ''}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Render full content with thinking blocks compressed (collapsed to
|
|
90
|
-
* a single-line indicator). If `showThinking` is true, returns the
|
|
91
|
-
* content unchanged (debug mode).
|
|
92
|
-
*/
|
|
93
|
-
export function compressThinking(content: string, showThinking: boolean = false): string {
|
|
94
|
-
if (showThinking || !content) return content;
|
|
95
|
-
const split = splitThinking(content);
|
|
96
|
-
if (!split.thinking) return content;
|
|
97
|
-
const summary = summarizeThinking(split);
|
|
98
|
-
return summary ? `${summary}\n${split.conclusion}` : split.conclusion;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function firstNonEmptyLine(s: string): string {
|
|
102
|
-
if (!s) return '';
|
|
103
|
-
for (const line of s.split('\n')) {
|
|
104
|
-
// Strip any leading/trailing thinking-tag fragments that may have
|
|
105
|
-
// been captured by the regex (e.g. "<think>The user said..."
|
|
106
|
-
// or "...</think>"). We strip ALL occurrences to handle the
|
|
107
|
-
// case where the first line wraps across a tag.
|
|
108
|
-
const t = line
|
|
109
|
-
.trim()
|
|
110
|
-
.replace(/<\/?think>/gi, '')
|
|
111
|
-
.replace(/<\/?reasoning>/gi, '')
|
|
112
|
-
.replace(/\[\/?THINK\]/gi, '')
|
|
113
|
-
.trim();
|
|
114
|
-
if (t) return t;
|
|
115
|
-
}
|
|
116
|
-
return '';
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export { THINKING_PATTERNS };
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { fingerprint, fingerprintTools, fingerprintSystemPrefix } from '../../src/cache/fingerprint.js';
|
|
3
|
-
import type { Tool } from '../../src/tools/types.js';
|
|
4
|
-
import type { SystemSegment } from '../../src/prompts/index.js';
|
|
5
|
-
|
|
6
|
-
describe('fingerprint', () => {
|
|
7
|
-
it('returns same hash for structurally equal objects', () => {
|
|
8
|
-
const a = { name: 'file', description: 'Read files', inputSchema: { type: 'object' } };
|
|
9
|
-
const b = { inputSchema: { type: 'object' }, description: 'Read files', name: 'file' };
|
|
10
|
-
expect(fingerprint(a)).toBe(fingerprint(b));
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('returns different hash for different content', () => {
|
|
14
|
-
expect(fingerprint({ name: 'a' })).not.toBe(fingerprint({ name: 'b' }));
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns 16-char hex string', () => {
|
|
18
|
-
const fp = fingerprint({ x: 1 });
|
|
19
|
-
expect(fp).toMatch(/^[0-9a-f]{16}$/);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('fingerprintTools ignores non-wire Tool metadata', () => {
|
|
23
|
-
// Two Tool objects with same name/description/inputSchema but different
|
|
24
|
-
// internal metadata (e.g. version, internal flags) must produce the
|
|
25
|
-
// same fingerprint, because only the three wire fields matter for
|
|
26
|
-
// upstream prompt cache lookup.
|
|
27
|
-
const t1: Tool = {
|
|
28
|
-
name: 'file',
|
|
29
|
-
description: 'Read/write files',
|
|
30
|
-
parameters: [],
|
|
31
|
-
inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
32
|
-
execute: async () => ({ success: true }),
|
|
33
|
-
// @ts-expect-error: simulate extra internal metadata
|
|
34
|
-
version: '1.0.0',
|
|
35
|
-
};
|
|
36
|
-
const t2: Tool = {
|
|
37
|
-
name: 'file',
|
|
38
|
-
description: 'Read/write files',
|
|
39
|
-
parameters: [],
|
|
40
|
-
inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
|
|
41
|
-
execute: async () => ({ success: true }),
|
|
42
|
-
// @ts-expect-error
|
|
43
|
-
version: '2.0.0',
|
|
44
|
-
};
|
|
45
|
-
expect(fingerprintTools([t1])).toBe(fingerprintTools([t2]));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('fingerprintSystemPrefix excludes volatile segments', () => {
|
|
49
|
-
const segments: SystemSegment[] = [
|
|
50
|
-
{ name: 'identity', content: 'You are Thatgfsj', volatile: false },
|
|
51
|
-
{ name: 'tools', content: '## Tools\nfile, shell', volatile: false },
|
|
52
|
-
{ name: 'nwt', content: 'Recent: did X, did Y, did Z', volatile: true },
|
|
53
|
-
{ name: 'date', content: '2026-08-08 12:34:56', volatile: true },
|
|
54
|
-
];
|
|
55
|
-
// Hash should be stable when only the volatile segments change.
|
|
56
|
-
const baseFp = fingerprintSystemPrefix(segments);
|
|
57
|
-
|
|
58
|
-
const segmentsMutated: SystemSegment[] = [
|
|
59
|
-
{ name: 'identity', content: 'You are Thatgfsj', volatile: false },
|
|
60
|
-
{ name: 'tools', content: '## Tools\nfile, shell', volatile: false },
|
|
61
|
-
{ name: 'nwt', content: 'Recent: did X, did Y, did ZZZZZZ', volatile: true }, // mutated
|
|
62
|
-
{ name: 'date', content: '2099-12-31 00:00:00', volatile: true }, // mutated
|
|
63
|
-
];
|
|
64
|
-
expect(fingerprintSystemPrefix(segmentsMutated)).toBe(baseFp);
|
|
65
|
-
|
|
66
|
-
// But change to an immutable segment should break the hash.
|
|
67
|
-
const segmentsIdentityChanged: SystemSegment[] = [
|
|
68
|
-
{ name: 'identity', content: 'You are a different agent', volatile: false },
|
|
69
|
-
{ name: 'tools', content: '## Tools\nfile, shell', volatile: false },
|
|
70
|
-
{ name: 'nwt', content: 'Recent: did X, did Y, did Z', volatile: true },
|
|
71
|
-
{ name: 'date', content: '2026-08-08 12:34:56', volatile: true },
|
|
72
|
-
];
|
|
73
|
-
expect(fingerprintSystemPrefix(segmentsIdentityChanged)).not.toBe(baseFp);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { getModelsForProvider, PROVIDERS } from '../../src/config/providers.js';
|
|
3
|
-
|
|
4
|
-
describe('ModelSelector catalog integration', () => {
|
|
5
|
-
it('deepseek provider has deepseek-chat and deepseek-reasoner', () => {
|
|
6
|
-
const models = getModelsForProvider('deepseek');
|
|
7
|
-
const ids = models.map(m => m.id);
|
|
8
|
-
expect(ids).toContain('deepseek-chat');
|
|
9
|
-
expect(ids).toContain('deepseek-reasoner');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('siliconflow provider has Qwen models', () => {
|
|
13
|
-
const models = getModelsForProvider('siliconflow');
|
|
14
|
-
const ids = models.map(m => m.id);
|
|
15
|
-
expect(ids).toContain('Qwen/Qwen2.5-7B-Instruct');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('custom_openai provider has gpt-4o fallback', () => {
|
|
19
|
-
const models = getModelsForProvider('custom_openai');
|
|
20
|
-
const ids = models.map(m => m.id);
|
|
21
|
-
expect(ids).toContain('gpt-4o');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('all known providers have a non-empty catalog', () => {
|
|
25
|
-
const knownProviders = Object.keys(PROVIDERS);
|
|
26
|
-
for (const p of knownProviders) {
|
|
27
|
-
const models = getModelsForProvider(p as any);
|
|
28
|
-
expect(models.length, `provider ${p} should have at least one model`).toBeGreaterThan(0);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { decideTTL, totalConversationChars, shouldDowngrade } from '../../src/cache/smartModel.js';
|
|
3
|
-
import type { ChatMessage } from '../../src/types.js';
|
|
4
|
-
|
|
5
|
-
function userMsg(s: string): ChatMessage {
|
|
6
|
-
return { role: 'user', content: s };
|
|
7
|
-
}
|
|
8
|
-
function assistantMsg(s: string): ChatMessage {
|
|
9
|
-
return { role: 'assistant', content: s };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe('decideTTL', () => {
|
|
13
|
-
it('defaults to 1h (long-task) on round 0 — task length unknown', () => {
|
|
14
|
-
const d = decideTTL([], null);
|
|
15
|
-
expect(d.ttl).toBe('1h');
|
|
16
|
-
expect(d.isFirstDecision).toBe(true);
|
|
17
|
-
expect(d.reason).toBe('default-long-task');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('defaults to 1h even for a small conversation — cannot predict future length', () => {
|
|
21
|
-
const messages = [
|
|
22
|
-
userMsg('hello'),
|
|
23
|
-
assistantMsg('hi'),
|
|
24
|
-
userMsg('how are you?'),
|
|
25
|
-
assistantMsg('good'),
|
|
26
|
-
];
|
|
27
|
-
const d = decideTTL(messages, null);
|
|
28
|
-
expect(d.ttl).toBe('1h');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('defaults to 1h for a large conversation too', () => {
|
|
32
|
-
const messages: ChatMessage[] = [];
|
|
33
|
-
for (let i = 0; i < 16; i++) {
|
|
34
|
-
messages.push(userMsg(`question ${i}`));
|
|
35
|
-
messages.push(assistantMsg(`answer ${i} long enough to be content`));
|
|
36
|
-
}
|
|
37
|
-
const d = decideTTL(messages, null);
|
|
38
|
-
expect(d.ttl).toBe('1h');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('reuses previous TTL (stability rule)', () => {
|
|
42
|
-
const messages = [userMsg('hello'), assistantMsg('hi')];
|
|
43
|
-
const d1 = decideTTL(messages, null);
|
|
44
|
-
expect(d1.ttl).toBe('1h');
|
|
45
|
-
expect(d1.isFirstDecision).toBe(true);
|
|
46
|
-
|
|
47
|
-
// Once pinned to 5m by the user, later rounds keep 5m.
|
|
48
|
-
const d2 = decideTTL(messages, '5m');
|
|
49
|
-
expect(d2.ttl).toBe('5m');
|
|
50
|
-
expect(d2.isFirstDecision).toBe(false);
|
|
51
|
-
expect(d2.reason).toBe('reused-from-previous-round');
|
|
52
|
-
|
|
53
|
-
// And once pinned to 1h, later rounds keep 1h.
|
|
54
|
-
const d3 = decideTTL(messages, '1h');
|
|
55
|
-
expect(d3.ttl).toBe('1h');
|
|
56
|
-
expect(d3.isFirstDecision).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('totalConversationChars', () => {
|
|
61
|
-
it('sums string content', () => {
|
|
62
|
-
const total = totalConversationChars([
|
|
63
|
-
userMsg('abc'),
|
|
64
|
-
assistantMsg('defgh'),
|
|
65
|
-
]);
|
|
66
|
-
expect(total).toBe(8);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('handles array content blocks', () => {
|
|
70
|
-
const total = totalConversationChars([
|
|
71
|
-
{ role: 'user', content: [
|
|
72
|
-
{ type: 'text', text: 'hello' },
|
|
73
|
-
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'XXXX' } },
|
|
74
|
-
] as any },
|
|
75
|
-
]);
|
|
76
|
-
expect(total).toBe(5);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('shouldDowngrade', () => {
|
|
81
|
-
it('downgrades short prompts without tool calls', () => {
|
|
82
|
-
const d = shouldDowngrade([], 'hi');
|
|
83
|
-
expect(d.downgrade).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('does not downgrade long prompts', () => {
|
|
87
|
-
const d = shouldDowngrade([], 'x'.repeat(300));
|
|
88
|
-
expect(d.downgrade).toBe(false);
|
|
89
|
-
expect(d.reason).toBe('prompt-too-long');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('does not downgrade when recent tool calls exist', () => {
|
|
93
|
-
const messages: ChatMessage[] = [
|
|
94
|
-
{ role: 'assistant', content: '...', tool_calls: [{ id: '1', type: 'function', function: { name: 'f', arguments: '{}' } }] },
|
|
95
|
-
];
|
|
96
|
-
const d = shouldDowngrade(messages, 'hi');
|
|
97
|
-
expect(d.downgrade).toBe(false);
|
|
98
|
-
expect(d.reason).toBe('recent-tool-call');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { stableStringify } from '../../src/utils/stableStringify.js';
|
|
3
|
-
|
|
4
|
-
describe('stableStringify', () => {
|
|
5
|
-
it('produces identical output for keys inserted in different orders', () => {
|
|
6
|
-
const a = { name: 'file', description: 'Read files', inputSchema: { type: 'object' } };
|
|
7
|
-
const b = { inputSchema: { type: 'object' }, description: 'Read files', name: 'file' };
|
|
8
|
-
expect(stableStringify(a)).toBe(stableStringify(b));
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('omits undefined values (matches OpenAI/Anthropic wire convention)', () => {
|
|
12
|
-
expect(stableStringify({ a: 1, b: undefined, c: 2 })).toBe(stableStringify({ a: 1, c: 2 }));
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('handles nested objects recursively', () => {
|
|
16
|
-
const a = { tools: [{ name: 'a', args: { x: 1 } }, { name: 'b', args: { y: 2 } }] };
|
|
17
|
-
const b = { tools: [{ args: { x: 1 }, name: 'a' }, { args: { y: 2 }, name: 'b' }] };
|
|
18
|
-
expect(stableStringify(a)).toBe(stableStringify(b));
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('handles primitives correctly', () => {
|
|
22
|
-
expect(stableStringify(null)).toBe('null');
|
|
23
|
-
expect(stableStringify('hello')).toBe('"hello"');
|
|
24
|
-
expect(stableStringify(42)).toBe('42');
|
|
25
|
-
expect(stableStringify(true)).toBe('true');
|
|
26
|
-
expect(stableStringify(false)).toBe('false');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('escapes special characters in strings', () => {
|
|
30
|
-
expect(stableStringify('hello\nworld')).toBe('"hello\\nworld"');
|
|
31
|
-
expect(stableStringify('"quoted"')).toBe('"\\"quoted\\""');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('drops NaN and Infinity to null (JSON-incompatible)', () => {
|
|
35
|
-
expect(stableStringify(NaN)).toBe('null');
|
|
36
|
-
expect(stableStringify(Infinity)).toBe('null');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('produces empty {} for empty objects and [] for empty arrays', () => {
|
|
40
|
-
expect(stableStringify({})).toBe('{}');
|
|
41
|
-
expect(stableStringify([])).toBe('[]');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { CacheStatsStore, estimateSavingsCNY } from '../../src/cache/stats.js';
|
|
6
|
-
import type { Usage } from '../../src/types.js';
|
|
7
|
-
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
let statsPath: string;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
tmpDir = mkdtempSync(join(tmpdir(), 'cache-stats-'));
|
|
13
|
-
statsPath = join(tmpDir, 'cache-stats.json');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('CacheStatsStore', () => {
|
|
21
|
-
it('starts empty when no file exists', () => {
|
|
22
|
-
const store = new CacheStatsStore(statsPath);
|
|
23
|
-
const s = store.snapshot();
|
|
24
|
-
expect(s.totalRequests).toBe(0);
|
|
25
|
-
expect(s.totalReadTokens).toBe(0);
|
|
26
|
-
expect(s.totalCreationTokens).toBe(0);
|
|
27
|
-
expect(s.totalInputTokens).toBe(0);
|
|
28
|
-
expect(s.hitRate).toBe(0);
|
|
29
|
-
expect(s.estimatedSavingsCNY).toBe(0);
|
|
30
|
-
expect(s.history).toEqual([]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('records Anthropic usage (cache_read / cache_creation)', () => {
|
|
34
|
-
const store = new CacheStatsStore(statsPath);
|
|
35
|
-
const usage: Usage = {
|
|
36
|
-
prompt_tokens: 1000,
|
|
37
|
-
completion_tokens: 200,
|
|
38
|
-
total_tokens: 1200,
|
|
39
|
-
cache_creation_input_tokens: 800,
|
|
40
|
-
cache_read_input_tokens: 100,
|
|
41
|
-
};
|
|
42
|
-
store.record(usage);
|
|
43
|
-
|
|
44
|
-
const s = store.snapshot();
|
|
45
|
-
expect(s.totalRequests).toBe(1);
|
|
46
|
-
expect(s.totalReadTokens).toBe(100);
|
|
47
|
-
expect(s.totalCreationTokens).toBe(800);
|
|
48
|
-
expect(s.totalInputTokens).toBe(1000);
|
|
49
|
-
expect(s.totalOutputTokens).toBe(200);
|
|
50
|
-
expect(s.hitRate).toBeCloseTo(0.1, 5); // 100 / 1000
|
|
51
|
-
expect(s.history.length).toBe(1);
|
|
52
|
-
expect(s.history[0].read).toBe(100);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('records DeepSeek usage (prompt_cache_hit / miss)', () => {
|
|
56
|
-
const store = new CacheStatsStore(statsPath);
|
|
57
|
-
const usage: Usage = {
|
|
58
|
-
prompt_tokens: 5000,
|
|
59
|
-
completion_tokens: 800,
|
|
60
|
-
total_tokens: 5800,
|
|
61
|
-
prompt_cache_hit_tokens: 4000,
|
|
62
|
-
prompt_cache_miss_tokens: 1000,
|
|
63
|
-
};
|
|
64
|
-
store.record(usage);
|
|
65
|
-
|
|
66
|
-
const s = store.snapshot();
|
|
67
|
-
expect(s.totalReadTokens).toBe(4000); // hit → read
|
|
68
|
-
expect(s.totalCreationTokens).toBe(1000); // miss → creation
|
|
69
|
-
expect(s.hitRate).toBeCloseTo(0.8, 5); // 4000 / 5000
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('persists across instances', () => {
|
|
73
|
-
const a = new CacheStatsStore(statsPath);
|
|
74
|
-
a.record({
|
|
75
|
-
prompt_tokens: 100,
|
|
76
|
-
completion_tokens: 50,
|
|
77
|
-
total_tokens: 150,
|
|
78
|
-
cache_read_input_tokens: 80,
|
|
79
|
-
});
|
|
80
|
-
expect(existsSync(statsPath)).toBe(true);
|
|
81
|
-
|
|
82
|
-
const b = new CacheStatsStore(statsPath);
|
|
83
|
-
const s = b.snapshot();
|
|
84
|
-
expect(s.totalRequests).toBe(1);
|
|
85
|
-
expect(s.totalReadTokens).toBe(80);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('reset() clears all stats', () => {
|
|
89
|
-
const store = new CacheStatsStore(statsPath);
|
|
90
|
-
store.record({
|
|
91
|
-
prompt_tokens: 100,
|
|
92
|
-
completion_tokens: 50,
|
|
93
|
-
total_tokens: 150,
|
|
94
|
-
cache_read_input_tokens: 80,
|
|
95
|
-
});
|
|
96
|
-
expect(store.snapshot().totalRequests).toBe(1);
|
|
97
|
-
|
|
98
|
-
store.reset();
|
|
99
|
-
const s = store.snapshot();
|
|
100
|
-
expect(s.totalRequests).toBe(0);
|
|
101
|
-
expect(s.totalReadTokens).toBe(0);
|
|
102
|
-
expect(s.history).toEqual([]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('history rolls over after HISTORY_LIMIT rounds', () => {
|
|
106
|
-
const store = new CacheStatsStore(statsPath);
|
|
107
|
-
for (let i = 0; i < 60; i++) {
|
|
108
|
-
store.record({
|
|
109
|
-
prompt_tokens: 100,
|
|
110
|
-
completion_tokens: 50,
|
|
111
|
-
total_tokens: 150,
|
|
112
|
-
cache_read_input_tokens: 10,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
// Internal HISTORY_LIMIT is 50; the array should be capped.
|
|
116
|
-
expect(store.snapshot().history.length).toBeLessThanOrEqual(50);
|
|
117
|
-
expect(store.snapshot().totalRequests).toBe(60); // totals not capped
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('estimateSavingsCNY', () => {
|
|
122
|
-
it('returns 0 when nothing cached', () => {
|
|
123
|
-
expect(estimateSavingsCNY({
|
|
124
|
-
totalReadTokens: 0,
|
|
125
|
-
totalCreationTokens: 0,
|
|
126
|
-
totalInputTokens: 0,
|
|
127
|
-
totalOutputTokens: 0,
|
|
128
|
-
totalRequests: 0,
|
|
129
|
-
history: [],
|
|
130
|
-
})).toBe(0);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('returns positive for cached tokens', () => {
|
|
134
|
-
// 1M tokens cached at 10x saving ≈ ¥2.7
|
|
135
|
-
const v = estimateSavingsCNY({
|
|
136
|
-
totalReadTokens: 1_000_000,
|
|
137
|
-
totalCreationTokens: 0,
|
|
138
|
-
totalInputTokens: 1_000_000,
|
|
139
|
-
totalOutputTokens: 0,
|
|
140
|
-
totalRequests: 1,
|
|
141
|
-
history: [],
|
|
142
|
-
});
|
|
143
|
-
expect(v).toBeGreaterThan(2);
|
|
144
|
-
expect(v).toBeLessThan(3.5);
|
|
145
|
-
});
|
|
146
|
-
});
|