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.
308 lines
14 KiB
TypeScript
308 lines
14 KiB
TypeScript
// The Host-half plugin module (src/host/index.ts) against the REAL cordis registry, session store, and session-projection registry: real
|
|
// envelopes appended to a real session, folded by the registered units, read back through the snapshot cut and the change feed.
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, test } from 'vitest'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import SessionStore from '@deepseek-ai/dsh-session'
|
|
import type { Session } from '@deepseek-ai/dsh-session'
|
|
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
|
import { apply, inject, name } from '../../src/host/index'
|
|
import { createContextTimelineDefinition } from '../../src/host/timeline'
|
|
import { BASELINE_DSH_VERSION } from '../../src/shared/version'
|
|
import type {} from '../../src/shared/types'
|
|
|
|
const plugin = { name, inject, apply } as never
|
|
const noConfig = {} as never
|
|
|
|
/** The committed version-probe homes (see version.spec.ts). */
|
|
const HOMES = fileURLToPath(new URL('./fixtures/version/homes', import.meta.url))
|
|
|
|
async function bootWithHome(home: string) {
|
|
const ctx = new Context()
|
|
ctx.provide('dshHomePath', (...segments: string[]) => join(HOMES, home, ...segments))
|
|
await ctx.plugin(SessionStore)
|
|
await ctx.plugin(SessionProjectionRegistry)
|
|
const fiber = await ctx.plugin(plugin, noConfig)
|
|
return { ctx, fiber }
|
|
}
|
|
|
|
async function until<T>(read: () => T | undefined, message: string): Promise<T> {
|
|
for (let i = 0; i < 200; i++) {
|
|
const value = read()
|
|
if (value !== undefined) return value
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
}
|
|
assert.fail(message)
|
|
}
|
|
|
|
/** The minimal real envelope trio: header epoch, user message, metered assistant reply. */
|
|
function appendRealEnvelopes(session: Session): void {
|
|
session.append('request/header', {
|
|
header: {
|
|
config: { model: 'deepseek-v4-flash', provider: 'deepseek' },
|
|
},
|
|
reason: 'initial',
|
|
})
|
|
session.append('user/message', {
|
|
content: [{ type: 'text', text: 'hi' }],
|
|
source: { kind: 'user' },
|
|
} as never, { surfaceOp: 'append' })
|
|
session.append('assistant/message', {
|
|
turn: 1,
|
|
step: 0,
|
|
message: { content: [{ type: 'text', text: 'hello' }] },
|
|
usage: { inputTokens: 10, outputTokens: 5 },
|
|
} as never, { surfaceOp: 'append' })
|
|
}
|
|
|
|
async function boot() {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SessionStore)
|
|
await ctx.plugin(SessionProjectionRegistry)
|
|
const fiber = await ctx.plugin(plugin, noConfig)
|
|
return { ctx, fiber }
|
|
}
|
|
|
|
describe('dsh-context host plugin', () => {
|
|
test('module shape: name and the sessionProjections inject gate', () => {
|
|
assert.equal(name, 'dsh-context')
|
|
assert.deepEqual(inject, ['sessionProjections'])
|
|
})
|
|
|
|
test('registers all three units and serves the folded views over real appends', async () => {
|
|
const { ctx } = await boot()
|
|
const session = ctx.sessions.create()
|
|
appendRealEnvelopes(session)
|
|
|
|
const snapshot = ctx.sessionProjections.snapshot(session)
|
|
const timeline = snapshot.values.contextTimeline
|
|
assert.ok(timeline !== undefined, 'contextTimeline served after real appends')
|
|
assert.equal(timeline.ok, true)
|
|
assert.equal(timeline.model, 'deepseek-v4-flash')
|
|
assert.equal(timeline.provider, 'deepseek')
|
|
assert.ok(timeline.current.total > 0, 'the folded totals are non-zero')
|
|
assert.equal(timeline.nodes.length, 2, 'user + assistant surface nodes')
|
|
|
|
const headers = snapshot.values.contextHeaders
|
|
assert.ok(headers !== undefined, 'contextHeaders served after real appends')
|
|
assert.equal(headers.headers.length, 1)
|
|
assert.ok(!('system' in headers.headers[0]), 'system text stays in the log, not the projection')
|
|
assert.ok(!('systemTokens' in headers.headers[0]), 'the system price lives in the timeline systems, never the epoch')
|
|
|
|
const activity = snapshot.values.contextActivity
|
|
assert.ok(activity !== undefined, 'contextActivity served after real appends')
|
|
const entries = Object.values(activity.days)
|
|
assert.equal(entries.length, 1, 'the one settlement books its day')
|
|
assert.equal(entries[0].requests, 1)
|
|
assert.equal(entries[0].tokens, 15, 'the metered buckets sum into the day (10 input + 5 output)')
|
|
})
|
|
|
|
test('the change feed fires with the schema-validated view', async () => {
|
|
const { ctx } = await boot()
|
|
const session = ctx.sessions.create()
|
|
const seen: Array<{ key: string; value: unknown }> = []
|
|
ctx.sessionProjections.onChanged((changedSession, key, value) => {
|
|
if (changedSession === session) seen.push({ key, value })
|
|
})
|
|
appendRealEnvelopes(session)
|
|
|
|
const timeline = seen.filter(e => e.key === 'contextTimeline')
|
|
assert.ok(timeline.length > 0, 'contextTimeline changes notified')
|
|
const last = timeline.at(-1)?.value
|
|
assert.equal(
|
|
createContextTimelineDefinition({}, () => false).wire.viewSchema.safeParse(last).success,
|
|
true,
|
|
'the notified value is the validated wire view',
|
|
)
|
|
assert.ok(seen.some(e => e.key === 'contextHeaders'), 'contextHeaders changes notified')
|
|
})
|
|
|
|
test('tool schema rows carry best-effort plugin attribution through real appends', async () => {
|
|
const { ctx } = await boot()
|
|
const session = ctx.sessions.create()
|
|
session.append('request/header', {
|
|
header: {
|
|
config: { model: 'deepseek-v4-flash', provider: 'deepseek' },
|
|
tools: [
|
|
{ name: 'bash', description: 'run', parameters: { type: 'object' } },
|
|
{ name: 'mcp__github__get_issue', description: 'm', parameters: { type: 'object' } },
|
|
{ name: 'agent_teams_add_member', description: 't', parameters: { type: 'object' } },
|
|
],
|
|
},
|
|
reason: 'initial',
|
|
} as never)
|
|
const headers = ctx.sessionProjections.snapshot(session).values.contextHeaders
|
|
assert.ok(headers !== undefined, 'contextHeaders served after real appends')
|
|
const tools = headers.headers[0].tools
|
|
assert.equal(tools.find(t => t.name === 'bash')?.plugin, '@deepseek-ai/dsh-tool-bash')
|
|
assert.equal(tools.find(t => t.name === 'mcp__github__get_issue')?.plugin, 'mcp:github')
|
|
assert.ok(!('plugin' in tools.find(t => t.name === 'agent_teams_add_member')!), 'unmapped third-party tools stay untagged')
|
|
})
|
|
|
|
test('register-hook attribution flows through to the rendered headers', async () => {
|
|
const { ctx } = await boot()
|
|
// dsh-context booted before any tools service existed; a late-provided
|
|
// instance is wrapped on first read inside the registering plugin.
|
|
ctx.provide('tools', { register() { return () => {} } })
|
|
await ctx.plugin({
|
|
name: 'my-agent-tools',
|
|
apply(agentCtx) {
|
|
const tools = (agentCtx as any).tools
|
|
tools.register({ name: 'custom_dynamic_tool', description: 'd', parameters: { type: 'object' } })
|
|
},
|
|
})
|
|
const session = ctx.sessions.create()
|
|
session.append('request/header', {
|
|
header: {
|
|
config: { model: 'deepseek-v4-flash', provider: 'deepseek' },
|
|
tools: [
|
|
{ name: 'custom_dynamic_tool', description: 'd', parameters: { type: 'object' } },
|
|
{ name: 'bash', description: 'run', parameters: { type: 'object' } },
|
|
],
|
|
},
|
|
reason: 'initial',
|
|
} as never)
|
|
const headers = ctx.sessionProjections.snapshot(session).values.contextHeaders
|
|
assert.ok(headers !== undefined, 'contextHeaders served after real appends')
|
|
const tools = headers.headers[0].tools
|
|
assert.equal(tools.find(t => t.name === 'custom_dynamic_tool')?.plugin, 'my-agent-tools')
|
|
assert.equal(tools.find(t => t.name === 'bash')?.plugin, '@deepseek-ai/dsh-tool-bash', 'static backbone still applies')
|
|
})
|
|
|
|
test('disposing the plugin fiber removes both keys from later snapshots', async () => {
|
|
const { ctx, fiber } = await boot()
|
|
const session = ctx.sessions.create()
|
|
appendRealEnvelopes(session)
|
|
assert.ok(ctx.sessionProjections.snapshot(session).values.contextTimeline !== undefined)
|
|
|
|
await fiber.dispose()
|
|
const snapshot = ctx.sessionProjections.snapshot(session)
|
|
assert.equal(snapshot.values.contextTimeline, undefined, 'an unloaded plugin reads as capability absence')
|
|
assert.equal(snapshot.values.contextHeaders, undefined)
|
|
})
|
|
|
|
test('the split generation with the detail route live: slim wire head + the endpoint serves the collections', async () => {
|
|
const ctx = new Context()
|
|
// The plugin's own routes (detail, fleet, attachment, warm-up trigger, balance, skills); keep them by path.
|
|
const routes = new Map<string, { fetch?: (request: Request) => Promise<Response> }>()
|
|
ctx.provide('connection', {
|
|
fetch: {
|
|
register: (r: never) => {
|
|
const route = r as { path: string; fetch?: (request: Request) => Promise<Response> }
|
|
routes.set(route.path, route)
|
|
return () => {}
|
|
},
|
|
},
|
|
})
|
|
await ctx.plugin(SessionStore)
|
|
await ctx.plugin(SessionProjectionRegistry)
|
|
await ctx.plugin(plugin, noConfig)
|
|
const session = ctx.sessions.create()
|
|
appendRealEnvelopes(session)
|
|
// A file-tool pair: the fold-derived op log serves through the detail endpoint.
|
|
const call = session.append('tool/call', { turn: 1, step: 1, callId: 'c1', name: 'read', arguments: JSON.stringify({ file_path: 'src/a.ts' }) } as never)
|
|
session.append('tool/result', {
|
|
message: { role: 'tool', toolCallId: 'c1', content: [{ type: 'text', text: 'ok' }], source: { kind: 'tool', callId: 'c1' } },
|
|
} as never, { surfaceOp: 'append', sourceEventSeqs: [call.seq] })
|
|
|
|
const timeline = ctx.sessionProjections.snapshot(session).values.contextTimeline
|
|
assert.ok(timeline !== undefined)
|
|
assert.equal(typeof timeline.detailRev, 'number', 'the split marker rides the slim head')
|
|
assert.deepEqual(timeline.counts, { turns: 1, steps: 1, injects: 0, compactions: 0, prunes: 0, skills: 0 })
|
|
assert.equal(timeline.nodes.length, 0, 'the collections stay off the wire value')
|
|
assert.equal(timeline.requests.length, 0)
|
|
|
|
assert.ok(routes.has('/api/dsh-context/attachment'), 'the attachment-location route registered beside the detail route')
|
|
const route = routes.get('/api/dsh-context/detail')
|
|
assert.ok(route !== undefined && route.fetch !== undefined, 'the detail route registered')
|
|
const response = await route.fetch(new Request('http://dsh.test/api/dsh-context/detail', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ sessionId: session.header.id }),
|
|
}))
|
|
const result = await response.json() as {
|
|
ok: boolean
|
|
value: { rev: number; nodes: unknown[]; requests: unknown[] } | null
|
|
}
|
|
assert.equal(result.ok, true)
|
|
assert.ok(result.value !== null, 'a viewed session is live — its detail serves')
|
|
assert.equal(result.value.rev, timeline.detailRev, 'the payload mirrors the head revision')
|
|
assert.equal(result.value.nodes.length, 3, 'user + assistant + tool-result surface nodes')
|
|
assert.equal(result.value.requests.length, 1)
|
|
const ops = (result.value as { fileOps?: { path: string; kind: string; tool: string }[] }).fileOps
|
|
assert.deepEqual(ops?.map(o => [o.kind, o.tool, o.path]), [['read', 'read', 'src/a.ts']])
|
|
})
|
|
|
|
test('without the connection service the wire value stays inline', async () => {
|
|
const { ctx } = await boot()
|
|
const session = ctx.sessions.create()
|
|
appendRealEnvelopes(session)
|
|
const timeline = ctx.sessionProjections.snapshot(session).values.contextTimeline
|
|
assert.ok(timeline !== undefined)
|
|
assert.equal(timeline.detailRev, undefined, 'no channel, no split marker')
|
|
assert.equal(timeline.nodes.length, 2, 'the collections ride the wire value as before')
|
|
})
|
|
|
|
test('stays pending without the registry, starts when it arrives', async () => {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SessionStore)
|
|
const fiber = ctx.plugin(plugin, noConfig)
|
|
const session = ctx.sessions.create()
|
|
assert.doesNotThrow(() => appendRealEnvelopes(session), 'an absent registry leaves the plugin inert')
|
|
assert.equal(ctx.get('sessionProjections'), undefined)
|
|
assert.notEqual((fiber as unknown as { state: number }).state, 2, 'the fiber is not ACTIVE without its inject')
|
|
|
|
await ctx.plugin(SessionProjectionRegistry)
|
|
const timeline = await until(
|
|
() => ctx.sessionProjections.snapshot(session).values.contextTimeline,
|
|
'the plugin never started after the registry arrived',
|
|
)
|
|
assert.equal(timeline.ok, true, 'the late-mounted registry folds the already-appended log')
|
|
await fiber
|
|
})
|
|
})
|
|
|
|
describe('the baseline gate', () => {
|
|
test('a below-baseline harness gets the fallback units instead of the folds', async () => {
|
|
const { ctx } = await bootWithHome('old')
|
|
const session = ctx.sessions.create()
|
|
appendRealEnvelopes(session)
|
|
|
|
const timeline = ctx.sessionProjections.snapshot(session).values.contextTimeline
|
|
assert.ok(timeline !== undefined, 'the fallback unit still delivers (no eternal loading)')
|
|
assert.deepEqual(timeline.unsupported, { current: '0.1.5-rc.1', minimum: BASELINE_DSH_VERSION })
|
|
assert.equal(timeline.current.total, 0, 'the log is NOT folded below the baseline')
|
|
assert.equal(timeline.nodes.length, 0, 'real appends leave no surface nodes')
|
|
assert.equal(timeline.model, undefined, 'no model metadata either')
|
|
|
|
const headers = ctx.sessionProjections.snapshot(session).values.contextHeaders
|
|
assert.ok(headers !== undefined)
|
|
assert.deepEqual(headers.headers, [], 'the header epoch is not folded either')
|
|
})
|
|
|
|
test('the baseline itself and newer compose the real units', async () => {
|
|
for (const home of ['baseline', 'future']) {
|
|
const { ctx } = await bootWithHome(home)
|
|
const session = ctx.sessions.create()
|
|
appendRealEnvelopes(session)
|
|
const timeline = ctx.sessionProjections.snapshot(session).values.contextTimeline
|
|
assert.ok(timeline !== undefined, home)
|
|
assert.equal(timeline.unsupported, undefined, home)
|
|
assert.ok(timeline.current.total > 0, `the real fold runs on ${home}`)
|
|
}
|
|
})
|
|
|
|
test('an unparseable harness version fails open into the real units', async () => {
|
|
const { ctx } = await bootWithHome('dev')
|
|
const session = ctx.sessions.create()
|
|
appendRealEnvelopes(session)
|
|
const timeline = ctx.sessionProjections.snapshot(session).values.contextTimeline
|
|
assert.ok(timeline !== undefined)
|
|
assert.equal(timeline.unsupported, undefined)
|
|
assert.ok(timeline.current.total > 0, 'a dev-channel harness build is never gated')
|
|
})
|
|
})
|