|
@@ -1,4 +1,4 @@
|
|
|
-import { mkdtempSync } from 'node:fs'
|
|
|
|
|
|
|
+import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
|
import { tmpdir } from 'node:os'
|
|
import { tmpdir } from 'node:os'
|
|
|
import { join } from 'node:path'
|
|
import { join } from 'node:path'
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
@@ -15,11 +15,14 @@ import { bootHost, startHost, type HostHandle, type RunningHost } from '../src/i
|
|
|
|
|
|
|
|
/** Scripted adapter: each model call consumes the next chunk list; 'hang' streams then waits for abort. */
|
|
/** Scripted adapter: each model call consumes the next chunk list; 'hang' streams then waits for abort. */
|
|
|
class ScriptedAdapter extends LlmAdapter {
|
|
class ScriptedAdapter extends LlmAdapter {
|
|
|
|
|
+ readonly requests: GenerateOptions[] = []
|
|
|
|
|
+
|
|
|
constructor(private script: (StreamChunk[] | 'hang')[]) {
|
|
constructor(private script: (StreamChunk[] | 'hang')[]) {
|
|
|
super()
|
|
super()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
|
|
|
+ this.requests.push(options)
|
|
|
const entry = this.script.shift()
|
|
const entry = this.script.shift()
|
|
|
if (!entry) throw new Error('ScriptedAdapter: script exhausted')
|
|
if (!entry) throw new Error('ScriptedAdapter: script exhausted')
|
|
|
if (entry === 'hang') {
|
|
if (entry === 'hang') {
|
|
@@ -79,7 +82,12 @@ afterEach(async () => {
|
|
|
|
|
|
|
|
async function boot(script: (StreamChunk[] | 'hang')[] = []): Promise<RunningHost> {
|
|
async function boot(script: (StreamChunk[] | 'hang')[] = []): Promise<RunningHost> {
|
|
|
host = await startHost({
|
|
host = await startHost({
|
|
|
- boot: { persistenceRoot: mkdtempSync(join(tmpdir(), 'dsh-host-runtime-')), provider: 'scripted', model: 'test-model' },
|
|
|
|
|
|
|
+ boot: {
|
|
|
|
|
+ persistenceRoot: mkdtempSync(join(tmpdir(), 'dsh-host-runtime-')),
|
|
|
|
|
+ workspaceContext: false,
|
|
|
|
|
+ provider: 'scripted',
|
|
|
|
|
+ model: 'test-model',
|
|
|
|
|
+ },
|
|
|
})
|
|
})
|
|
|
host.ctx.llm.registerAdapter(['scripted'], new ScriptedAdapter(script))
|
|
host.ctx.llm.registerAdapter(['scripted'], new ScriptedAdapter(script))
|
|
|
return host
|
|
return host
|
|
@@ -87,7 +95,10 @@ async function boot(script: (StreamChunk[] | 'hang')[] = []): Promise<RunningHos
|
|
|
|
|
|
|
|
describe('bootHost / startHost', () => {
|
|
describe('bootHost / startHost', () => {
|
|
|
it('falls back to the deepseek defaults and disposes idempotently', async () => {
|
|
it('falls back to the deepseek defaults and disposes idempotently', async () => {
|
|
|
- const handle: HostHandle = await bootHost({ persistenceRoot: mkdtempSync(join(tmpdir(), 'dsh-boot-')) })
|
|
|
|
|
|
|
+ const handle: HostHandle = await bootHost({
|
|
|
|
|
+ persistenceRoot: mkdtempSync(join(tmpdir(), 'dsh-boot-')),
|
|
|
|
|
+ workspaceContext: false,
|
|
|
|
|
+ })
|
|
|
expect(handle.defaults).toMatchObject({ provider: 'deepseek', model: 'deepseek-v4-flash' })
|
|
expect(handle.defaults).toMatchObject({ provider: 'deepseek', model: 'deepseek-v4-flash' })
|
|
|
expect(typeof handle.defaults.cwd).toBe('string')
|
|
expect(typeof handle.defaults.cwd).toBe('string')
|
|
|
await handle.dispose()
|
|
await handle.dispose()
|
|
@@ -105,6 +116,41 @@ describe('bootHost / startHost', () => {
|
|
|
await first
|
|
await first
|
|
|
host = undefined
|
|
host = undefined
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ it('routes workspace instructions through the assembled agent request prefix', async () => {
|
|
|
|
|
+ const workspace = mkdtempSync(join(tmpdir(), 'dsh-host-workspace-'))
|
|
|
|
|
+ mkdirSync(join(workspace, '.git'))
|
|
|
|
|
+ writeFileSync(join(workspace, 'AGENTS.md'), 'host-workspace-context-probe\n')
|
|
|
|
|
+ const adapter = new ScriptedAdapter([textResponse('done')])
|
|
|
|
|
+ host = await startHost({
|
|
|
|
|
+ boot: {
|
|
|
|
|
+ persistenceRoot: mkdtempSync(join(tmpdir(), 'dsh-host-workspace-sessions-')),
|
|
|
|
|
+ workspaceContext: { dshHome: join(workspace, '.dsh'), maxBytes: 65_536 },
|
|
|
|
|
+ provider: 'scripted',
|
|
|
|
|
+ model: 'test-model',
|
|
|
|
|
+ cwd: workspace,
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ host.ctx.llm.registerAdapter(['scripted'], adapter)
|
|
|
|
|
+ const { sessionId } = expectOk(await host.api.sessions.create(request({})))
|
|
|
|
|
+ const agent = host.ctx.agents.get(sessionId) as Agent
|
|
|
|
|
+ const idle = waitForIdle(host.ctx, agent)
|
|
|
|
|
+
|
|
|
|
|
+ expectOk(await host.api.sessions.prompt(request({
|
|
|
|
|
+ sessionId,
|
|
|
|
|
+ mode: 'queue' as const,
|
|
|
|
|
+ content: [{ type: 'text' as const, text: 'go' }],
|
|
|
|
|
+ })))
|
|
|
|
|
+ await idle
|
|
|
|
|
+
|
|
|
|
|
+ const requestText = adapter.requests[0]?.messages
|
|
|
|
|
+ .flatMap(message => message.content)
|
|
|
|
|
+ .filter(block => block.type === 'text')
|
|
|
|
|
+ .map(block => block.text)
|
|
|
|
|
+ .join('\n') ?? ''
|
|
|
|
|
+ expect(requestText).toContain('Instructions from: AGENTS.md')
|
|
|
|
|
+ expect(requestText).toContain('host-workspace-context-probe')
|
|
|
|
|
+ })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('host.describe', () => {
|
|
describe('host.describe', () => {
|
|
@@ -196,7 +242,9 @@ describe('sessions.prompt / cancel', () => {
|
|
|
describe('sessions.history', () => {
|
|
describe('sessions.history', () => {
|
|
|
it('implicitly resumes a cold session, deduplicating concurrent calls to one attach', async () => {
|
|
it('implicitly resumes a cold session, deduplicating concurrent calls to one attach', async () => {
|
|
|
const persistenceRoot = mkdtempSync(join(tmpdir(), 'dsh-host-resume-'))
|
|
const persistenceRoot = mkdtempSync(join(tmpdir(), 'dsh-host-resume-'))
|
|
|
- const first = await startHost({ boot: { persistenceRoot, provider: 'scripted', model: 'test-model' } })
|
|
|
|
|
|
|
+ const first = await startHost({
|
|
|
|
|
+ boot: { persistenceRoot, workspaceContext: false, provider: 'scripted', model: 'test-model' },
|
|
|
|
|
+ })
|
|
|
first.ctx.llm.registerAdapter(['scripted'], new ScriptedAdapter([textResponse('persisted')]))
|
|
first.ctx.llm.registerAdapter(['scripted'], new ScriptedAdapter([textResponse('persisted')]))
|
|
|
const { sessionId } = expectOk(await first.api.sessions.create(request({})))
|
|
const { sessionId } = expectOk(await first.api.sessions.create(request({})))
|
|
|
const agent = first.ctx.agents.get(sessionId) as Agent
|
|
const agent = first.ctx.agents.get(sessionId) as Agent
|
|
@@ -205,7 +253,9 @@ describe('sessions.history', () => {
|
|
|
await idle
|
|
await idle
|
|
|
await first.dispose()
|
|
await first.dispose()
|
|
|
|
|
|
|
|
- host = await startHost({ boot: { persistenceRoot, provider: 'scripted', model: 'test-model' } })
|
|
|
|
|
|
|
+ host = await startHost({
|
|
|
|
|
+ boot: { persistenceRoot, workspaceContext: false, provider: 'scripted', model: 'test-model' },
|
|
|
|
|
+ })
|
|
|
host.ctx.llm.registerAdapter(['scripted'], new ScriptedAdapter([]))
|
|
host.ctx.llm.registerAdapter(['scripted'], new ScriptedAdapter([]))
|
|
|
expect(host.ctx.agents.get(sessionId)).toBeUndefined()
|
|
expect(host.ctx.agents.get(sessionId)).toBeUndefined()
|
|
|
const [a, b] = await Promise.all([
|
|
const [a, b] = await Promise.all([
|