|
|
@@ -1,12 +1,12 @@
|
|
|
import { chmod, mkdtemp, mkdir, rm, stat, symlink, utimes, writeFile } from 'node:fs/promises'
|
|
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
|
import { tmpdir } from 'node:os'
|
|
|
-import { describe, expect, it, vi } from 'vitest'
|
|
|
+import { afterAll, describe, expect, it, vi } from 'vitest'
|
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
|
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
|
|
import * as workspaceContext from '@deepseek-ai/dsh-agent-instructions'
|
|
|
import LlmRuntime, { createUserMessage, ToolCallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
|
|
-import SessionStore, { SessionId, type SessionEvent, type UserMessage } from '@deepseek-ai/dsh-session'
|
|
|
+import SessionStore, { SessionId, type SessionEvent, type SurfaceIntent, type UserMessage } from '@deepseek-ai/dsh-session'
|
|
|
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
|
|
import AgentLoop, { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
|
|
|
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
|
|
@@ -44,8 +44,8 @@ import { resolveConfig } from '../src/config.ts'
|
|
|
import { candidateScopeKey, renderInstructionChanges, renderWorkspaceInstructionSet, USER_GLOBAL_DIRECTORY, USER_GLOBAL_FILE } from '../src/render.ts'
|
|
|
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
|
|
import {
|
|
|
- createInboxFixture,
|
|
|
- type InboxFixture,
|
|
|
+ mountAgentLoopTestDependencies,
|
|
|
+ mountAgentLoopTestHarness,
|
|
|
} from '@deepseek-ai/dsh-agent-loop-testkit'
|
|
|
|
|
|
/** Per-candidate reconciliation scope key: directory paired with the file name. */
|
|
|
@@ -53,19 +53,16 @@ const sk = (directory: string, candidateName: string): string => candidateScopeK
|
|
|
|
|
|
const testToolSignal = new AbortController().signal
|
|
|
const isolatedInboxCtx = new Context()
|
|
|
-await isolatedInboxCtx.plugin(SessionStore)
|
|
|
-await isolatedInboxCtx.plugin(SessionProjectionRegistry)
|
|
|
-await isolatedInboxCtx.plugin(AgentRegistry)
|
|
|
+await mountAgentLoopTestDependencies(isolatedInboxCtx)
|
|
|
+const isolatedAgentLoop = await mountAgentLoopTestHarness(isolatedInboxCtx)
|
|
|
let nextStubSession = 1
|
|
|
+afterAll(() => isolatedInboxCtx.fiber.dispose())
|
|
|
|
|
|
type TestAgent = Agent
|
|
|
-const inboxFixtures = new WeakMap<Agent, InboxFixture>()
|
|
|
|
|
|
-/** Return the loop-driver operations paired with one structural test Agent. */
|
|
|
-function inboxFixture(agent: Agent): InboxFixture {
|
|
|
- const fixture = inboxFixtures.get(agent)
|
|
|
- if (fixture === undefined) throw new Error('agent Inbox fixture is unavailable')
|
|
|
- return fixture
|
|
|
+/** Admit one test Agent's pending input through the production loop driver. */
|
|
|
+function claimInbox(agent: Agent, target: 'next-turn' | 'next-step'): UserMessage[] {
|
|
|
+ return isolatedAgentLoop.claim(agent, target, 1)
|
|
|
}
|
|
|
const requestTimeoutMs = process.platform === 'win32' ? 5_000 : 1_000
|
|
|
|
|
|
@@ -209,28 +206,27 @@ async function mountFileToolsAndWorkspaceContext(ctx: Context, config: workspace
|
|
|
|
|
|
function stubAgent(cwd?: string, seed: readonly SessionEvent[] = []): TestAgent {
|
|
|
const id = SessionId(`agent-instructions-${String(nextStubSession++)}`)
|
|
|
- const agentCtx = isolatedInboxCtx
|
|
|
- const session = agentCtx.sessions.create(id, {
|
|
|
- seed,
|
|
|
- ...cwd === undefined ? {} : { meta: { createdAt: 0, cwd } },
|
|
|
- })
|
|
|
- const fixture = createInboxFixture(agentCtx.sessionProjections, session)
|
|
|
- const agent: TestAgent = {
|
|
|
- ctx: agentCtx,
|
|
|
- id: SessionId('a1'),
|
|
|
- options: {},
|
|
|
- session,
|
|
|
- inbox: fixture.inbox,
|
|
|
- status: 'idle',
|
|
|
- send: () => {},
|
|
|
- followup: () => {},
|
|
|
- steer: () => {},
|
|
|
- inject: () => { throw new Error('agent-instructions must append directly to the open step') },
|
|
|
- cancel() {},
|
|
|
- runMaintenance: task => task(new AbortController().signal),
|
|
|
- whenIdle: () => Promise.resolve(),
|
|
|
+ const agent = isolatedAgentLoop.create(
|
|
|
+ id,
|
|
|
+ {},
|
|
|
+ cwd === undefined ? {} : { cwd },
|
|
|
+ )
|
|
|
+ const append = agent.session.append.bind(agent.session) as unknown as (
|
|
|
+ type: SessionEvent['type'],
|
|
|
+ data: SessionEvent['data'],
|
|
|
+ opts?: Partial<SurfaceIntent>,
|
|
|
+ ) => SessionEvent
|
|
|
+ for (const event of seed) {
|
|
|
+ if ('surfaceOp' in event || 'sourceEventSeqs' in event) {
|
|
|
+ append(event.type, event.data, {
|
|
|
+ ...event.surfaceOp === undefined ? {} : { surfaceOp: event.surfaceOp },
|
|
|
+ ...event.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: event.sourceEventSeqs },
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ append(event.type, event.data)
|
|
|
+ }
|
|
|
}
|
|
|
- inboxFixtures.set(agent, fixture)
|
|
|
+ if (seed.at(-1)?.type !== 'session/end-seed') agent.session.append('session/end-seed', {})
|
|
|
return agent
|
|
|
}
|
|
|
|
|
|
@@ -282,7 +278,7 @@ function baselineEvents(agent: Agent): SessionEvent[] {
|
|
|
async function appendAdditionalContexts(ctx: Context, agent: TestAgent): Promise<number | undefined> {
|
|
|
await syncedWorkspaceContext(ctx, agent)
|
|
|
let lastSeq: number | undefined
|
|
|
- for (const claimed of inboxFixture(agent).claim('next-step')) {
|
|
|
+ for (const claimed of claimInbox(agent, 'next-step')) {
|
|
|
if (claimed.source.kind !== 'agent-instructions') continue
|
|
|
const event = agent.session.append('user/message', claimed, { surfaceOp: 'append' })
|
|
|
ctx.emit('session/event', agent.session, event)
|
|
|
@@ -300,7 +296,7 @@ async function composeBaselinePrefix(ctx: Context, agent: TestAgent): Promise<Me
|
|
|
{ messages: [], turn: 1, step: 1, signal },
|
|
|
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
|
|
|
)
|
|
|
- const claimed = inboxFixture(agent).claim('next-step')
|
|
|
+ const claimed = claimInbox(agent, 'next-step')
|
|
|
const decision = await agentEvents(ctx, agent).waterfall(
|
|
|
'agent/pre-step',
|
|
|
{ messages: claimed, turn: 1, step: 2, signal },
|
|
|
@@ -1413,7 +1409,7 @@ describe('workspace context request injection', () => {
|
|
|
await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
|
|
|
const resumed = stubAgent(root, original.session.snapshotEvents())
|
|
|
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
|
|
|
- const claimed = inboxFixture(resumed).claim('next-step')
|
|
|
+ const claimed = claimInbox(resumed, 'next-step')
|
|
|
const decision = await agentEvents(ctx, resumed).waterfall(
|
|
|
'agent/pre-step',
|
|
|
{ messages: claimed, turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) },
|
|
|
@@ -1459,7 +1455,7 @@ describe('workspace context request injection', () => {
|
|
|
await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
|
|
|
const resumed = stubAgent(root, original.session.snapshotEvents())
|
|
|
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
|
|
|
- const staleClaim = inboxFixture(resumed).claim('next-step')
|
|
|
+ const staleClaim = claimInbox(resumed, 'next-step')
|
|
|
const staleDecision = await agentEvents(ctx, resumed).waterfall(
|
|
|
'agent/pre-step',
|
|
|
{ messages: staleClaim, turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) },
|
|
|
@@ -1512,7 +1508,7 @@ describe('workspace context request injection', () => {
|
|
|
await mountWorkspaceContextPlugin(resumedCtx, { dshHome: home, maxBytes })
|
|
|
const resumed = stubAgent(root, original.session.snapshotEvents())
|
|
|
agentEvents(resumedCtx, resumed).emit('agent/session-start', { source: 'resume' })
|
|
|
- const claimed = inboxFixture(resumed).claim('next-step')
|
|
|
+ const claimed = claimInbox(resumed, 'next-step')
|
|
|
const decision = await agentEvents(resumedCtx, resumed).waterfall(
|
|
|
'agent/pre-step',
|
|
|
{ messages: claimed, turn: 1, step: 1, signal: AbortSignal.timeout(requestTimeoutMs) },
|
|
|
@@ -4667,7 +4663,7 @@ describe('workspace context inbox synchronization', () => {
|
|
|
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
|
|
|
const agent = stubAgent(join(root, 'pkg'))
|
|
|
await syncedWorkspaceContext(ctx, agent)
|
|
|
- const claimed = inboxFixture(agent).claim('next-step')
|
|
|
+ const claimed = claimInbox(agent, 'next-step')
|
|
|
await write(join(root, 'pkg/AGENTS.md'), 'new claimed rule with more detail')
|
|
|
const downstream = { kind: 'enter' as const, messages: claimed }
|
|
|
|