dify/web/features/skills/detail/__tests__/shared.spec.ts
wangxiaolei 0024a845f7
feat: support agent skill (#39675)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: 起岚 <155608604+xiaoyaoqilan@users.noreply.github.com>
Co-authored-by: Parman Mohammadalizadeh <prmma23@gmail.com>
Co-authored-by: Escape0707 <tothesong@gmail.com>
Co-authored-by: Xin Zhang <zhangxin@dify.ai>
Co-authored-by: iridescentWen <66297467+iridescentWen@users.noreply.github.com>
Co-authored-by: Bond Zhu <37842169+MRZHUH@users.noreply.github.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Chester <superdiao6@gmail.com>
Co-authored-by: WH-2099 <wh2099@pm.me>
Co-authored-by: Jony <619963502@qq.com>
Co-authored-by: Jony <13896935+zyz619963502zyz@users.noreply.github.com>
Co-authored-by: Harsh Kashyap <harsh.kashyap2001@gmail.com>
Co-authored-by: Harsh Kashyap <Harsh23Kashyap@users.noreply.github.com>
Co-authored-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: csurong <csurong1@gmail.com>
Co-authored-by: caosurong <surong.cao@thinkingdata.cn>
Co-authored-by: Taranum Wasu <81034301+Taranum01@users.noreply.github.com>
Co-authored-by: Taranum01 <50813317+Taranum01@users.noreply.github.com>
Co-authored-by: Pranav Agarwal <agarwalpranav0711@gmail.com>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: zl86790 <marshal_li_b@163.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: yunlu.wen <yunlu.wen@dify.ai>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
2026-08-24 05:29:14 +00:00

206 lines
6.8 KiB
TypeScript

import { QueryClient } from '@tanstack/react-query'
import { describe, expect, it, vi } from 'vitest'
import { consoleQuery } from '@/service/client'
import {
createUploadItemId,
deriveSkillDetailFromDraftFiles,
getErrorCode,
getErrorDetailNumber,
getErrorDetailString,
getUploadFileName,
getUploadPath,
isEditableKeyboardTarget,
joinSkillPath,
setSkillDetailCache,
} from '../shared'
describe('skill detail shared utilities', () => {
it('reads error codes and details from supported error shapes', () => {
expect(getErrorCode({ code: 'direct' })).toBe('direct')
expect(getErrorCode({ data: { code: 'data' } })).toBe('data')
expect(getErrorCode({ body: { code: 'body' } })).toBe('body')
expect(getErrorCode('error')).toBeUndefined()
expect(
getErrorDetailNumber({ details: { current_updated_at: 12 } }, 'current_updated_at'),
).toBe(12)
expect(
getErrorDetailNumber({ data: { details: { current_updated_at: 13 } } }, 'current_updated_at'),
).toBe(13)
expect(
getErrorDetailString(
{ body: { details: { current_file_hash: 'hash' } } },
'current_file_hash',
),
).toBe('hash')
expect(
getErrorDetailString({ details: { current_file_hash: 1 } }, 'current_file_hash'),
).toBeUndefined()
})
it('normalizes upload paths and names', () => {
const plainFile = new File(['guide'], 'guide.md', { type: 'text/markdown' })
const nestedFile = new File(['guide'], 'guide.md', { type: 'text/markdown' })
Object.defineProperty(nestedFile, 'webkitRelativePath', {
configurable: true,
value: 'folder/guide.md',
})
expect(joinSkillPath(undefined, '/guide.md')).toBe('guide.md')
expect(joinSkillPath('/references/', '/guide.md')).toBe('references/guide.md')
expect(getUploadPath(plainFile, 'references')).toBe('references/guide.md')
expect(getUploadPath(nestedFile, 'references')).toBe('references/folder/guide.md')
expect(getUploadFileName(nestedFile)).toBe('folder/guide.md')
})
it('detects editable keyboard targets', () => {
const input = document.createElement('input')
const textarea = document.createElement('textarea')
const select = document.createElement('select')
const editor = document.createElement('div')
editor.contentEditable = 'true'
const nested = document.createElement('span')
editor.appendChild(nested)
const plain = document.createElement('button')
expect(isEditableKeyboardTarget(input)).toBe(true)
expect(isEditableKeyboardTarget(textarea)).toBe(true)
expect(isEditableKeyboardTarget(select)).toBe(true)
expect(isEditableKeyboardTarget(editor)).toBe(true)
expect(isEditableKeyboardTarget(nested)).toBe(true)
expect(isEditableKeyboardTarget(plain)).toBe(false)
expect(isEditableKeyboardTarget(null)).toBe(false)
})
it('falls back to a deterministic upload item id when randomUUID is unavailable', () => {
const originalCrypto = globalThis.crypto
Object.defineProperty(globalThis, 'crypto', {
configurable: true,
value: {},
})
try {
const file = new File(['guide'], 'guide.md', { type: 'text/markdown' })
vi.spyOn(file, 'lastModified', 'get').mockReturnValue(123)
expect(createUploadItemId(file, 2)).toBe('guide.md-5-123-2')
} finally {
Object.defineProperty(globalThis, 'crypto', {
configurable: true,
value: originalCrypto,
})
}
})
it('does not derive entity name or display name from SKILL.md draft content', () => {
expect(
deriveSkillDetailFromDraftFiles({
description: 'Entity description',
display_name: 'Entity Display Name',
files: [
{
content:
'---\nname: skill-md-name\ndescription: Skill.md description\nmetadata:\n display-name: Skill.md Display Name\n---\n# Body\n',
kind: 'file',
mime_type: 'text/markdown',
path: 'SKILL.md',
storage: 'text',
},
],
latest_published_version_id: null,
name: 'entity-name',
name_manually_edited: false,
} as Parameters<typeof deriveSkillDetailFromDraftFiles>[0]),
).toMatchObject({
description: 'Skill.md description',
display_name: 'Entity Display Name',
name: 'entity-name',
})
})
it('derives untitled builder draft name and display name from SKILL.md content', () => {
expect(
deriveSkillDetailFromDraftFiles({
description: 'Entity description',
display_name: 'Untitled skill',
files: [
{
content:
'---\nname: untitled-skill-12345678\ndescription: Skill.md description\nmetadata:\n display-name: Untitled skill\n---\n# Customer Issue Triage\n',
kind: 'file',
mime_type: 'text/markdown',
path: 'SKILL.md',
storage: 'text',
},
],
latest_published_version_id: null,
name: 'untitled-skill-12345678',
name_manually_edited: false,
} as Parameters<typeof deriveSkillDetailFromDraftFiles>[0]),
).toMatchObject({
description: 'Skill.md description',
display_name: 'Customer Issue Triage',
name: 'customer-issue-triage',
})
})
it('updates cached skill list entries when detail cache changes', () => {
const queryClient = new QueryClient()
const detail = {
id: 'skill-1',
name: 'customer-issue-triage',
display_name: 'Customer Issue Triage',
icon: '📄',
description: 'Classify customer issues.',
tags: [],
name_manually_edited: false,
visibility: 'workspace',
latest_published_version_id: null,
latest_published_version_number: null,
latest_published_at: null,
reference_count: 0,
created_by: 'user-1',
created_by_name: 'Fate',
updated_by: 'user-1',
updated_by_name: 'Fate',
created_at: 1,
updated_at: 2,
files: [],
} as Parameters<typeof setSkillDetailCache>[2]
const infiniteKey = consoleQuery.workspaces.current.skills.get.key({ type: 'infinite' })
queryClient.setQueryData(infiniteKey, {
pageParams: [1],
pages: [
{
data: [
{
...detail,
name: 'untitled-skill-12345678',
display_name: 'Untitled skill',
latest_published_version_number: null,
},
],
has_more: false,
limit: 20,
page: 1,
total: 1,
},
],
})
setSkillDetailCache(queryClient, detail.id, detail)
expect(queryClient.getQueryData(infiniteKey)).toMatchObject({
pages: [
{
data: [
{
display_name: 'Customer Issue Triage',
name: 'customer-issue-triage',
},
],
},
],
})
})
})