feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import NodeRenderer from '../packages/markstream-vue2/src/components/NodeRenderer/NodeRenderer.vue'
|
||||
import { flushAll } from './setup/flush-all'
|
||||
|
||||
describe('markstream-vue2 text node streaming consistency', () => {
|
||||
it('keeps the stale-delta settle hook wired to outer stream ticks', () => {
|
||||
const nodeRendererSource = readFileSync(
|
||||
resolve(process.cwd(), 'packages/markstream-vue2/src/components/NodeRenderer/NodeRenderer.vue'),
|
||||
'utf8',
|
||||
)
|
||||
const textNodeSource = readFileSync(
|
||||
resolve(process.cwd(), 'packages/markstream-vue2/src/components/TextNode/TextNode.vue'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
expect(nodeRendererSource).toContain('const streamRenderVersion = ref(0)')
|
||||
expect(nodeRendererSource).toContain('provide(\'markstreamStreamVersion\', streamRenderVersion)')
|
||||
expect(nodeRendererSource).toContain('[() => props.content, () => props.nodes]')
|
||||
|
||||
expect(textNodeSource).toContain('inject<{ value?: number } | undefined>(\'markstreamStreamVersion\', undefined)')
|
||||
expect(textNodeSource).toContain('[() => props.node.content, streamStateKey, typewriterEnabled, () => inheritedStreamVersion?.value]')
|
||||
expect(textNodeSource).toContain('if (normalized === previousContent)')
|
||||
expect(textNodeSource).toContain('if (streamedDelta.value)')
|
||||
expect(textNodeSource).toContain('settleStreamedDelta()')
|
||||
})
|
||||
|
||||
it('keeps explanatory list text visible while an inline code span is still streaming', async () => {
|
||||
const originalWarn = console.warn
|
||||
const originalError = console.error
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation((message?: any, ...args: any[]) => {
|
||||
const text = String(message ?? '')
|
||||
if (
|
||||
text.includes('provide() can only be used inside setup().')
|
||||
|| text.includes('onMounted is called when there is no active component instance')
|
||||
|| text.includes('onBeforeUnmount is called when there is no active component instance')
|
||||
) {
|
||||
return
|
||||
}
|
||||
originalWarn.call(console, message, ...args)
|
||||
})
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation((message?: any, ...args: any[]) => {
|
||||
const text = String(message ?? '')
|
||||
if (
|
||||
text.includes('provide() can only be used inside setup().')
|
||||
|| text.includes('onMounted is called when there is no active component instance')
|
||||
|| text.includes('onBeforeUnmount is called when there is no active component instance')
|
||||
) {
|
||||
return
|
||||
}
|
||||
originalError.call(console, message, ...args)
|
||||
})
|
||||
|
||||
const initial = `- **计算工具验证**
|
||||
通过数学计算工具确认结果:`
|
||||
const mid = `${initial}
|
||||
\`363 ÷ 15,135 × 100 = 2.39841427...`
|
||||
const final = `${mid}\``
|
||||
const md = getMarkdown('vue2-inline-code-explanatory-text-stability')
|
||||
const getListItemChildren = (content: string) => {
|
||||
const nodes = parseMarkdownToStructure(content, md, { final: false }) as any[]
|
||||
return (nodes[0] as any)?.items?.[0]?.children ?? []
|
||||
}
|
||||
const initialNodes = getListItemChildren(initial)
|
||||
const midNodes = getListItemChildren(mid)
|
||||
const finalNodes = getListItemChildren(final)
|
||||
const getParagraphChildren = (nodes: any[]) => (nodes[0] as any)?.children ?? []
|
||||
expect(getParagraphChildren(midNodes).some((child: any) => child?.type === 'inline_code')).toBe(true)
|
||||
expect(getParagraphChildren(finalNodes).some((child: any) => child?.type === 'inline_code')).toBe(true)
|
||||
|
||||
const wrapper = mount(NodeRenderer as any, {
|
||||
props: {
|
||||
nodes: initialNodes as any,
|
||||
batchRendering: false,
|
||||
deferNodesUntilVisible: false,
|
||||
viewportPriority: false,
|
||||
maxLiveNodes: 0,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
await flushAll()
|
||||
|
||||
await wrapper.setProps({
|
||||
nodes: midNodes as any,
|
||||
})
|
||||
await flushAll()
|
||||
|
||||
const midText = wrapper.text().replace(/\s*\n\s*/g, '')
|
||||
expect(midText).toContain('计算工具验证通过数学计算工具确认结果:')
|
||||
expect(midText.match(/通过数学计算工具确认结果:/g) ?? []).toHaveLength(1)
|
||||
expect(wrapper.get('.strong-node').text()).toBe('计算工具验证')
|
||||
|
||||
await wrapper.setProps({
|
||||
nodes: finalNodes as any,
|
||||
})
|
||||
await flushAll()
|
||||
|
||||
const finalText = wrapper.text().replace(/\s*\n\s*/g, '')
|
||||
expect(finalText).toContain('计算工具验证通过数学计算工具确认结果:')
|
||||
expect(finalText.match(/通过数学计算工具确认结果:/g) ?? []).toHaveLength(1)
|
||||
expect(wrapper.get('.strong-node').text()).toBe('计算工具验证')
|
||||
}
|
||||
finally {
|
||||
wrapper.unmount()
|
||||
warnSpy.mockRestore()
|
||||
errorSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user