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,111 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const md = getMarkdown('e2e')
|
||||
|
||||
const FIXTURES_DIR = resolve(__dirname, 'fixtures')
|
||||
|
||||
describe('e2e markdown parsing (fixtures)', () => {
|
||||
const files = [
|
||||
'headings.md',
|
||||
'code-diff.md',
|
||||
'table.md',
|
||||
'admonition.md',
|
||||
'math.md',
|
||||
'footnotes.md',
|
||||
'image-link.md',
|
||||
'checkbox.md',
|
||||
'mermaid.md',
|
||||
// edge-case fixtures
|
||||
'unclosed-fence.md',
|
||||
'trailing-backticks.md',
|
||||
'unmatched-brackets.md',
|
||||
'escaped-brackets.md',
|
||||
'nested-lists-edge.md',
|
||||
'fence-with-meta.md',
|
||||
]
|
||||
|
||||
for (const f of files) {
|
||||
it(`parses fixture ${f}`, () => {
|
||||
const src = readFileSync(resolve(FIXTURES_DIR, f), 'utf8')
|
||||
const nodes = parseMarkdownToStructure(src, md)
|
||||
expect(nodes).toBeInstanceOf(Array)
|
||||
// Ensure we have at least one top-level node
|
||||
expect(nodes.length).toBeGreaterThan(0)
|
||||
// Create a stable, minimal serialization for snapshot
|
||||
// Helper to find first descendant text for a node
|
||||
const findFirstText = (node: any): string | undefined => {
|
||||
if (!node)
|
||||
return undefined
|
||||
if (node.type === 'text' && typeof node.content === 'string')
|
||||
return node.content.slice(0, 80)
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const c of node.children) {
|
||||
const t = findFirstText(c)
|
||||
if (t)
|
||||
return t
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const pickMeta = (meta: any) => {
|
||||
if (!meta || typeof meta !== 'object')
|
||||
return undefined
|
||||
const keys = ['diff', 'language', 'closed', 'unclosed']
|
||||
const out: any = {}
|
||||
for (const k of keys) {
|
||||
if (k in meta)
|
||||
out[k] = meta[k]
|
||||
}
|
||||
return Object.keys(out).length ? out : undefined
|
||||
}
|
||||
|
||||
const minimal = nodes.map((n: any) => {
|
||||
const out: any = { type: n.type }
|
||||
out.firstText = findFirstText(n)
|
||||
if (n.meta)
|
||||
out.meta = pickMeta(n.meta)
|
||||
|
||||
if (n.type === 'heading') {
|
||||
out.level = (n as any).level
|
||||
out.children = (n as any).children?.map((c: any) => c.type)
|
||||
}
|
||||
else if (n.type === 'list') {
|
||||
out.ordered = (n as any).ordered
|
||||
out.items = (n as any).items?.length ?? 0
|
||||
}
|
||||
else if (n.type === 'code_block') {
|
||||
out.language = (n as any).language
|
||||
out.diff = !!(n as any).diff
|
||||
// include a short preview of code
|
||||
out.preview = (n as any).code ? (n as any).code.slice(0, 120) : undefined
|
||||
}
|
||||
else if (n.type === 'table') {
|
||||
out.rows = (n as any).rows?.length ?? 0
|
||||
}
|
||||
else if (n.type === 'footnote') {
|
||||
out.items = (n as any).items?.length ?? 0
|
||||
}
|
||||
else {
|
||||
// generic: record if has children
|
||||
out.children = (n as any).children ? (n as any).children.map((item: any) => item.type) : undefined
|
||||
}
|
||||
// Tolerate environment differences for escaped backslash in a specific fixture
|
||||
if (f === 'escaped-brackets.md' && typeof out.firstText === 'string') {
|
||||
// Normalize to snapshot’s expected form (no visible backslash)
|
||||
out.firstText = out.firstText
|
||||
.replace('literal backslash \\ and', 'literal backslash and')
|
||||
.replace('literal backslash and', 'literal backslash and')
|
||||
}
|
||||
return out
|
||||
})
|
||||
|
||||
// In some environments, a literal backslash may be normalized in text nodes.
|
||||
// Keep snapshot tolerant to a missing visual backslash by stringifying the object.
|
||||
expect(minimal).toMatchSnapshot(f)
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user