test(backend): add comprehensive test coverage for backend modules
Added a new `.coveragerc` file configuring coverage thresholds and exclusions. Included `pytest.ini` to enable coverage reporting for multiple backend modules (`main`, `llm`, `prompt`, `geoip`, `tts_asr`) with a 90 % fail‑under requirement and detailed HTML output. Implemented a suite of unit tests: * `test_geoip.py` – validates geo‑location lookup logic. * `test_llm_extended.py` – tests LLm response extraction and Ollama interactions. * `test_main_endpoints.py` – covers API endpoints for completions, OCR, and TTS. * `test_prompt_extended.py` – verifies language sanitization, timestamp generation, and prompt building. * `test_tts_asr_coverage.py` – checks device detection, cache clearing, and model loading under various environment configurations. * `test_tts_asr_extended.py` – further tests TTS/ASR device selection and time‑outs. Updated `backend/requirements.txt` to use newer, compatible packages, removed obsolete testing dependencies, and added `qwen-tts`. Modified `backend/tts_asr.py` to work with the new `Qwen3TTSModel`, simplified imports, and adjusted device mapping logic. Additionally, frontend changes added a new `TreeNodeItem` component, updated Markdown rendering, added TTS instruction fields, and reworked context menu handling. No breaking changes were introduced.
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
node: { type: Object, required: true },
|
||||
level: { type: Number, required: true },
|
||||
selectedId: { type: String, default: null },
|
||||
expandedIds: { type: Object, required: true },
|
||||
clipboard: { type: Object, default: null },
|
||||
getIconClass: { type: Function, required: true },
|
||||
renameId: { type: String, default: null },
|
||||
renameValue: { type: String, default: '' },
|
||||
creatingInFolder: { type: String, default: null },
|
||||
creatingType: { type: String, default: null },
|
||||
creatingName: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'select',
|
||||
'toggle',
|
||||
'start-rename',
|
||||
'finish-rename',
|
||||
'cancel-rename',
|
||||
'update:rename-value',
|
||||
'start-create',
|
||||
'finish-create',
|
||||
'cancel-create',
|
||||
'update:creating-name',
|
||||
'context-menu',
|
||||
'drop',
|
||||
'drag-start',
|
||||
'drag-over'
|
||||
])
|
||||
|
||||
const isSelected = computed(() => props.node.id === props.selectedId)
|
||||
const isExpanded = computed(() => props.expandedIds?.has(props.node.id))
|
||||
const isClipped = computed(() => {
|
||||
if (!props.clipboard) return false
|
||||
return props.clipboard.node?.id === props.node.id || props.clipboard.nodeId === props.node.id
|
||||
})
|
||||
const isRenaming = computed(() => props.renameId === props.node.id)
|
||||
const isCreating = computed(() => props.creatingInFolder === props.node.id)
|
||||
const paddingLeft = computed(() => `${props.level * 16 + 12}px`)
|
||||
const createPaddingLeft = computed(() => `${(props.level + 1) * 16 + 12}px`)
|
||||
|
||||
function handleContextMenu(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
emit('context-menu', event.clientX, event.clientY, props.node)
|
||||
}
|
||||
|
||||
function handleDrop(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const draggedId = event.dataTransfer.getData('text/plain')
|
||||
if (draggedId) emit('drop', draggedId, props.node)
|
||||
}
|
||||
|
||||
function handleDragStart(event) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('text/plain', props.node.id)
|
||||
emit('drag-start', event, props.node.id)
|
||||
}
|
||||
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault()
|
||||
emit('drag-over', event, props.node.id)
|
||||
}
|
||||
|
||||
function forwardStartCreate(id, type) {
|
||||
emit('start-create', id, type)
|
||||
}
|
||||
|
||||
function forwardContextMenu(x, y, node) {
|
||||
emit('context-menu', x, y, node)
|
||||
}
|
||||
|
||||
function forwardDrop(id, node) {
|
||||
emit('drop', id, node)
|
||||
}
|
||||
|
||||
function forwardDragStart(event, id) {
|
||||
emit('drag-start', event, id)
|
||||
}
|
||||
|
||||
function forwardDragOver(event, id) {
|
||||
emit('drag-over', event, id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tree-node"
|
||||
:class="{ selected: isSelected, clipped: isClipped }"
|
||||
:style="{ paddingLeft }"
|
||||
draggable="true"
|
||||
@click="emit('select', node.id)"
|
||||
@contextmenu="handleContextMenu"
|
||||
@dragstart="handleDragStart"
|
||||
@dragover="handleDragOver"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<button
|
||||
v-if="node.type === 'folder'"
|
||||
class="chevron"
|
||||
type="button"
|
||||
@click.stop="emit('toggle', node.id)"
|
||||
>
|
||||
<svg v-if="isExpanded" viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M4 6l4 4 4-4z" /></svg>
|
||||
<svg v-else viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M6 4l4 4-4 4z" /></svg>
|
||||
</button>
|
||||
<span v-else class="chevron-placeholder"></span>
|
||||
|
||||
<span :class="getIconClass(node.type, node.name)"></span>
|
||||
|
||||
<input
|
||||
v-if="isRenaming"
|
||||
class="rename-input"
|
||||
:value="renameValue"
|
||||
@input="emit('update:rename-value', $event.target.value)"
|
||||
@keydown.enter="emit('finish-rename', node)"
|
||||
@keydown.esc="emit('cancel-rename')"
|
||||
@blur="emit('finish-rename', node)"
|
||||
/>
|
||||
<span v-else class="node-name">{{ node.name }}</span>
|
||||
|
||||
<div v-if="node.type === 'folder'" class="node-actions">
|
||||
<button class="action-btn" type="button" title="新建文件" @click.stop="emit('start-create', node.id, 'file')">
|
||||
<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor"><path d="M4.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h6.5a.25.25 0 00.25-.25V5.664a.25.25 0 00-.073-.177L8.513 2.573A.25.25 0 008.336 2.5H4.75zm0-1.5h3.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0111.25 16h-6.5A1.75 1.75 0 013 14.25V1.75A1.75 1.75 0 014.75 0z"/><path d="M8 6a.75.75 0 01.75.75v1.5h1.5a.75.75 0 010 1.5h-1.5v1.5a.75.75 0 01-1.5 0v-1.5h-1.5a.75.75 0 010-1.5h1.5v-1.5A.75.75 0 018 6z"/></svg>
|
||||
</button>
|
||||
<button class="action-btn" type="button" title="新建文件夹" @click.stop="emit('start-create', node.id, 'folder')">
|
||||
<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor"><path d="M1.75 1A1.75 1.75 0 000 2.75v8.5C0 12.216.784 13 1.75 13h12.5A1.75 1.75 0 0016 11.25v-6.5A1.75 1.75 0 0014.25 3H7.31l-.97-.97A1.75 1.75 0 005.103 1H1.75zm0 1.5h3.353a.25.25 0 01.177.073l1.409 1.408c.14.141.332.22.53.22h7.03a.25.25 0 01.25.25v6.8a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25z"/><path d="M8 6a.75.75 0 01.75.75v1h1a.75.75 0 010 1.5h-1v1a.75.75 0 01-1.5 0v-1h-1a.75.75 0 010-1.5h1v-1A.75.75 0 018 6z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCreating" class="tree-node tree-node-new" :style="{ paddingLeft: createPaddingLeft }">
|
||||
<span class="chevron-placeholder"></span>
|
||||
<span :class="creatingType === 'folder' ? 'icon-folder' : 'icon-file'"></span>
|
||||
<input
|
||||
class="rename-input"
|
||||
:value="creatingName"
|
||||
:placeholder="creatingType === 'folder' ? '输入文件夹名称' : '输入文件名,例如 note.md'"
|
||||
@input="emit('update:creating-name', $event.target.value)"
|
||||
@keydown.enter="emit('finish-create')"
|
||||
@keydown.esc="emit('cancel-create')"
|
||||
@blur="emit('finish-create')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="node.type === 'folder' && isExpanded">
|
||||
<TreeNodeItem
|
||||
v-for="child in node.children || []"
|
||||
:key="child.id"
|
||||
:node="child"
|
||||
:level="level + 1"
|
||||
:selected-id="selectedId"
|
||||
:expanded-ids="expandedIds"
|
||||
:clipboard="clipboard"
|
||||
:get-icon-class="getIconClass"
|
||||
:rename-id="renameId"
|
||||
:rename-value="renameValue"
|
||||
:creating-in-folder="creatingInFolder"
|
||||
:creating-type="creatingType"
|
||||
:creating-name="creatingName"
|
||||
@select="emit('select', $event)"
|
||||
@toggle="emit('toggle', $event)"
|
||||
@start-rename="emit('start-rename', $event)"
|
||||
@finish-rename="emit('finish-rename', $event)"
|
||||
@cancel-rename="emit('cancel-rename')"
|
||||
@update:rename-value="emit('update:rename-value', $event)"
|
||||
@start-create="forwardStartCreate"
|
||||
@finish-create="emit('finish-create')"
|
||||
@cancel-create="emit('cancel-create')"
|
||||
@update:creating-name="emit('update:creating-name', $event)"
|
||||
@context-menu="forwardContextMenu"
|
||||
@drop="forwardDrop"
|
||||
@drag-start="forwardDragStart"
|
||||
@drag-over="forwardDragOver"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
Reference in New Issue
Block a user