Files
deepseek-harness/apps/web/tests/assembled-remote.ts
T
Yichen Jiang 9be99a0e28 feat(web): upload composer images when they are added
An ordinary Session's composer now uploads each image draft through the
background upload queue as soon as it is added. The raw upload route stages
the body as a prompt image when the request names its media type: the Host
reads at most the per-image byte limit, then validates, normalizes, and
stores the image as prompt admission would. The prompt cites the receipt in
a file part; prompt admission resolves it to the stored image, applies the
image-input check, and counts stored images toward maxImagesPerMessage.

Send waits for every image receipt, and a failed image upload turns its
thumbnail into a retry control. Commands and subagent continuations keep
encoding images inline.
2026-10-21 13:50:19 +08:00

459 lines
17 KiB
TypeScript

/**
* RemoteMock scenario for built-client tests that do not own a Host.
* The adjacent JSON is maintained with this module when Remote responses or
* the current Session header version change.
* Fixture Sessions have no retained Host terminals to restore.
*/
import { Buffer } from 'node:buffer'
import { readFileSync } from 'node:fs'
import { randomUUID } from 'node:crypto'
import { join } from 'node:path'
import { ok, RemoteMock } from '@deepseek-ai/dsh-remote-mock'
import { remoteDefaultResponses } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/remote-default-responses.ts'
interface SessionSummary {
readonly sessionId: string
updatedAt: number
running: boolean
blank: boolean
readonly parentSessionId?: string
readonly origin?: 'subagent'
readonly cwd?: string
readonly projections?: {
readonly kind: 'cached' | 'sequenced'
readonly asOfSeq: number
readonly values: Readonly<Record<string, unknown>>
}
}
interface WorkspaceView {
readonly workspaceId: string
readonly path: string
readonly title: string
sessionIds: string[]
readonly createdAt: string
updatedAt: string
}
interface EventRecord {
readonly type: 'event'
readonly event: {
readonly seq: number
readonly time: number
readonly type: string
readonly data: unknown
readonly surfaceOp?: string
}
}
interface FollowSnapshot {
readonly type: 'snapshot'
readonly header: Readonly<Record<string, unknown>>
readonly cursor: number
readonly records: readonly EventRecord[]
readonly hasMore: boolean
readonly projections: Readonly<Record<string, unknown>>
readonly assistantStream?: Readonly<Record<string, unknown>>
}
interface ControlBaseline {
readonly type: 'baseline'
readonly value: {
readonly queues: Readonly<Record<string, readonly unknown[]>>
readonly approvals: readonly unknown[]
readonly questions: readonly unknown[]
readonly projections: Readonly<Record<string, {
readonly asOfSeq: number
readonly values: Readonly<Record<string, unknown>>
}>>
}
}
interface CapturedFixture {
readonly sessionList: { readonly ok: true; readonly value: { readonly items: readonly SessionSummary[] } }
readonly settingsDescribe: {
readonly ok: true
readonly value: {
readonly writable: boolean
readonly hasDocument: boolean
readonly namespaces: readonly unknown[]
}
}
readonly credentialsDescribe: unknown
readonly modelCatalog: unknown
readonly agentPresets: unknown
readonly commands: unknown
readonly workspace: {
readonly type: 'baseline'
readonly value: {
readonly items: readonly WorkspaceView[]
readonly archivedSessionIds: readonly string[]
readonly pinnedSessionIds: readonly string[]
}
}
readonly control: ControlBaseline
readonly remoteEvents: readonly unknown[]
readonly follow: FollowSnapshot
readonly attachment: unknown
}
export interface AssembledRemoteOptions {
/** Override the schema-resolved Host preference for developer-tool scenarios. */
readonly developerTools?: boolean
/** Return the fixture's image-dimension admission error from Session prompt. */
readonly rejectPrompt?: boolean
}
export interface AssembledRemote {
readonly mock: RemoteMock
/** Page-owned upload carrier: stages an image upload as the Host would and answers its receipt. */
readonly fileUpload: (input: string | URL, init: RequestInit) => Promise<Response>
}
const fixtureSource = readFileSync(
join(process.cwd(), 'apps/web/tests/fixtures/assembled-remote.fixture.json'),
'utf8',
)
/** Create one isolated RemoteMock world for an assembled built-client case. */
export function createAssembledRemote(options: AssembledRemoteOptions = {}): AssembledRemote {
const fixture = JSON.parse(fixtureSource) as CapturedFixture
const sessions = [...structuredClone(fixture.sessionList.value.items)]
const workspaces = [...structuredClone(fixture.workspace.value.items)]
const records = new Map<string, EventRecord[]>([[
'fx-alpha',
structuredClone(fixture.follow.records) as EventRecord[],
]])
const nextTurns = new Map([...records].map(([sessionId, sessionRecords]) => {
let next = 0
for (const { event } of sessionRecords) {
if (event.type !== 'turn/start' || !isRecord(event.data)) continue
const turn = event.data['turn']
if (typeof turn === 'number') next = Math.max(next, turn + 1)
}
return [sessionId, next] as const
}))
const attachments = new Map<string, unknown>([['fixture:image', structuredClone(fixture.attachment)]])
const stagedImages = new Map<string, Record<string, unknown>>()
const blankSessionProjections = fixture.control.value.projections['fx-gamma']
if (blankSessionProjections === undefined) {
throw new Error('assembled fixture: blank Session projections missing')
}
let nextSession = 1
const mock = RemoteMock.create().load(remoteDefaultResponses)
mock.load({
unary: {
'settings/describe': ok({
...fixture.settingsDescribe.value,
namespaces: [...fixture.settingsDescribe.value.namespaces, {
ns: 'ui-settings',
schema: { type: 'object', dict: { enabled: { type: 'boolean' } } },
value: { enabled: options.developerTools ?? true }, autoGenerate: false, applies: 'live', secrets: [], revision: 0,
}],
}),
'credentials/describe': structuredClone(fixture.credentialsDescribe),
'session/modelCatalog': structuredClone(fixture.modelCatalog),
'agentPresets/list': structuredClone(fixture.agentPresets),
'commands/list': structuredClone(fixture.commands),
'settings/openSettingsDocument': ok({ opened: true }),
'subagents/list': ok({ entries: [], parentAvailable: true }),
'terminal/list': ok([]),
'skills/list': ok({ skills: [] }),
'session/canOpenWorkspacePath': ok(true),
'session/openWorkspacePath': ok({ opened: true }),
'session/updateQueue': {
ok: false,
error: {
code: 'session/queue-item-not-found',
message: 'assembled fixture has no pending queue item',
details: {},
},
},
'session/cancel': ok({ accepted: true }),
},
})
mock.stream('$events', (_args, stream) => {
for (const frame of fixture.remoteEvents) stream.push(structuredClone(frame))
})
mock.stream('session/control', (_args, stream) => {
stream.push(structuredClone(fixture.control))
})
mock.stream('job/list', (_args, stream) => {
stream.push({ type: 'rows', jobs: [] })
})
mock.stream('workspace/follow', (_args, stream) => {
stream.push({
type: 'baseline',
value: {
items: structuredClone(workspaces),
archivedSessionIds: structuredClone(fixture.workspace.value.archivedSessionIds),
pinnedSessionIds: structuredClone(fixture.workspace.value.pinnedSessionIds),
},
})
})
mock.stream('session/follow', ([args], stream) => {
const request = recordValue(args, 'request')
const sessionId = followedSessionId(request)
if (sessionId === 'fx-alpha') {
stream.push(structuredClone(fixture.follow))
return
}
const summary = sessions.find(candidate => candidate.sessionId === sessionId)
if (summary === undefined) throw new Error(`assembled fixture: no Session ${sessionId}`)
const sessionRecords = records.get(sessionId) ?? []
stream.push({
type: 'snapshot',
header: {
version: 3,
id: sessionId,
createdAt: summary.updatedAt,
cwd: summary.cwd,
isSeeded: false,
},
cursor: sessionRecords.at(-1)?.event.seq ?? -1,
records: structuredClone(sessionRecords),
hasMore: false,
projections: structuredClone(blankSessionProjections),
...isAssistantStreamRequested(request) ? { assistantStream: { revision: 0 } } : {},
})
})
mock.unary('$events/result', (result: unknown) => {
const eventId = recordString(result, 'eventId')
mock.streams.push('$events', { type: 'cancel', eventId })
return ok(undefined)
})
mock.unary('session/list', () => ok({ items: structuredClone(sessions) }))
// The shipped composition mounts the `ui-schedule` row, so the Schedule
// client reads the catalog and the per-Session list over this mock.
mock.unary('schedule/catalog', () => ok([]))
mock.unary('schedule/list', () => ok([]))
mock.unary('session/projections', (request: unknown) => {
const sessionId = recordString(recordValue(request, 'request'), 'sessionId')
const summary = sessions.find(candidate => candidate.sessionId === sessionId)
return ok(structuredClone(summary?.projections ?? fixture.control.value.projections[sessionId] ?? null))
})
mock.unary('workspace/create', (request: unknown) => {
const path = recordString(recordValue(request, 'request'), 'path')
const existing = workspaces.find(workspace => workspace.path === path)
if (existing !== undefined) return ok({ workspace: structuredClone(existing), created: false })
const now = new Date().toISOString()
const workspace = {
workspaceId: `fx-ws-${String(workspaces.length + 1)}`,
path,
title: path.split('/').filter(Boolean).at(-1) ?? path,
sessionIds: [],
createdAt: now,
updatedAt: now,
}
workspaces.unshift(workspace)
mock.streams.push('workspace/follow', { type: 'upsert', workspace: structuredClone(workspace) })
return ok({ workspace: structuredClone(workspace), created: true })
})
mock.unary('session/create', (request: unknown) => {
request = recordValue(request, 'request')
const requestedId = optionalRecordString(request, 'sessionId')
const sessionId = requestedId ?? `fx-${String(nextSession++)}`
const existing = sessions.find(candidate => candidate.sessionId === sessionId)
if (existing !== undefined) return ok({ sessionId })
const workspaceId = optionalRecordString(request, 'workspaceId')
const workspace = workspaces.find(candidate => candidate.workspaceId === workspaceId)
const cwd = workspace?.path ?? optionalRecordString(request, 'cwd') ?? '/tmp/fixture'
const summary: SessionSummary = {
sessionId,
updatedAt: Date.now(),
running: false,
blank: true,
cwd,
// The created Session is live on this fixture Host: its list block is
// sequenced, like the block the real live registry would serve.
projections: { kind: 'sequenced', ...structuredClone(blankSessionProjections) },
}
sessions.push(summary)
records.set(sessionId, [])
nextTurns.set(sessionId, 0)
if (workspace !== undefined && !workspace.sessionIds.includes(sessionId)) {
workspace.sessionIds = [sessionId, ...workspace.sessionIds]
workspace.updatedAt = new Date().toISOString()
mock.streams.push('workspace/follow', {
type: 'upsert',
workspace: structuredClone(workspace),
})
}
mock.streams.push('$events', { type: 'emit', event: 'api-session/added', args: [structuredClone(summary)] })
return ok({ sessionId })
})
mock.unary('session/attachment', (request: unknown) => {
request = recordValue(request, 'request')
const attachmentId = recordString(request, 'attachmentId')
return attachments.get(attachmentId) ?? {
ok: false,
error: {
code: 'session/attachment-invalid',
message: `assembled fixture attachment ${attachmentId} is missing`,
details: { reason: 'ATTACHMENT_NOT_FOUND' },
},
}
})
mock.unary('session/prompt', (request: unknown) => {
request = recordValue(request, 'request')
if (options.rejectPrompt === true) {
return {
ok: false,
error: {
code: 'session/attachment-invalid',
message: 'assembled fixture: image side exceeds the deployment limit',
details: { reason: 'IMAGE_DIMENSION_TOO_LARGE' },
},
}
}
const sessionId = recordString(request, 'sessionId')
const requestId = recordString(request, 'requestId')
const sessionRecords = records.get(sessionId) ?? []
const summary = sessions.find(candidate => candidate.sessionId === sessionId)
if (summary === undefined) throw new Error(`assembled fixture: no Session ${sessionId}`)
const content = recordArray(request, 'content').map((part) => {
if (isRecord(part) && part.type === 'file') {
const image = stagedImages.get(recordString(part, 'receiptId'))
return image === undefined ? part : { type: 'image', attachment: image }
}
if (!isRecord(part) || part.type !== 'image') return part
const attachmentId = `assembled:${randomUUID()}`
const data = recordString(part, 'data')
const attachment = {
attachmentId,
mediaType: recordString(part, 'mediaType'),
bytes: Math.max(1, Math.floor(data.length * 3 / 4)),
width: 160,
height: 90,
...optionalRecordString(part, 'name') === undefined
? {}
: { name: optionalRecordString(part, 'name') },
}
attachments.set(attachmentId, ok({ attachment, data }))
return { type: 'image', attachment }
})
const turn = nextTurns.get(sessionId) ?? 0
nextTurns.set(sessionId, turn + 1)
summary.updatedAt = Date.now()
summary.blank = false
if (!summary.running) {
summary.running = true
mock.streams.push('$events', { type: 'emit', event: 'api-session/status', args: [sessionId, true] })
}
const turnEvent = eventOf(sessionRecords.length, 'turn/start', { turn })
const userEvent = eventOf(sessionRecords.length + 1, 'user/message', {
content,
source: { kind: 'user', rpcId: requestId },
role: 'user',
id: randomUUID(),
}, 'append')
sessionRecords.push(turnEvent, userEvent)
records.set(sessionId, sessionRecords)
mock.streams.push('session/follow', turnEvent, follows(sessionId))
mock.streams.push('session/follow', userEvent, follows(sessionId))
return ok({ accepted: true })
})
mock.unary('commands/execute', (args: unknown) => {
const line = optionalRecordString(args, 'line') ?? ''
const name = /^\/(\S+)/u.exec(line.trim())?.[1]
if (name === undefined || !['compact', 'echo', 'goal', 'permission', 'plan'].includes(name)) {
return ok(undefined)
}
return ok({
commandId: `assembled-command-${randomUUID()}`,
result: {
kind: 'success',
...name === 'echo' ? { text: line.replace(/^\/echo\s*/u, '') } : {},
},
})
})
const fileUpload = async (input: string | URL, init: RequestInit): Promise<Response> => {
const url = new URL(String(input), 'http://assembled.test/')
const mediaType = url.searchParams.get('mediaType')
if (!url.pathname.endsWith('/api/session/uploadFileBinary') || mediaType === null || !(init.body instanceof Blob)) {
throw new Error(`assembled fixture: unexpected upload ${url.pathname}${url.search}`)
}
const bytes = new Uint8Array(await init.body.arrayBuffer())
const name = url.searchParams.get('name')
const image = {
attachmentId: `assembled:${randomUUID()}`,
mediaType,
bytes: bytes.byteLength,
width: 160,
height: 90,
...name === null ? {} : { name },
}
attachments.set(image.attachmentId, ok({ attachment: image, data: Buffer.from(bytes).toString('base64') }))
const receiptId = `assembled-receipt:${randomUUID()}`
stagedImages.set(receiptId, image)
return new Response(JSON.stringify({ ok: true, value: { receiptId, image } }), { status: 200 })
}
return { mock, fileUpload }
}
function eventOf(
seq: number,
type: string,
data: unknown,
surfaceOp?: string,
): EventRecord {
return {
type: 'event',
event: {
seq,
time: Date.now(),
type,
data,
...(surfaceOp === undefined ? {} : { surfaceOp }),
},
}
}
function follows(sessionId: string): (args: readonly unknown[]) => boolean {
return ([args]) => followedSessionId(recordValue(args, 'request')) === sessionId
}
function followedSessionId(value: unknown): string {
if (!isRecord(value) || !isRecord(value.address)) throw new TypeError('assembled fixture follow request is invalid')
return value.address.kind === 'session'
? recordString(value.address, 'sessionId')
: recordString(value.address, 'childSessionId')
}
function isAssistantStreamRequested(value: unknown): boolean {
return isRecord(value) && value.assistantStream === true
}
function recordString(value: unknown, key: string): string {
const selected = isRecord(value) ? value[key] : undefined
if (typeof selected !== 'string') throw new TypeError(`assembled fixture ${key} must be a string`)
return selected
}
function optionalRecordString(value: unknown, key: string): string | undefined {
const selected = isRecord(value) ? value[key] : undefined
if (selected === undefined) return undefined
if (typeof selected !== 'string') throw new TypeError(`assembled fixture ${key} must be a string`)
return selected
}
function recordArray(value: unknown, key: string): readonly unknown[] {
const selected = isRecord(value) ? value[key] : undefined
if (!Array.isArray(selected)) throw new TypeError(`assembled fixture ${key} must be an array`)
return selected
}
function recordValue(value: unknown, key: string): unknown {
if (!isRecord(value) || !(key in value)) throw new TypeError(`assembled fixture ${key} is missing`)
return value[key]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}