|
|
@@ -1,28 +1,31 @@
|
|
|
/**
|
|
|
- * Web reference source coverage: Host-backed file/session discovery,
|
|
|
+ * Web reference source coverage: Remote-backed file/session discovery,
|
|
|
* deterministic ordering and labels, quoted-path suppression, pick projections, codec
|
|
|
* round-trip, and registration lifecycle.
|
|
|
*/
|
|
|
-import { Context } from '@deepseek-ai/cordis'
|
|
|
+import { Context, Service } from '@deepseek-ai/cordis'
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
-import type { FileReferenceItem, SessionReferenceItem } from '@deepseek-ai/dsh-api-remotes/client'
|
|
|
+import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
|
|
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
|
import type {
|
|
|
CandidateRequest, ClientSessionContext, InputTriggerCandidate, InputTriggerSource,
|
|
|
} from '@deepseek-ai/dsh-client-ui-input-trigger/client'
|
|
|
+import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
|
|
+import type { SessionReferenceMentionCandidate } from '@deepseek-ai/dsh-session-reference/types'
|
|
|
import { apply, inject } from '../src/client/index.ts'
|
|
|
|
|
|
const sid = (value: string): SessionId => value as SessionId
|
|
|
const session: ClientSessionContext = { sessionId: sid('target') }
|
|
|
|
|
|
-type ReferenceResponse<T> =
|
|
|
- | { result: { ok: true; value: { items: T[] } } }
|
|
|
- | { result: { ok: false; error: { code: string; message: string } } }
|
|
|
+type RemoteEnvelope<T> =
|
|
|
+ | { ok: true; value: T }
|
|
|
+ | { ok: false; error: { code: string; message: string; details: object } }
|
|
|
|
|
|
-type ReferenceLookup<T> = (
|
|
|
- payload: unknown,
|
|
|
+type RemoteLookup<T> = (
|
|
|
+ agentId: SessionId,
|
|
|
+ query: string,
|
|
|
signal?: AbortSignal,
|
|
|
-) => Promise<ReferenceResponse<T>>
|
|
|
+) => Promise<RemoteEnvelope<T[]>>
|
|
|
|
|
|
function request(
|
|
|
query: string,
|
|
|
@@ -37,30 +40,22 @@ function request(
|
|
|
}
|
|
|
|
|
|
async function bench(
|
|
|
- files: ReferenceLookup<FileReferenceItem> = vi.fn(() => Promise.resolve({
|
|
|
- result: {
|
|
|
- ok: true as const,
|
|
|
- value: {
|
|
|
- items: [
|
|
|
- { path: 'src', kind: 'directory' as const },
|
|
|
- { path: 'docs/a b.md', kind: 'file' as const },
|
|
|
- ],
|
|
|
- },
|
|
|
- },
|
|
|
+ files: RemoteLookup<FileReferenceCandidate> = vi.fn(() => Promise.resolve({
|
|
|
+ ok: true as const,
|
|
|
+ value: [
|
|
|
+ { path: 'src', kind: 'directory' as const },
|
|
|
+ { path: 'docs/a b.md', kind: 'file' as const },
|
|
|
+ ],
|
|
|
})),
|
|
|
- sessions: ReferenceLookup<SessionReferenceItem> = vi.fn(() => Promise.resolve({
|
|
|
- result: {
|
|
|
- ok: true as const,
|
|
|
- value: {
|
|
|
- items: [{
|
|
|
- sessionId: sid('source'),
|
|
|
- label: 'Research',
|
|
|
- cwd: '/project',
|
|
|
- createdAt: 1_700_000_000_000,
|
|
|
- mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
|
|
- }],
|
|
|
- },
|
|
|
- },
|
|
|
+ sessions: RemoteLookup<SessionReferenceMentionCandidate> = vi.fn(() => Promise.resolve({
|
|
|
+ ok: true as const,
|
|
|
+ value: [{
|
|
|
+ sessionId: sid('source'),
|
|
|
+ label: 'Research',
|
|
|
+ cwd: '/project',
|
|
|
+ createdAt: 1_700_000_000_000,
|
|
|
+ mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
|
|
+ }],
|
|
|
})),
|
|
|
): Promise<{ ctx: Context; fiber: ReturnType<Context['plugin']>; source: InputTriggerSource }> {
|
|
|
const ctx = new Context()
|
|
|
@@ -71,7 +66,15 @@ async function bench(
|
|
|
return () => { source = undefined }
|
|
|
},
|
|
|
})
|
|
|
- ctx.provide('connection', { api: { references: { files, sessions } } } as never)
|
|
|
+ class RemoteService extends Service {
|
|
|
+ constructor(serviceCtx: Context) {
|
|
|
+ super(serviceCtx, 'remote')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ new RemoteService(ctx)
|
|
|
+ ctx.provide('remote.fileReferences', { list: files })
|
|
|
+ ctx.provide('remote.sessionReferenceResolver', { candidates: sessions })
|
|
|
+ ctx.provide('locale', new LocaleRuntime(ctx))
|
|
|
const fiber = ctx.plugin({ inject: [...inject], apply })
|
|
|
await fiber.await()
|
|
|
if (source === undefined) throw new Error('reference source was not registered')
|
|
|
@@ -80,80 +83,74 @@ async function bench(
|
|
|
|
|
|
describe('apply', () => {
|
|
|
it('declares its services and releases the @ reference registration on disposal', async () => {
|
|
|
- expect(inject).toEqual(['inputTriggers', 'connection'])
|
|
|
- const ctx = new Context()
|
|
|
+ expect(inject).toEqual([
|
|
|
+ 'inputTriggers', 'locale', 'remote', 'remote.fileReferences', 'remote.sessionReferenceResolver',
|
|
|
+ ])
|
|
|
+ const { fiber } = await bench()
|
|
|
let registered: InputTriggerSource | undefined
|
|
|
+ const ctx = new Context()
|
|
|
ctx.provide('inputTriggers', {
|
|
|
registerSource(source: InputTriggerSource) {
|
|
|
registered = source
|
|
|
return () => { registered = undefined }
|
|
|
},
|
|
|
})
|
|
|
- ctx.provide('connection', {
|
|
|
- api: {
|
|
|
- references: {
|
|
|
- files: () => Promise.resolve({ result: { ok: true, value: { items: [] } } }),
|
|
|
- sessions: () => Promise.resolve({ result: { ok: true, value: { items: [] } } }),
|
|
|
- },
|
|
|
- },
|
|
|
- } as never)
|
|
|
- const fiber = ctx.plugin({ inject: [...inject], apply })
|
|
|
- await fiber.await()
|
|
|
+ class RemoteService extends Service {
|
|
|
+ constructor(serviceCtx: Context) {
|
|
|
+ super(serviceCtx, 'remote')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ new RemoteService(ctx)
|
|
|
+ ctx.provide('remote.fileReferences', { list: () => Promise.resolve({ ok: true, value: [] }) })
|
|
|
+ ctx.provide('remote.sessionReferenceResolver', { candidates: () => Promise.resolve({ ok: true, value: [] }) })
|
|
|
+ ctx.provide('locale', new LocaleRuntime(ctx))
|
|
|
+ const ownFiber = ctx.plugin({ inject: [...inject], apply })
|
|
|
+ await ownFiber.await()
|
|
|
expect(registered).toMatchObject({ trigger: '@', name: 'reference' })
|
|
|
- await fiber.dispose()
|
|
|
+ await ownFiber.dispose()
|
|
|
expect(registered).toBeUndefined()
|
|
|
+ await fiber.dispose()
|
|
|
})
|
|
|
})
|
|
|
|
|
|
describe('candidates', () => {
|
|
|
- it('starts both Host lookups together and renders files before sessions with stable labels', async () => {
|
|
|
+ it('starts both Remote lookups together and renders files before sessions with stable labels', async () => {
|
|
|
let releaseFiles!: () => void
|
|
|
let releaseSessions!: () => void
|
|
|
const files = vi.fn(() => new Promise<{
|
|
|
- result: { ok: true; value: { items: { path: string; kind: 'file' | 'directory' }[] } }
|
|
|
+ ok: true
|
|
|
+ value: { path: string; kind: 'file' | 'directory' }[]
|
|
|
}>((resolve) => {
|
|
|
releaseFiles = () => {
|
|
|
resolve({
|
|
|
- result: {
|
|
|
- ok: true,
|
|
|
- value: {
|
|
|
- items: [
|
|
|
- { path: 'src', kind: 'directory' },
|
|
|
- { path: 'docs/a b.md', kind: 'file' },
|
|
|
- ],
|
|
|
- },
|
|
|
- },
|
|
|
+ ok: true,
|
|
|
+ value: [
|
|
|
+ { path: 'src', kind: 'directory' },
|
|
|
+ { path: 'docs/a b.md', kind: 'file' },
|
|
|
+ ],
|
|
|
})
|
|
|
}
|
|
|
}))
|
|
|
const sessions = vi.fn(() => new Promise<{
|
|
|
- result: {
|
|
|
- ok: true
|
|
|
- value: {
|
|
|
- items: {
|
|
|
- sessionId: SessionId
|
|
|
- label: string
|
|
|
- cwd: string
|
|
|
- createdAt: number
|
|
|
- mention: string
|
|
|
- }[]
|
|
|
- }
|
|
|
- }
|
|
|
+ ok: true
|
|
|
+ value: {
|
|
|
+ sessionId: SessionId
|
|
|
+ label: string
|
|
|
+ cwd: string
|
|
|
+ createdAt: number
|
|
|
+ mention: string
|
|
|
+ }[]
|
|
|
}>((resolve) => {
|
|
|
releaseSessions = () => {
|
|
|
resolve({
|
|
|
- result: {
|
|
|
- ok: true,
|
|
|
- value: {
|
|
|
- items: [{
|
|
|
- sessionId: sid('source'),
|
|
|
- label: 'Research',
|
|
|
- cwd: '/project',
|
|
|
- createdAt: 1_700_000_000_000,
|
|
|
- mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
|
|
- }],
|
|
|
- },
|
|
|
- },
|
|
|
+ ok: true,
|
|
|
+ value: [{
|
|
|
+ sessionId: sid('source'),
|
|
|
+ label: 'Research',
|
|
|
+ cwd: '/project',
|
|
|
+ createdAt: 1_700_000_000_000,
|
|
|
+ mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
|
|
+ }],
|
|
|
})
|
|
|
}
|
|
|
}))
|
|
|
@@ -165,12 +162,12 @@ describe('candidates', () => {
|
|
|
releaseFiles()
|
|
|
await expect(pending).resolves.toEqual([
|
|
|
expect.objectContaining({
|
|
|
- name: 'Folder · src/',
|
|
|
+ name: '文件夹 · src/',
|
|
|
description: 'src',
|
|
|
section: '文件与文件夹',
|
|
|
}),
|
|
|
expect.objectContaining({
|
|
|
- name: 'File · a b.md',
|
|
|
+ name: '文件 · a b.md',
|
|
|
description: 'docs/a b.md',
|
|
|
section: '文件与文件夹',
|
|
|
}),
|
|
|
@@ -185,29 +182,23 @@ describe('candidates', () => {
|
|
|
it('suppresses sessions for an open quoted path and degrades each failed domain independently', async () => {
|
|
|
const files = vi.fn()
|
|
|
.mockResolvedValueOnce({
|
|
|
- result: {
|
|
|
- ok: true as const,
|
|
|
- value: { items: [{ path: 'README.md', kind: 'file' as const }] },
|
|
|
- },
|
|
|
+ ok: true as const,
|
|
|
+ value: [{ path: 'README.md', kind: 'file' as const }],
|
|
|
})
|
|
|
.mockRejectedValueOnce(new Error('file scan failed'))
|
|
|
const sessions = vi.fn(() => Promise.resolve({
|
|
|
- result: {
|
|
|
- ok: true as const,
|
|
|
- value: {
|
|
|
- items: [{
|
|
|
- sessionId: sid('source'),
|
|
|
- label: 'Research',
|
|
|
- cwd: '/project',
|
|
|
- createdAt: 0,
|
|
|
- mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
|
|
- }],
|
|
|
- },
|
|
|
- },
|
|
|
+ ok: true as const,
|
|
|
+ value: [{
|
|
|
+ sessionId: sid('source'),
|
|
|
+ label: 'Research',
|
|
|
+ cwd: '/project',
|
|
|
+ createdAt: 0,
|
|
|
+ mention: '@[Research](dsh-session:InNvdXJjZSI)',
|
|
|
+ }],
|
|
|
}))
|
|
|
const { source } = await bench(files, sessions)
|
|
|
const quoted = await source.candidates(session, request('READ', { quoted: true }))
|
|
|
- expect(quoted).toEqual([expect.objectContaining({ name: 'File · README.md' })])
|
|
|
+ expect(quoted).toEqual([expect.objectContaining({ name: '文件 · README.md' })])
|
|
|
expect(source.onPick({
|
|
|
candidate: quoted[0]!,
|
|
|
session,
|
|
|
@@ -229,55 +220,43 @@ describe('candidates', () => {
|
|
|
await expect(pending).resolves.toEqual([])
|
|
|
})
|
|
|
|
|
|
- it('treats Host errors as empty domains and filters paths that cannot be mentioned', async () => {
|
|
|
+ it('treats Remote failures as empty domains and filters paths that cannot be mentioned', async () => {
|
|
|
const files = vi.fn(() => Promise.resolve({
|
|
|
- result: {
|
|
|
- ok: true as const,
|
|
|
- value: { items: [{ path: 'bad\nname', kind: 'file' as const }] },
|
|
|
- },
|
|
|
+ ok: true as const,
|
|
|
+ value: [{ path: 'bad\nname', kind: 'file' as const }],
|
|
|
}))
|
|
|
const sessions = vi.fn()
|
|
|
.mockRejectedValueOnce(new Error('session lookup failed'))
|
|
|
.mockResolvedValueOnce({
|
|
|
- result: {
|
|
|
- ok: false as const,
|
|
|
- error: { code: 'reference-failed', message: 'session lookup failed' },
|
|
|
- },
|
|
|
+ ok: false as const,
|
|
|
+ error: { code: 'internal', message: 'session lookup failed', details: {} },
|
|
|
})
|
|
|
const { source } = await bench(files, sessions)
|
|
|
await expect(source.candidates(session, request('bad'))).resolves.toEqual([])
|
|
|
|
|
|
files.mockResolvedValueOnce({
|
|
|
- result: {
|
|
|
- ok: false as const,
|
|
|
- error: { code: 'reference-failed', message: 'file lookup failed' },
|
|
|
- },
|
|
|
+ ok: false as const,
|
|
|
+ error: { code: 'internal', message: 'file lookup failed', details: {} },
|
|
|
} as never)
|
|
|
await expect(source.candidates(session, request('bad'))).resolves.toEqual([])
|
|
|
})
|
|
|
|
|
|
it('omits redundant session ids and labels sessions without a cwd', async () => {
|
|
|
- const files = vi.fn(() => Promise.resolve({
|
|
|
- result: { ok: true as const, value: { items: [] } },
|
|
|
- }))
|
|
|
+ const files = vi.fn(() => Promise.resolve({ ok: true as const, value: [] }))
|
|
|
const sessions = vi.fn(() => Promise.resolve({
|
|
|
- result: {
|
|
|
- ok: true as const,
|
|
|
- value: {
|
|
|
- items: [{
|
|
|
- sessionId: sid('same'),
|
|
|
- label: 'same',
|
|
|
- createdAt: 0,
|
|
|
- mention: '@[same](dsh-session:InNhbWUi)',
|
|
|
- }],
|
|
|
- },
|
|
|
- },
|
|
|
+ ok: true as const,
|
|
|
+ value: [{
|
|
|
+ sessionId: sid('same'),
|
|
|
+ label: 'same',
|
|
|
+ createdAt: 0,
|
|
|
+ mention: '@[same](dsh-session:InNhbWUi)',
|
|
|
+ }],
|
|
|
}))
|
|
|
const { source } = await bench(files, sessions)
|
|
|
await expect(source.candidates(session, request('same'))).resolves.toEqual([
|
|
|
expect.objectContaining({
|
|
|
name: 'Session · same',
|
|
|
- description: '(no cwd) · 1970-01-01T00:00:00.000Z',
|
|
|
+ description: '(无工作目录) · 1970-01-01T00:00:00.000Z',
|
|
|
}),
|
|
|
])
|
|
|
})
|