Files
dsh-context/tests/client/primitives.spec.ts
T
bowenliang123 6487602f1e feat(client): render message attachments in order and preview them in the sidebar
A user message's image and file blocks now render as one ordered attachment
block in the Context browser: images keep their session-authorized thumbnail,
other files show their type glyph, extension, and stored size, and the row
chip counts every attachment. One click IS the preview — the card hands the
attachment's named host path to dsh's own preview column (the File Activity
card's `previewAddressOf` → `openResourceVia` path), which renders markdown,
code, PDF, office, and image viewers under the file's real name. A refused
open reports on that card alone; the image lightbox and the per-type modal it
replaced are gone with `images.tsx`.

The path comes from the plugin's own `/api/dsh-context/attachment` route (the
same authenticated `/api` fence, Connection exact-route registry, and
cold-read gate as the detail and fleet routes). It answers only an id the
session's own log cites — a depth-bounded, total walk over the event payload
(user content, tool-result blocks, spliced inbox deliveries) — and answers
the store's named alias: `attachments.fileHostPath` for verbatim files, and a
published-or-reused named hard link for images, whose objects are digest-named
and suffixless while the preview column dispatches viewers by suffix. The
link shares the immutable object bytes and lives in a disposable cache
(`<home>/cache/dsh-context/previews/`); a store that is not host-file-backed,
or a media type the harness never admits, resolves the typed null the card
reports as an unavailable preview.

`Section` moves to its own module so the attachment block heads itself
without importing the tool card, and the compat matrix probes `FileTypeIcon`
alongside the icons it already pins.
2026-10-13 21:19:47 +08:00

53 lines
2.4 KiB
TypeScript

// The defensive icon resolver (src/client/primitives.ts): each ui-primitives icon resolves by its export name;
// a name missing from the module (a future rename) or a namespace that throws on the read degrades to a
// render-nothing component instead of the React #130 element-type crash.
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { FileTypeGlyph, resolveFileGlyph, resolveIcon, IconBranch, IconPlus, IconCheck, IconCopy, IconClose, IconSettings, IconChevronDown } from '../../src/client/primitives'
describe('resolveIcon', () => {
const icon = function SomeIcon(): null { return null }
test('a present export resolves to the component', () => {
assert.equal(resolveIcon('icon', { icon }), icon)
})
test('a name present but not a component renders nothing', () => {
const resolved = resolveIcon('icon', { icon: 42 })
assert.equal(resolved({}), null, 'the fallback component renders null')
})
test('a missing name renders nothing (a future rename)', () => {
const resolved = resolveIcon('IconGhostOutlineRegular', {})
assert.equal(resolved({}), null)
})
test('a namespace that throws on property access degrades to the fallback', () => {
// Partial module mocks and hostile namespaces throw on the READ itself.
const hostile = new Proxy({}, { get() { throw new Error('hostile export read') } })
const resolved = resolveIcon('IconBranchOutlineRegular', hostile)
assert.equal(resolved({}), null)
})
test('the module-level exports resolve against the pinned generation', () => {
// The devDep carries the supported vocabulary, so these resolved at module load.
for (const icon of [IconBranch, IconPlus, IconCheck, IconCopy, IconClose, IconSettings, IconChevronDown]) {
assert.equal(typeof icon, 'function', 'every seam resolves to a component')
}
assert.equal(typeof FileTypeGlyph, 'function', 'the file-type glyph resolves too')
})
})
describe('resolveFileGlyph', () => {
const glyph = function SomeGlyph(): null { return null }
test('a present export resolves; a missing, non-component, or hostile read renders nothing', () => {
assert.equal(resolveFileGlyph({ FileTypeIcon: glyph }), glyph)
assert.equal(resolveFileGlyph({ FileTypeIcon: 42 })({}), null)
assert.equal(resolveFileGlyph({})({}), null)
const hostile = new Proxy({}, { get() { throw new Error('hostile export read') } })
assert.equal(resolveFileGlyph(hostile)({}), null)
})
})