Add unit tests for skill capabilities, skill planner, and UV setup

- Implement tests for random ID generation, ensuring preference for crypto.randomUUID.
- Create tests for runtime context capabilities, validating the injection of enabled skill capabilities.
- Add tests for skill capability parsing, including classification and command example extraction.
- Introduce tests for the skill planner, verifying tool call planning based on user requests and attachment requirements.
- Establish tests for UV setup, ensuring proper handling of Python installation scenarios and environment checks.
This commit is contained in:
DEV_DSW
2026-04-24 17:02:59 +08:00
parent e11a2296cc
commit 4c61e93c3e
42 changed files with 12560 additions and 224 deletions

View File

@@ -0,0 +1,550 @@
export type ToolRegistryEntryKind = 'builtin-tool' | 'skill';
export type ToolRiskLevel = 'low' | 'medium' | 'high';
export type ToolInputKind =
| 'text'
| 'url'
| 'file'
| 'attachment'
| 'history'
| 'structured';
export type ToolOutputKind =
| 'text'
| 'json'
| 'file'
| 'artifacts'
| 'browser-state'
| 'status';
export interface ToolRegistryCapabilityInput {
capabilityKey?: string;
familyKey?: string;
toolName?: string;
skillKey?: string;
slug?: string;
name?: string;
displayName?: string;
description?: string;
kind?: ToolRegistryEntryKind;
aliases?: string[];
inputKinds?: string[];
outputKinds?: string[];
triggerHints?: string[];
supportedFileTypes?: string[];
requiresFiles?: boolean;
requiresExplicitUserIntent?: boolean;
enabled?: boolean;
disabled?: boolean;
riskLevel?: ToolRiskLevel;
source?: string;
metadata?: Record<string, unknown>;
}
export interface ToolRegistryEntry {
capabilityKey: string;
familyKey: string;
toolName: string;
kind: ToolRegistryEntryKind;
displayName: string;
description: string;
aliases: string[];
inputKinds: ToolInputKind[];
outputKinds: ToolOutputKind[];
triggerHints: string[];
supportedFileTypes: string[];
requiresFiles: boolean;
requiresExplicitUserIntent: boolean;
riskLevel: ToolRiskLevel;
enabled: boolean;
source?: string;
metadata: Record<string, unknown>;
}
export interface CreateToolRegistryOptions {
capabilities?: ToolRegistryCapabilityInput[];
includeBuiltins?: boolean;
}
const SPREADSHEET_FAMILY_KEY = 'spreadsheet.analysis';
const SPREADSHEET_EXTENSIONS = ['.xlsx', '.xls', '.csv', '.tsv', '.ods'];
const SPREADSHEET_MIME_PATTERNS = [
'spreadsheet',
'excel',
'sheet',
'csv',
'tab-separated-values',
'officedocument.spreadsheetml',
];
const DEFAULT_BUILTIN_ENTRIES: ToolRegistryEntry[] = [
{
capabilityKey: 'browser.open_url',
familyKey: 'browser.open_url',
toolName: 'browser.open_url',
kind: 'builtin-tool',
displayName: 'Browser Open URL',
description: 'Open an explicit URL in the managed browser when the user clearly asks to open or visit a page.',
aliases: ['browser.open_url', 'open url', 'open link', 'visit url'],
inputKinds: ['url'],
outputKinds: ['browser-state', 'text'],
triggerHints: ['open this url', 'open the link', 'visit the page', '打开链接', '打开网页', '访问网页'],
supportedFileTypes: [],
requiresFiles: false,
requiresExplicitUserIntent: true,
riskLevel: 'medium',
enabled: true,
metadata: {
intent: 'open_url',
sideEffect: 'browser-navigation',
},
},
{
capabilityKey: 'skills.install',
familyKey: 'skills.install',
toolName: 'skills.install',
kind: 'builtin-tool',
displayName: 'Skills Install',
description: 'Install a skill from the marketplace slug or a GitHub skill URL when the user explicitly asks to install one.',
aliases: ['skills.install', 'install skill', 'skill install', 'add skill'],
inputKinds: ['text', 'url'],
outputKinds: ['status', 'text'],
triggerHints: ['install this skill', 'install skill', 'github skill url', '安装 skill', '安装技能'],
supportedFileTypes: [],
requiresFiles: false,
requiresExplicitUserIntent: true,
riskLevel: 'medium',
enabled: true,
metadata: {
intent: 'install_skill',
sideEffect: 'skill-installation',
},
},
];
function normalizeTextValue(value: string | undefined | null): string {
return String(value ?? '').trim();
}
function normalizeLookupValue(value: string | undefined | null): string {
return normalizeTextValue(value).toLowerCase();
}
function dedupeStrings(values: Array<string | undefined | null>): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const trimmed = normalizeTextValue(value);
if (!trimmed) {
continue;
}
const lookup = trimmed.toLowerCase();
if (seen.has(lookup)) {
continue;
}
seen.add(lookup);
result.push(trimmed);
}
return result;
}
function normalizeStringList(values: unknown, fallback: string[] = []): string[] {
if (!Array.isArray(values)) {
return [...fallback];
}
return dedupeStrings(values.map((item) => (typeof item === 'string' ? item : '')));
}
function normalizeInputKinds(values: unknown): ToolInputKind[] {
const allowed: ToolInputKind[] = ['text', 'url', 'file', 'attachment', 'history', 'structured'];
return normalizeStringList(values)
.map((value) => value.toLowerCase())
.filter((value): value is ToolInputKind => allowed.includes(value as ToolInputKind));
}
function normalizeOutputKinds(values: unknown): ToolOutputKind[] {
const allowed: ToolOutputKind[] = ['text', 'json', 'file', 'artifacts', 'browser-state', 'status'];
return normalizeStringList(values)
.map((value) => value.toLowerCase())
.filter((value): value is ToolOutputKind => allowed.includes(value as ToolOutputKind));
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function getMetadataStringArray(
metadata: Record<string, unknown>,
...keys: string[]
): string[] {
for (const key of keys) {
const value = metadata[key];
if (Array.isArray(value)) {
return normalizeStringList(value);
}
}
return [];
}
function getMetadataBoolean(
metadata: Record<string, unknown>,
...keys: string[]
): boolean | undefined {
for (const key of keys) {
const value = metadata[key];
if (typeof value === 'boolean') {
return value;
}
}
return undefined;
}
function inferCapabilityKind(input: ToolRegistryCapabilityInput, capabilityKey: string): ToolRegistryEntryKind {
if (input.kind === 'builtin-tool' || input.kind === 'skill') {
return input.kind;
}
if (capabilityKey === 'browser.open_url' || capabilityKey === 'skills.install') {
return 'builtin-tool';
}
return 'skill';
}
function normalizeRiskLevel(value: string | undefined): ToolRiskLevel {
switch (normalizeLookupValue(value)) {
case 'high':
return 'high';
case 'medium':
return 'medium';
default:
return 'low';
}
}
function isSpreadsheetCapability(
capabilityKey: string,
displayName: string,
description: string,
aliases: string[],
supportedFileTypes: string[],
): boolean {
const haystack = [
capabilityKey,
displayName,
description,
...aliases,
...supportedFileTypes,
].map((item) => item.toLowerCase());
if (haystack.some((item) => item === 'minimax-xlsx' || item === 'xlsx')) {
return true;
}
if (supportedFileTypes.some((item) => isSpreadsheetFileType(item))) {
return true;
}
return haystack.some((item) =>
item.includes('spreadsheet')
|| item.includes('excel')
|| item.includes('sheet')
|| item.includes('csv')
|| item.includes('表格')
|| item.includes('excel')
|| item.includes('工作表'),
);
}
function normalizeSupportedFileTypes(
input: ToolRegistryCapabilityInput,
metadata: Record<string, unknown>,
): string[] {
const explicit = dedupeStrings([
...normalizeStringList(input.supportedFileTypes),
...getMetadataStringArray(metadata, 'supportedFileTypes', 'supported_file_types'),
]);
if (explicit.length > 0) {
return explicit;
}
const capabilityKey = normalizeLookupValue(
input.capabilityKey || input.toolName || input.skillKey || input.slug || input.name,
);
if (capabilityKey === 'xlsx' || capabilityKey === 'minimax-xlsx') {
return [...SPREADSHEET_EXTENSIONS];
}
return [];
}
function normalizeAliases(
input: ToolRegistryCapabilityInput,
capabilityKey: string,
displayName: string,
toolName: string,
isSpreadsheet: boolean,
): string[] {
return dedupeStrings([
capabilityKey,
toolName,
displayName,
input.skillKey,
input.slug,
input.name,
...normalizeStringList(input.aliases),
...(isSpreadsheet ? ['minimax-xlsx', 'xlsx', 'spreadsheet', 'excel analyzer', 'excel analysis'] : []),
]);
}
function normalizeTriggerHints(
input: ToolRegistryCapabilityInput,
metadata: Record<string, unknown>,
isSpreadsheet: boolean,
): string[] {
const hints = dedupeStrings([
...normalizeStringList(input.triggerHints),
...getMetadataStringArray(metadata, 'triggerHints', 'trigger_hints'),
]);
if (!isSpreadsheet) {
return hints;
}
return dedupeStrings([
...hints,
'analyze spreadsheet',
'analyze excel',
'analyze table file',
'summarize worksheet',
'分析表格',
'分析 excel',
'分析工作表',
'分析这个表',
]);
}
function mergeEntries(base: ToolRegistryEntry, incoming: ToolRegistryEntry): ToolRegistryEntry {
return {
...base,
...incoming,
aliases: dedupeStrings([...base.aliases, ...incoming.aliases]),
inputKinds: Array.from(new Set([...base.inputKinds, ...incoming.inputKinds])),
outputKinds: Array.from(new Set([...base.outputKinds, ...incoming.outputKinds])),
triggerHints: dedupeStrings([...base.triggerHints, ...incoming.triggerHints]),
supportedFileTypes: dedupeStrings([...base.supportedFileTypes, ...incoming.supportedFileTypes]),
metadata: {
...base.metadata,
...incoming.metadata,
},
enabled: base.enabled || incoming.enabled,
requiresFiles: base.requiresFiles || incoming.requiresFiles,
requiresExplicitUserIntent: base.requiresExplicitUserIntent || incoming.requiresExplicitUserIntent,
};
}
function buildRegistryEntry(input: ToolRegistryCapabilityInput): ToolRegistryEntry | null {
const metadata = isPlainRecord(input.metadata) ? input.metadata : {};
const capabilityKey = normalizeTextValue(
input.capabilityKey || input.toolName || input.skillKey || input.slug || input.name,
);
if (!capabilityKey) {
return null;
}
const toolName = normalizeTextValue(input.toolName || capabilityKey);
const displayName = normalizeTextValue(input.displayName || input.name || input.skillKey || input.slug || toolName);
const description = normalizeTextValue(input.description);
const supportedFileTypes = normalizeSupportedFileTypes(input, metadata);
const isSpreadsheet = isSpreadsheetCapability(
capabilityKey,
displayName,
description,
normalizeStringList(input.aliases),
supportedFileTypes,
);
const aliases = normalizeAliases(input, capabilityKey, displayName, toolName, isSpreadsheet);
const triggerHints = normalizeTriggerHints(input, metadata, isSpreadsheet);
const inputKinds = normalizeInputKinds(input.inputKinds);
const outputKinds = normalizeOutputKinds(input.outputKinds);
const inferredRequiresFiles = isSpreadsheet
|| supportedFileTypes.length > 0
|| inputKinds.includes('file')
|| inputKinds.includes('attachment');
const requiresFiles = input.requiresFiles
?? getMetadataBoolean(metadata, 'requiresFiles', 'requires_files')
?? inferredRequiresFiles;
const requiresExplicitUserIntent = input.requiresExplicitUserIntent
?? getMetadataBoolean(metadata, 'requiresExplicitUserIntent', 'requires_explicit_user_intent')
?? false;
const familyKey = normalizeTextValue(input.familyKey)
|| (isSpreadsheet ? SPREADSHEET_FAMILY_KEY : capabilityKey);
return {
capabilityKey,
familyKey,
toolName,
kind: inferCapabilityKind(input, capabilityKey),
displayName,
description,
aliases,
inputKinds: inputKinds.length > 0 ? inputKinds : (requiresFiles ? ['text', 'file', 'attachment'] : ['text']),
outputKinds: outputKinds.length > 0 ? outputKinds : ['text'],
triggerHints,
supportedFileTypes,
requiresFiles,
requiresExplicitUserIntent,
riskLevel: normalizeRiskLevel(input.riskLevel),
enabled: input.disabled === true ? false : input.enabled !== false,
source: normalizeTextValue(input.source) || undefined,
metadata: {
...metadata,
familyKey,
},
};
}
export function createToolRegistry(options: CreateToolRegistryOptions = {}): ToolRegistryEntry[] {
const includeBuiltins = options.includeBuiltins !== false;
const merged = new Map<string, ToolRegistryEntry>();
if (includeBuiltins) {
for (const entry of DEFAULT_BUILTIN_ENTRIES) {
merged.set(entry.capabilityKey, { ...entry, aliases: [...entry.aliases], triggerHints: [...entry.triggerHints] });
}
}
for (const capability of options.capabilities || []) {
if (capability.disabled === true || capability.enabled === false) {
continue;
}
const entry = buildRegistryEntry(capability);
if (!entry || entry.enabled === false) {
continue;
}
const existing = merged.get(entry.capabilityKey);
merged.set(entry.capabilityKey, existing ? mergeEntries(existing, entry) : entry);
}
return Array.from(merged.values()).sort((left, right) => {
if (left.kind !== right.kind) {
return left.kind === 'builtin-tool' ? -1 : 1;
}
return left.displayName.localeCompare(right.displayName);
});
}
export function getRegistryEntryByName(
registry: ToolRegistryEntry[],
reference: string,
): ToolRegistryEntry | undefined {
const lookup = normalizeLookupValue(reference);
if (!lookup) {
return undefined;
}
return registry.find((entry) =>
normalizeLookupValue(entry.capabilityKey) === lookup
|| normalizeLookupValue(entry.toolName) === lookup
|| normalizeLookupValue(entry.familyKey) === lookup
|| entry.aliases.some((alias) => normalizeLookupValue(alias) === lookup),
);
}
export function getRegistryEntriesByFamily(
registry: ToolRegistryEntry[],
familyKey: string,
): ToolRegistryEntry[] {
const lookup = normalizeLookupValue(familyKey);
if (!lookup) {
return [];
}
return registry.filter((entry) => normalizeLookupValue(entry.familyKey) === lookup);
}
export function matchRegistryEntriesByAlias(
registry: ToolRegistryEntry[],
text: string,
): Array<{ entry: ToolRegistryEntry; alias: string }> {
const normalizedText = normalizeLookupValue(text);
if (!normalizedText) {
return [];
}
const results: Array<{ entry: ToolRegistryEntry; alias: string }> = [];
for (const entry of registry) {
for (const alias of entry.aliases) {
const normalizedAlias = normalizeLookupValue(alias);
if (!normalizedAlias) {
continue;
}
if (normalizedText.includes(normalizedAlias)) {
results.push({ entry, alias });
}
}
}
return results;
}
export function isSpreadsheetFileType(value: string): boolean {
const normalized = normalizeLookupValue(value);
if (!normalized) {
return false;
}
if (SPREADSHEET_EXTENSIONS.some((ext) => normalized.endsWith(ext))) {
return true;
}
return SPREADSHEET_MIME_PATTERNS.some((pattern) => normalized.includes(pattern));
}
export function registryEntrySupportsFileType(
entry: ToolRegistryEntry,
fileType: string,
): boolean {
if (!fileType) {
return false;
}
if (entry.supportedFileTypes.length === 0) {
return !entry.requiresFiles;
}
const normalizedFileType = normalizeLookupValue(fileType);
return entry.supportedFileTypes.some((candidate) => {
const normalizedCandidate = normalizeLookupValue(candidate);
if (!normalizedCandidate) {
return false;
}
if (normalizedCandidate.startsWith('.')) {
return normalizedFileType.endsWith(normalizedCandidate);
}
return normalizedFileType.includes(normalizedCandidate);
});
}
export function isSpreadsheetFamilyEntry(entry: ToolRegistryEntry): boolean {
return normalizeLookupValue(entry.familyKey) === SPREADSHEET_FAMILY_KEY;
}
export function getSpreadsheetFamilyKey(): string {
return SPREADSHEET_FAMILY_KEY;
}