fix(session-query): rename session log export package
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { SessionLogDownloadHeaderAction } from '../src/client/HeaderAction.tsx'
|
||||
import { apply, inject } from '../src/client/index.ts'
|
||||
|
||||
const SID = 'session-export-apply' as SessionId
|
||||
|
||||
afterEach(() => { vi.unstubAllGlobals() })
|
||||
|
||||
function declare(slots: SlotRegistry): () => void {
|
||||
return slots.register({
|
||||
name: 'root',
|
||||
children: {
|
||||
'conversation.session.header.actions': { kind: 'list', scope: 'session' },
|
||||
'conversation.session.header.utilities': { kind: 'list', scope: 'session' },
|
||||
},
|
||||
} as never, () => null)
|
||||
}
|
||||
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SlotRegistry).await()
|
||||
const slots = ctx.get('slots') as SlotRegistry
|
||||
const declaration = declare(slots)
|
||||
ctx.provide('locale', new LocaleRuntime(ctx))
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
return { ctx, slots, declaration, fiber }
|
||||
}
|
||||
|
||||
describe('session-log-download browser plugin', () => {
|
||||
it('provides one controller and removes its Header contribution on disposal', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response('', { status: 500 })))
|
||||
const b = await bench()
|
||||
expect(inject).toEqual(['slots', 'locale'])
|
||||
expect(b.ctx.sessionLogDownload).toBeDefined()
|
||||
expect(b.slots.entries('conversation.session.header.actions')).toHaveLength(0)
|
||||
const entry = b.slots.entries('conversation.session.header.utilities')[0]
|
||||
expect(entry?.component).toBe(SessionLogDownloadHeaderAction)
|
||||
expect(entry?.options).toMatchObject({ id: 'session-log-download' })
|
||||
const injected = (entry?.inject as unknown as () => import('../src/client/Dialog.tsx').SessionLogDownloadDialogInjected)()
|
||||
await injected.request(SID)
|
||||
expect(b.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]?.status).toBe('error')
|
||||
injected.dismiss(SID)
|
||||
expect(b.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]?.open).toBe(false)
|
||||
|
||||
await b.fiber.dispose()
|
||||
expect(b.slots.entries('conversation.session.header.utilities')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('downloads only for an export execution acknowledged by this browser client', async () => {
|
||||
const fetcher = vi.fn(async () => new Response('', { status: 500 }))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const first = await bench()
|
||||
const second = await bench()
|
||||
|
||||
first.ctx.emit('command/executed', SID, 'plan', { kind: 'success' })
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
first.ctx.emit('command/executed', SID, 'export', { kind: 'error', text: 'bad path' })
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
first.ctx.emit('command/executed', SID, 'export', { kind: 'success' })
|
||||
await vi.waitFor(() => {
|
||||
expect(fetcher).toHaveBeenCalledOnce()
|
||||
expect(first.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]?.status).toBe('error')
|
||||
})
|
||||
expect(second.ctx.sessionLogDownload.store.getSnapshot().bySession[SID]).toBeUndefined()
|
||||
|
||||
await first.fiber.dispose()
|
||||
await second.fiber.dispose()
|
||||
})
|
||||
|
||||
it('re-registers after the declaring Header slot collapses and returns', async () => {
|
||||
const b = await bench()
|
||||
b.declaration()
|
||||
expect(b.slots.entries('conversation.session.header.utilities')).toHaveLength(0)
|
||||
const redeclare = declare(b.slots)
|
||||
await Promise.resolve()
|
||||
expect(b.slots.entries('conversation.session.header.utilities')[0]?.component).toBe(SessionLogDownloadHeaderAction)
|
||||
redeclare()
|
||||
await b.fiber.dispose()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { CommandDefinition, CommandInvocation } from '@deepseek-ai/dsh-commands'
|
||||
import * as SessionLogDownload from '../src/index.ts'
|
||||
|
||||
describe('/export Web download command', () => {
|
||||
it('registers one pathless command and removes it with the plugin fiber', async () => {
|
||||
let descriptor: CommandDefinition | undefined
|
||||
const ctx = new Context()
|
||||
ctx.provide('commands', {
|
||||
register(next: CommandDefinition) {
|
||||
descriptor = next
|
||||
return () => { descriptor = undefined }
|
||||
},
|
||||
} as never)
|
||||
const fiber = await ctx.plugin(SessionLogDownload)
|
||||
|
||||
expect(descriptor).toMatchObject({
|
||||
name: 'export',
|
||||
description: 'Download this Session log as a ZIP archive',
|
||||
})
|
||||
const invoke = (rawInput: string) => descriptor?.handler({ rawInput } as CommandInvocation)
|
||||
await expect(invoke('')).resolves.toEqual({
|
||||
kind: 'success', text: 'Session log download requested.',
|
||||
})
|
||||
await expect(invoke(' output.zip')).resolves.toEqual({
|
||||
kind: 'error', text: 'The Web /export command does not accept a path.',
|
||||
})
|
||||
|
||||
await fiber.dispose()
|
||||
expect(descriptor).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
downloadUrl, SessionLogDownloadController, sessionLogZipFilename,
|
||||
} from '../src/client/controller.ts'
|
||||
|
||||
const SID = 'session-export-controller' as SessionId
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('SessionLogDownloadController', () => {
|
||||
it('downloads the host ZIP and publishes one shared success state', async () => {
|
||||
const fetcher = vi.fn(async () => new Response('zip', { status: 200 }))
|
||||
const save = vi.fn()
|
||||
const controller = new SessionLogDownloadController(fetcher, save)
|
||||
|
||||
await controller.download(SID)
|
||||
|
||||
expect(fetcher).toHaveBeenCalledOnce()
|
||||
const [url, init] = fetcher.mock.calls[0] as unknown as [URL, RequestInit]
|
||||
expect(url.pathname).toBe('/api/session.export')
|
||||
expect(url.searchParams.get('sessionId')).toBe(SID)
|
||||
expect(url.searchParams.get('includeDescendants')).toBe('true')
|
||||
expect(init.method).toBe('HEAD')
|
||||
expect(init.signal).toBeInstanceOf(AbortSignal)
|
||||
expect(save).toHaveBeenCalledWith(
|
||||
url.toString(),
|
||||
'dsh-session-session-export-controller.zip',
|
||||
)
|
||||
expect(controller.store.getSnapshot().bySession[SID]).toEqual({
|
||||
open: true, status: 'success', error: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses concurrent gestures and preserves a dismissed dialog', async () => {
|
||||
const response = Promise.withResolvers<Response>()
|
||||
const fetcher = vi.fn(() => response.promise)
|
||||
const controller = new SessionLogDownloadController(fetcher, vi.fn())
|
||||
|
||||
const first = controller.download(SID)
|
||||
const second = controller.download(SID)
|
||||
expect(first).toBe(second)
|
||||
controller.dismiss(SID)
|
||||
response.resolve(new Response('zip', { status: 200 }))
|
||||
await first
|
||||
|
||||
expect(fetcher).toHaveBeenCalledOnce()
|
||||
expect(controller.store.getSnapshot().bySession[SID]?.open).toBe(false)
|
||||
controller.dismiss(SID)
|
||||
})
|
||||
|
||||
it('publishes HTTP and transport failures without leaking rejections', async () => {
|
||||
const http = new SessionLogDownloadController(
|
||||
async () => new Response('backend unavailable', { status: 500 }), vi.fn(),
|
||||
)
|
||||
await http.download(SID)
|
||||
expect(http.store.getSnapshot().bySession[SID]).toEqual({
|
||||
open: true,
|
||||
status: 'error',
|
||||
error: 'Export failed: HTTP 500 backend unavailable',
|
||||
})
|
||||
|
||||
const transport = new SessionLogDownloadController(async () => { throw 'offline' }, vi.fn())
|
||||
await transport.download(SID)
|
||||
expect(transport.store.getSnapshot().bySession[SID]?.error).toBe('offline')
|
||||
|
||||
transport.dismiss('absent' as SessionId)
|
||||
|
||||
const emptyDetail = new SessionLogDownloadController(
|
||||
async () => ({
|
||||
ok: false, status: 503, text: async () => { throw new Error('body unavailable') },
|
||||
}) as unknown as Response,
|
||||
vi.fn(),
|
||||
)
|
||||
await emptyDetail.download(SID)
|
||||
expect(emptyDetail.store.getSnapshot().bySession[SID]?.error).toBe('Export failed: HTTP 503')
|
||||
})
|
||||
|
||||
it('aborts active fetches on disposal and rejects later requests', async () => {
|
||||
let signal: AbortSignal | undefined
|
||||
const fetcher = vi.fn((_input: string | URL, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
|
||||
signal = init?.signal ?? undefined
|
||||
signal?.addEventListener('abort', () => {
|
||||
reject(signal?.reason instanceof Error ? signal.reason : new Error('aborted'))
|
||||
}, { once: true })
|
||||
}))
|
||||
const controller = new SessionLogDownloadController(fetcher, vi.fn())
|
||||
const pending = controller.download(SID)
|
||||
|
||||
await controller.dispose()
|
||||
|
||||
await expect(pending).resolves.toBeUndefined()
|
||||
expect(signal?.aborted).toBe(true)
|
||||
await expect(controller.download(SID)).resolves.toBeUndefined()
|
||||
await controller.dispose()
|
||||
})
|
||||
|
||||
it('uses the null-origin fallback and default browser operations', async () => {
|
||||
vi.stubGlobal('location', { origin: 'null' })
|
||||
const fetcher = vi.fn(async (_input: string | URL, _init?: RequestInit) => new Response('zip'))
|
||||
vi.stubGlobal('fetch', fetcher)
|
||||
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
||||
const controller = new SessionLogDownloadController()
|
||||
|
||||
await controller.download(SID)
|
||||
|
||||
expect((fetcher.mock.calls[0]?.[0] as URL).origin).toBe('http://dsh.internal')
|
||||
expect(fetcher.mock.calls[0]?.[1]).toMatchObject({ method: 'HEAD' })
|
||||
expect(click).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('defaults dialog openness when state is externally cleared before settlement', async () => {
|
||||
const success = Promise.withResolvers<Response>()
|
||||
const successful = new SessionLogDownloadController(() => success.promise, vi.fn())
|
||||
const successRun = successful.download(SID)
|
||||
successful.store.set({ bySession: {} })
|
||||
success.resolve(new Response('zip'))
|
||||
await successRun
|
||||
expect(successful.store.getSnapshot().bySession[SID]?.open).toBe(true)
|
||||
|
||||
const failure = Promise.withResolvers<Response>()
|
||||
const failing = new SessionLogDownloadController(() => failure.promise, vi.fn())
|
||||
const failureRun = failing.download(SID)
|
||||
failing.store.set({ bySession: {} })
|
||||
failure.reject(new Error('failed after clear'))
|
||||
await failureRun
|
||||
expect(failing.store.getSnapshot().bySession[SID]?.open).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('browser download helpers', () => {
|
||||
it('sanitizes the archive filename and hands the URL to a download anchor', () => {
|
||||
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
||||
|
||||
expect(sessionLogZipFilename('a/b' as SessionId)).toBe('dsh-session-a_b.zip')
|
||||
downloadUrl('http://host/api/session.export?sessionId=a', 'archive.zip')
|
||||
expect(click).toHaveBeenCalledOnce()
|
||||
const anchor = click.mock.instances[0] as HTMLAnchorElement
|
||||
expect(anchor.href).toBe('http://host/api/session.export?sessionId=a')
|
||||
expect(anchor.download).toBe('archive.zip')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SessionLogDownloadController } from '../src/client/controller.ts'
|
||||
import { SessionLogDownloadDialog } from '../src/client/Dialog.tsx'
|
||||
import type { SessionLogDownloadDialogProps } from '../src/client/Dialog.tsx'
|
||||
import { en } from '../src/client/locales.ts'
|
||||
|
||||
const SID = 'session-export-dialog' as SessionId
|
||||
|
||||
function bench(
|
||||
controller = new SessionLogDownloadController(
|
||||
async () => new Response('zip', { status: 200 }), vi.fn(),
|
||||
),
|
||||
) {
|
||||
const dismiss = vi.fn((sessionId: SessionId) => { controller.dismiss(sessionId) })
|
||||
function useSessionLogDownload<T>(selector: (state: ReturnType<typeof controller.store.getSnapshot>) => T): T {
|
||||
return useSyncExternalStore(
|
||||
listener => controller.store.subscribe(listener),
|
||||
() => selector(controller.store.getSnapshot()),
|
||||
)
|
||||
}
|
||||
const t = (key: keyof typeof en): string => en[key]
|
||||
const props = { sessionId: SID, useSessionLogDownload, dismiss, t } as unknown as SessionLogDownloadDialogProps
|
||||
const view = render(<SessionLogDownloadDialog {...props} />)
|
||||
return { controller, dismiss, view }
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('SessionLogDownloadDialog', () => {
|
||||
it('shows a controller failure and closes it without reading Session history', async () => {
|
||||
const b = bench()
|
||||
act(() => {
|
||||
b.controller.store.set({
|
||||
bySession: { [SID]: { open: true, status: 'error', error: 'toolbar failed' } },
|
||||
})
|
||||
})
|
||||
const dialog = await b.view.findByRole('dialog', { name: 'Session export failed' })
|
||||
expect(dialog.textContent).toContain('toolbar failed')
|
||||
const close = b.view.getAllByRole('button', { name: 'Close' })[0]
|
||||
if (close === undefined) throw new Error('Session export dialog has no close button')
|
||||
fireEvent.click(close)
|
||||
await waitFor(() => { expect(b.dismiss).toHaveBeenCalledWith(SID) })
|
||||
})
|
||||
|
||||
it('renders the in-flight state and the settled browser download state', async () => {
|
||||
let release!: (response: Response) => void
|
||||
const pending = new Promise<Response>((resolve) => { release = resolve })
|
||||
const controller = new SessionLogDownloadController(() => pending, vi.fn())
|
||||
const b = bench(controller)
|
||||
|
||||
const download = controller.download(SID)
|
||||
expect(await b.view.findByRole('dialog', { name: 'Exporting Session' })).toBeTruthy()
|
||||
release(new Response('zip', { status: 200 }))
|
||||
await download
|
||||
expect(await b.view.findByRole('dialog', { name: 'Session download started' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('uses fallback copy when a failure has no detail', async () => {
|
||||
const b = bench()
|
||||
act(() => {
|
||||
b.controller.store.set({
|
||||
bySession: { [SID]: { open: true, status: 'error', error: '' } },
|
||||
})
|
||||
})
|
||||
const dialog = await b.view.findByRole('dialog', { name: 'Session export failed' })
|
||||
expect(dialog.textContent).toContain('Could not start the Session export.')
|
||||
const close = b.view.getAllByRole('button', { name: 'Close' }).at(-1)
|
||||
if (close === undefined) throw new Error('Session export dialog has no footer action')
|
||||
fireEvent.click(close)
|
||||
await waitFor(() => { expect(b.dismiss).toHaveBeenCalledWith(SID) })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SessionLogDownloadController } from '../src/client/controller.ts'
|
||||
import { SessionLogDownloadHeaderAction } from '../src/client/HeaderAction.tsx'
|
||||
import type { SessionLogDownloadDialogProps } from '../src/client/Dialog.tsx'
|
||||
import { en } from '../src/client/locales.ts'
|
||||
|
||||
const SID = 'session-export-header' as SessionId
|
||||
|
||||
function bindSessionExport(controller: SessionLogDownloadController) {
|
||||
return function useSessionLogDownload<T>(selector: (state: ReturnType<typeof controller.store.getSnapshot>) => T): T {
|
||||
return useSyncExternalStore(
|
||||
listener => controller.store.subscribe(listener),
|
||||
() => selector(controller.store.getSnapshot()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function bench() {
|
||||
const controller = new SessionLogDownloadController(async () => new Response('zip'), vi.fn())
|
||||
const request = vi.fn((sessionId: SessionId) => controller.download(sessionId))
|
||||
const dismiss = vi.fn((sessionId: SessionId) => { controller.dismiss(sessionId) })
|
||||
const useSessionLogDownload = bindSessionExport(controller)
|
||||
const props = {
|
||||
sessionId: SID,
|
||||
useSessionLogDownload,
|
||||
request,
|
||||
dismiss,
|
||||
t: (key: keyof typeof en): string => en[key],
|
||||
} as unknown as SessionLogDownloadDialogProps
|
||||
const view = render(<SessionLogDownloadHeaderAction {...props} />)
|
||||
return { controller, request, view }
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('Session export Header action', () => {
|
||||
it('renders the 111×32 text capsule and downloads through the shared controller', async () => {
|
||||
const b = bench()
|
||||
const button = b.view.getByRole('button', { name: 'Session log' })
|
||||
expect(button.querySelector('svg')).not.toBeNull()
|
||||
fireEvent.click(button)
|
||||
await waitFor(() => { expect(b.request).toHaveBeenCalledWith(SID) })
|
||||
expect(await b.view.findByRole('dialog', { name: 'Session download started' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('disables the capsule while either entry path downloads this Session', async () => {
|
||||
const b = bench()
|
||||
let release!: (response: Response) => void
|
||||
const pending = new Promise<Response>((resolve) => { release = resolve })
|
||||
const controller = new SessionLogDownloadController(() => pending, vi.fn())
|
||||
const useSessionLogDownload = bindSessionExport(controller)
|
||||
b.view.rerender(<SessionLogDownloadHeaderAction {...({
|
||||
sessionId: SID,
|
||||
useSessionLogDownload,
|
||||
request: (sessionId: SessionId) => controller.download(sessionId),
|
||||
dismiss: (sessionId: SessionId) => { controller.dismiss(sessionId) },
|
||||
t: (key: keyof typeof en): string => en[key],
|
||||
} as unknown as SessionLogDownloadDialogProps)} />)
|
||||
|
||||
const download = controller.download(SID)
|
||||
const button = b.view.getByRole('button', { name: 'Session log' })
|
||||
await waitFor(() => { expect(button.getAttribute('aria-busy')).toBe('true') })
|
||||
expect((button as HTMLButtonElement).disabled).toBe(true)
|
||||
release(new Response('zip'))
|
||||
await download
|
||||
await waitFor(() => { expect(button.getAttribute('aria-busy')).toBe('false') })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { apply, inject, name } from '../src/invariant.ts'
|
||||
|
||||
describe('@deepseek-ai/dsh-session-log-export/invariant', () => {
|
||||
it('registers the package-owned empty companion', async () => {
|
||||
const register = vi.fn(() => vi.fn())
|
||||
const ctx = new Context()
|
||||
ctx.provide('invariants', { register })
|
||||
const dispose = await apply(ctx)
|
||||
expect(name).toBe('session-export-invariant')
|
||||
expect(inject).toEqual(['invariants'])
|
||||
expect(register).toHaveBeenCalledWith('@deepseek-ai/dsh-session-log-export', expect.any(Function))
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import Include from '@deepseek-ai/cordis-plugin-include'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import CommandRuntime from '@deepseek-ai/dsh-commands'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import * as SessionLogDownload from '@deepseek-ai/dsh-session-log-export'
|
||||
|
||||
let root: string | undefined
|
||||
let context: Context | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
await context?.fiber.dispose()
|
||||
context = undefined
|
||||
if (root !== undefined) await rm(root, { recursive: true, force: true })
|
||||
root = undefined
|
||||
})
|
||||
|
||||
describe('session-log-download real Loader composition', () => {
|
||||
it('discovers and executes /export through the assembled command plane', async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'dsh-session-export-loader-'))
|
||||
const configPath = join(root, 'cordis.yml')
|
||||
await writeFile(configPath, [
|
||||
"- name: '@deepseek-ai/dsh-session'",
|
||||
"- name: '@deepseek-ai/dsh-commands'",
|
||||
"- name: '@deepseek-ai/dsh-session-log-export'",
|
||||
'',
|
||||
].join('\n'))
|
||||
|
||||
context = new Context()
|
||||
context.baseUrl = pathToFileURL(root).href + '/'
|
||||
await context.plugin(Loader)
|
||||
context.loader.builtins.include = Include
|
||||
const modules = new Map<string, unknown>([
|
||||
['@deepseek-ai/dsh-session', SessionStore],
|
||||
['@deepseek-ai/dsh-commands', CommandRuntime],
|
||||
['@deepseek-ai/dsh-session-log-export', SessionLogDownload],
|
||||
])
|
||||
context.loader.internal = {
|
||||
version: 'v2',
|
||||
async import(specifier: string) {
|
||||
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
|
||||
return modules.get(specifier)
|
||||
},
|
||||
} as unknown as NonNullable<typeof context.loader.internal>
|
||||
await context.loader.create({
|
||||
name: 'cordis:include',
|
||||
config: { path: pathToFileURL(configPath).href },
|
||||
})
|
||||
await context.loader.await()
|
||||
|
||||
const session = (context.get('sessions') as unknown as SessionStore)
|
||||
.create(SessionId('loader-session-export'), { meta: { createdAt: 1 } })
|
||||
const agent = { session, status: 'idle', options: {} } as unknown as Agent
|
||||
expect(context.commands.list(agent)).toContainEqual({
|
||||
name: 'export', description: 'Download this Session log as a ZIP archive',
|
||||
})
|
||||
const execution = await context.commands.execute(agent, '/export', new AbortController().signal)
|
||||
expect(execution?.result).toEqual({ kind: 'success', text: 'Session log download requested.' })
|
||||
expect(session.events.map(event => event.type)).toEqual(['command/run', 'command/done'])
|
||||
expect(session.deriveMessages()).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user