mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
135
libs/model-resolver/README.md
Normal file
135
libs/model-resolver/README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# @automaker/model-resolver
|
||||
|
||||
Claude model resolution and mapping utilities.
|
||||
|
||||
## Overview
|
||||
|
||||
This package handles Claude model resolution, converting user-friendly aliases to actual Claude model identifiers and providing default model configurations.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @automaker/model-resolver
|
||||
```
|
||||
|
||||
## Exports
|
||||
|
||||
### Model Resolution
|
||||
|
||||
Convert model aliases to full model identifiers.
|
||||
|
||||
```typescript
|
||||
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
|
||||
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||
|
||||
// Resolve model string
|
||||
const model = resolveModelString('sonnet');
|
||||
// Returns: 'claude-sonnet-4-20250514'
|
||||
|
||||
const model2 = resolveModelString('haiku');
|
||||
// Returns: 'claude-haiku-4-5'
|
||||
|
||||
const model3 = resolveModelString('opus');
|
||||
// Returns: 'claude-opus-4-5-20251101'
|
||||
|
||||
// Use with custom default
|
||||
const model4 = resolveModelString(undefined, 'claude-sonnet-4-20250514');
|
||||
// Returns: 'claude-sonnet-4-20250514' (default)
|
||||
|
||||
// Direct model ID passthrough
|
||||
const model5 = resolveModelString('claude-opus-4-5-20251101');
|
||||
// Returns: 'claude-opus-4-5-20251101' (unchanged)
|
||||
```
|
||||
|
||||
### Get Effective Model
|
||||
|
||||
Get the actual model that will be used.
|
||||
|
||||
```typescript
|
||||
import { getEffectiveModel } from '@automaker/model-resolver';
|
||||
|
||||
// Get effective model with fallback chain
|
||||
const model = getEffectiveModel({
|
||||
requestedModel: 'sonnet',
|
||||
featureModel: undefined,
|
||||
defaultModel: 'claude-sonnet-4-20250514',
|
||||
});
|
||||
```
|
||||
|
||||
### Model Constants
|
||||
|
||||
Access model mappings and defaults.
|
||||
|
||||
```typescript
|
||||
import { DEFAULT_MODELS } from '@automaker/model-resolver';
|
||||
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||
|
||||
// Default models for different contexts
|
||||
console.log(DEFAULT_MODELS.claude); // 'claude-sonnet-4-20250514'
|
||||
console.log(DEFAULT_MODELS.autoMode); // 'claude-sonnet-4-20250514'
|
||||
console.log(DEFAULT_MODELS.chat); // 'claude-sonnet-4-20250514'
|
||||
|
||||
// Model alias mappings
|
||||
console.log(CLAUDE_MODEL_MAP.haiku); // 'claude-haiku-4-5'
|
||||
console.log(CLAUDE_MODEL_MAP.sonnet); // 'claude-sonnet-4-20250514'
|
||||
console.log(CLAUDE_MODEL_MAP.opus); // 'claude-opus-4-5-20251101'
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';
|
||||
import type { Feature } from '@automaker/types';
|
||||
|
||||
function prepareFeatureExecution(feature: Feature) {
|
||||
// Resolve model from feature or use default
|
||||
const model = resolveModelString(feature.model, DEFAULT_MODELS.autoMode);
|
||||
|
||||
console.log(`Executing feature with model: ${model}`);
|
||||
|
||||
return {
|
||||
featureId: feature.id,
|
||||
model,
|
||||
// ... other options
|
||||
};
|
||||
}
|
||||
|
||||
// Example usage
|
||||
const feature: Feature = {
|
||||
id: 'auth-feature',
|
||||
category: 'backend',
|
||||
description: 'Add authentication',
|
||||
model: 'opus', // User-friendly alias
|
||||
};
|
||||
|
||||
prepareFeatureExecution(feature);
|
||||
// Output: Executing feature with model: claude-opus-4-5-20251101
|
||||
```
|
||||
|
||||
## Supported Models
|
||||
|
||||
### Current Model Aliases
|
||||
|
||||
- `haiku` → `claude-haiku-4-5`
|
||||
- `sonnet` → `claude-sonnet-4-20250514`
|
||||
- `opus` → `claude-opus-4-5-20251101`
|
||||
|
||||
### Model Selection Guide
|
||||
|
||||
- **Haiku**: Fast responses, simple tasks, lower cost
|
||||
- **Sonnet**: Balanced performance, most tasks (recommended default)
|
||||
- **Opus**: Maximum capability, complex reasoning, highest cost
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@automaker/types` - Model type definitions and constants
|
||||
|
||||
## Used By
|
||||
|
||||
- `@automaker/server` - Feature execution, agent chat, enhancement
|
||||
|
||||
## Notes
|
||||
|
||||
- Model strings that don't match aliases are passed through unchanged
|
||||
- This allows direct use of specific model versions like `claude-sonnet-4-20250514`
|
||||
- Always falls back to a sensible default if no model is specified
|
||||
29
libs/model-resolver/package.json
Normal file
29
libs/model-resolver/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@automaker/model-resolver",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Model resolution utilities for AutoMaker",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"keywords": [
|
||||
"automaker",
|
||||
"model",
|
||||
"resolver"
|
||||
],
|
||||
"author": "AutoMaker Team",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@automaker/types": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
10
libs/model-resolver/src/index.ts
Normal file
10
libs/model-resolver/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @automaker/model-resolver
|
||||
* Model resolution utilities for AutoMaker
|
||||
*/
|
||||
|
||||
// Re-export constants from types
|
||||
export { CLAUDE_MODEL_MAP, DEFAULT_MODELS, type ModelAlias } from '@automaker/types';
|
||||
|
||||
// Export resolver functions
|
||||
export { resolveModelString, getEffectiveModel } from './resolver.js';
|
||||
61
libs/model-resolver/src/resolver.ts
Normal file
61
libs/model-resolver/src/resolver.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Model resolution utilities for handling model string mapping
|
||||
*
|
||||
* Provides centralized model resolution logic:
|
||||
* - Maps Claude model aliases to full model strings
|
||||
* - Provides default models per provider
|
||||
* - Handles multiple model sources with priority
|
||||
*/
|
||||
|
||||
import { CLAUDE_MODEL_MAP, DEFAULT_MODELS } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Resolve a model key/alias to a full model string
|
||||
*
|
||||
* @param modelKey - Model key (e.g., "opus", "gpt-5.2", "claude-sonnet-4-20250514")
|
||||
* @param defaultModel - Fallback model if modelKey is undefined
|
||||
* @returns Full model string
|
||||
*/
|
||||
export function resolveModelString(
|
||||
modelKey?: string,
|
||||
defaultModel: string = DEFAULT_MODELS.claude
|
||||
): string {
|
||||
// No model specified - use default
|
||||
if (!modelKey) {
|
||||
return defaultModel;
|
||||
}
|
||||
|
||||
// Full Claude model string - pass through unchanged
|
||||
if (modelKey.includes('claude-')) {
|
||||
console.log(`[ModelResolver] Using full Claude model string: ${modelKey}`);
|
||||
return modelKey;
|
||||
}
|
||||
|
||||
// Look up Claude model alias
|
||||
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
||||
if (resolved) {
|
||||
console.log(`[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"`);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Unknown model key - use default
|
||||
console.warn(`[ModelResolver] Unknown model key "${modelKey}", using default: "${defaultModel}"`);
|
||||
return defaultModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective model from multiple sources
|
||||
* Priority: explicit model > session model > default
|
||||
*
|
||||
* @param explicitModel - Explicitly provided model (highest priority)
|
||||
* @param sessionModel - Model from session (medium priority)
|
||||
* @param defaultModel - Fallback default model (lowest priority)
|
||||
* @returns Resolved model string
|
||||
*/
|
||||
export function getEffectiveModel(
|
||||
explicitModel?: string,
|
||||
sessionModel?: string,
|
||||
defaultModel?: string
|
||||
): string {
|
||||
return resolveModelString(explicitModel || sessionModel, defaultModel);
|
||||
}
|
||||
300
libs/model-resolver/tests/resolver.test.ts
Normal file
300
libs/model-resolver/tests/resolver.test.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { resolveModelString, getEffectiveModel } from '../src/resolver';
|
||||
import { CLAUDE_MODEL_MAP, DEFAULT_MODELS } from '@automaker/types';
|
||||
|
||||
describe('model-resolver', () => {
|
||||
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('resolveModelString', () => {
|
||||
describe('with undefined/null input', () => {
|
||||
it('should return default model when modelKey is undefined', () => {
|
||||
const result = resolveModelString(undefined);
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
|
||||
it('should return custom default when modelKey is undefined', () => {
|
||||
const customDefault = 'claude-opus-4-20241113';
|
||||
const result = resolveModelString(undefined, customDefault);
|
||||
expect(result).toBe(customDefault);
|
||||
});
|
||||
|
||||
it('should return default when modelKey is empty string', () => {
|
||||
const result = resolveModelString('');
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with full Claude model strings', () => {
|
||||
it('should pass through full Claude model string unchanged', () => {
|
||||
const fullModel = 'claude-sonnet-4-20250514';
|
||||
const result = resolveModelString(fullModel);
|
||||
|
||||
expect(result).toBe(fullModel);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Using full Claude model string')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle claude-opus model strings', () => {
|
||||
const fullModel = 'claude-opus-4-20241113';
|
||||
const result = resolveModelString(fullModel);
|
||||
|
||||
expect(result).toBe(fullModel);
|
||||
});
|
||||
|
||||
it('should handle claude-haiku model strings', () => {
|
||||
const fullModel = 'claude-3-5-haiku-20241022';
|
||||
const result = resolveModelString(fullModel);
|
||||
|
||||
expect(result).toBe(fullModel);
|
||||
});
|
||||
|
||||
it("should handle any string containing 'claude-'", () => {
|
||||
const customModel = 'claude-custom-experimental-v1';
|
||||
const result = resolveModelString(customModel);
|
||||
|
||||
expect(result).toBe(customModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with model aliases', () => {
|
||||
it("should resolve 'sonnet' alias", () => {
|
||||
const result = resolveModelString('sonnet');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Resolved model alias: "sonnet"')
|
||||
);
|
||||
});
|
||||
|
||||
it("should resolve 'opus' alias", () => {
|
||||
const result = resolveModelString('opus');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Resolved model alias: "opus"')
|
||||
);
|
||||
});
|
||||
|
||||
it("should resolve 'haiku' alias", () => {
|
||||
const result = resolveModelString('haiku');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.haiku);
|
||||
});
|
||||
|
||||
it('should log the resolution for aliases', () => {
|
||||
resolveModelString('sonnet');
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Resolved model alias'));
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(CLAUDE_MODEL_MAP.sonnet)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with unknown model keys', () => {
|
||||
it('should return default for unknown model key', () => {
|
||||
const result = resolveModelString('unknown-model');
|
||||
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
|
||||
it('should warn about unknown model key', () => {
|
||||
resolveModelString('unknown-model');
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown model key'));
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('unknown-model'));
|
||||
});
|
||||
|
||||
it('should use custom default for unknown model key', () => {
|
||||
const customDefault = 'claude-opus-4-20241113';
|
||||
const result = resolveModelString('gpt-4', customDefault);
|
||||
|
||||
expect(result).toBe(customDefault);
|
||||
});
|
||||
|
||||
it('should warn and show default being used', () => {
|
||||
const customDefault = 'claude-custom-default';
|
||||
resolveModelString('invalid-key', customDefault);
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining(customDefault));
|
||||
});
|
||||
});
|
||||
|
||||
describe('case sensitivity', () => {
|
||||
it('should be case-sensitive for aliases', () => {
|
||||
const resultUpper = resolveModelString('SONNET');
|
||||
const resultLower = resolveModelString('sonnet');
|
||||
|
||||
// Uppercase should not resolve (falls back to default)
|
||||
expect(resultUpper).toBe(DEFAULT_MODELS.claude);
|
||||
// Lowercase should resolve
|
||||
expect(resultLower).toBe(CLAUDE_MODEL_MAP.sonnet);
|
||||
});
|
||||
|
||||
it('should handle mixed case in claude- strings', () => {
|
||||
const result = resolveModelString('Claude-Sonnet-4-20250514');
|
||||
|
||||
// Capital 'C' means it won't match 'claude-', falls back to default
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle model key with whitespace', () => {
|
||||
const result = resolveModelString(' sonnet ');
|
||||
|
||||
// Will not match due to whitespace, falls back to default
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
|
||||
it('should handle special characters in model key', () => {
|
||||
const result = resolveModelString('model@123');
|
||||
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEffectiveModel', () => {
|
||||
describe('priority handling', () => {
|
||||
it('should prioritize explicit model over all others', () => {
|
||||
const explicit = 'claude-opus-4-20241113';
|
||||
const session = 'claude-sonnet-4-20250514';
|
||||
const defaultModel = 'claude-3-5-haiku-20241022';
|
||||
|
||||
const result = getEffectiveModel(explicit, session, defaultModel);
|
||||
|
||||
expect(result).toBe(explicit);
|
||||
});
|
||||
|
||||
it('should use session model when explicit is undefined', () => {
|
||||
const session = 'claude-sonnet-4-20250514';
|
||||
const defaultModel = 'claude-3-5-haiku-20241022';
|
||||
|
||||
const result = getEffectiveModel(undefined, session, defaultModel);
|
||||
|
||||
expect(result).toBe(session);
|
||||
});
|
||||
|
||||
it('should use default model when both explicit and session are undefined', () => {
|
||||
const defaultModel = 'claude-opus-4-20241113';
|
||||
|
||||
const result = getEffectiveModel(undefined, undefined, defaultModel);
|
||||
|
||||
expect(result).toBe(defaultModel);
|
||||
});
|
||||
|
||||
it('should use system default when all are undefined', () => {
|
||||
const result = getEffectiveModel(undefined, undefined, undefined);
|
||||
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with aliases', () => {
|
||||
it('should resolve explicit model alias', () => {
|
||||
const result = getEffectiveModel('opus', 'sonnet');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
});
|
||||
|
||||
it('should resolve session model alias when explicit is undefined', () => {
|
||||
const result = getEffectiveModel(undefined, 'haiku');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.haiku);
|
||||
});
|
||||
|
||||
it('should prioritize explicit alias over session full string', () => {
|
||||
const result = getEffectiveModel('sonnet', 'claude-opus-4-20241113');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with empty strings', () => {
|
||||
it('should treat empty explicit string as undefined', () => {
|
||||
const session = 'claude-sonnet-4-20250514';
|
||||
|
||||
const result = getEffectiveModel('', session);
|
||||
|
||||
expect(result).toBe(session);
|
||||
});
|
||||
|
||||
it('should treat empty session string as undefined', () => {
|
||||
const defaultModel = 'claude-opus-4-20241113';
|
||||
|
||||
const result = getEffectiveModel(undefined, '', defaultModel);
|
||||
|
||||
expect(result).toBe(defaultModel);
|
||||
});
|
||||
|
||||
it('should handle all empty strings', () => {
|
||||
const result = getEffectiveModel('', '', '');
|
||||
|
||||
// Empty strings are falsy, so explicit || session becomes "" || "" = ""
|
||||
// Then resolveModelString("", "") returns "" (not in CLAUDE_MODEL_MAP, not containing "claude-")
|
||||
// This actually returns the custom default which is ""
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should handle user overriding session model with alias', () => {
|
||||
const sessionModel = 'claude-sonnet-4-20250514';
|
||||
const userChoice = 'opus';
|
||||
|
||||
const result = getEffectiveModel(userChoice, sessionModel);
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.opus);
|
||||
});
|
||||
|
||||
it('should handle fallback chain: unknown -> session -> default', () => {
|
||||
const result = getEffectiveModel('invalid', 'also-invalid', 'claude-opus-4-20241113');
|
||||
|
||||
// Both invalid models fall back to default
|
||||
expect(result).toBe('claude-opus-4-20241113');
|
||||
});
|
||||
|
||||
it('should handle session with alias, no explicit', () => {
|
||||
const result = getEffectiveModel(undefined, 'haiku');
|
||||
|
||||
expect(result).toBe(CLAUDE_MODEL_MAP.haiku);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLAUDE_MODEL_MAP integration', () => {
|
||||
it('should have valid mappings for all known aliases', () => {
|
||||
const aliases = ['sonnet', 'opus', 'haiku'];
|
||||
|
||||
for (const alias of aliases) {
|
||||
const resolved = resolveModelString(alias);
|
||||
expect(resolved).toBeDefined();
|
||||
expect(resolved).toContain('claude-');
|
||||
expect(resolved).toBe(CLAUDE_MODEL_MAP[alias]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_MODELS integration', () => {
|
||||
it('should use DEFAULT_MODELS.claude as fallback', () => {
|
||||
const result = resolveModelString(undefined);
|
||||
|
||||
expect(result).toBe(DEFAULT_MODELS.claude);
|
||||
expect(DEFAULT_MODELS.claude).toBeDefined();
|
||||
expect(DEFAULT_MODELS.claude).toContain('claude-');
|
||||
});
|
||||
});
|
||||
});
|
||||
9
libs/model-resolver/tsconfig.json
Normal file
9
libs/model-resolver/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
21
libs/model-resolver/vitest.config.ts
Normal file
21
libs/model-resolver/vitest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: ['src/**/*.d.ts', 'src/index.ts'],
|
||||
thresholds: {
|
||||
lines: 95,
|
||||
functions: 95,
|
||||
branches: 90,
|
||||
statements: 95,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user