# Conflicts: # apps/web/tests/expected/agent-preset-selection/header.expected.md # docs/architecture.i18n.yaml # docs/config-catalog.i18n.yaml # docs/config-catalog.md # docs/event-producer-consumer.i18n.yaml # docs/event-producer-consumer.md # docs/event-producer-consumer.zh.md # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # docs/persistence-catalog.i18n.yaml # docs/persistence-catalog.md # docs/persistence-catalog.zh.md # docs/persistence-schema.json # docs/subsystems/web-client.i18n.yaml # packages/api/README.i18n.yaml # packages/api/README.md # packages/api/README.zh.md # packages/api/session-controller/README.i18n.yaml # packages/api/session-controller/README.md # packages/api/session-controller/README.zh.md # packages/api/session-controller/src/client/sessions/manager.ts # packages/api/session-controller/src/client/sessions/service.ts # packages/api/session-controller/tests/manager.client.spec.ts # packages/client/locale/tests/language-row.client.spec.tsx # packages/client/ui-chat/tests/chat-view.client.spec.tsx # packages/client/ui-chat/tests/transcript-view-row.client.spec.tsx # packages/client/ui-conversation/tests/conversation-registry.client.spec.ts # packages/client/ui-conversation/tests/enter-behavior-row.client.spec.tsx # packages/client/ui-conversation/tests/input-bar.client.spec.tsx # packages/client/ui-conversation/tests/input-matrix.client.spec.tsx # packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx # packages/client/ui-conversation/tests/skeleton.client.spec.tsx # packages/client/ui-deliverables/tests/deliverables.client.spec.tsx # packages/client/ui-jobs/tests/job-list-action.client.spec.tsx # packages/client/ui-layout/tests/app-frame.client.spec.tsx # packages/client/ui-layout/tests/document-title.client.spec.tsx # packages/client/ui-open-in-app/tests/open-in-app-action.client.spec.tsx # packages/client/ui-primitives/README.i18n.yaml # packages/client/ui-session/tests/ui-session.client.spec.ts # packages/client/ui-settings-general/tests/settings-root.client.spec.tsx # packages/client/ui-settings-unarchive-sessions/tests/components.client.spec.tsx # packages/client/ui-subagent/README.i18n.yaml # packages/client/ui-subagent/README.md # packages/client/ui-subagent/README.zh.md # packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx # packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx # packages/client/ui-theme/tests/appearance-row.client.spec.tsx # packages/client/ui-theme/tests/font-size-row.client.spec.tsx # packages/client/ui-tool/tests/coverage-tails.client.spec.tsx # packages/client/ui-tool/tests/diff-card.client.spec.tsx # packages/client/ui-tool/tests/image-card.client.spec.tsx # packages/client/ui-tool/tests/read-card.client.spec.tsx # packages/client/ui-tool/tests/terminal-card.client.spec.tsx # packages/client/ui-trajectory/tests/views.client.spec.tsx # packages/client/ui-user-questions/tests/plan-review-panel.client.spec.tsx # packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx # packages/client/ui-workflow-run/tests/workflow-run.client.spec.tsx # packages/client/ui-workspace/tests/apply.client.spec.ts # packages/client/ui-workspace/tests/tree.client.spec.ts # packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx # packages/client/ui-workspace/tests/workspace-picker.client.spec.tsx # packages/client/ui-workspace/tests/workspaces-service.client.spec.ts # packages/extensions/cordis-client-runner/src/client/slot-catalog.ts # packages/jobs/tool-jobs/src/index.ts # packages/shell/tool-bash/README.i18n.yaml # packages/test-support/client-runtime/src/sessions.ts
102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
import { Context } from '@deepseek-ai/cordis'
|
|
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
|
import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import type { JobAppendOptions, JobHandle, JobOutcome } from '@deepseek-ai/dsh-jobs'
|
|
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
|
|
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
|
|
/** A registry with a tiny ring so eviction is cheap to reach. */
|
|
export async function harness(): Promise<Context> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(AgentRegistry)
|
|
await ctx.plugin(LocalJobRegistry, { retainBytes: 64, settledRetainBytes: 64 })
|
|
ctx.jobs.attachController('job-controller-test')
|
|
return ctx
|
|
}
|
|
|
|
/** Register a live agent for `rawId` under its own lifecycle scope. */
|
|
export async function registerAgent(ctx: Context, rawId: string): Promise<Agent & { disposeScope: () => Promise<void> }> {
|
|
const scopeFiber = ctx.plugin(() => {})
|
|
const id = SessionId(rawId)
|
|
const session = Session.create(id)
|
|
const agent: Agent & { disposeScope: () => Promise<void> } = {
|
|
id,
|
|
options: {},
|
|
session,
|
|
inbox: unsupportedInbox(),
|
|
status: 'idle',
|
|
ctx: scopeFiber.ctx,
|
|
send: () => {},
|
|
followup: () => {},
|
|
steer: () => {},
|
|
inject: () => {},
|
|
cancel: () => {},
|
|
runMaintenance: task => task(new AbortController().signal),
|
|
whenIdle: () => Promise.resolve(),
|
|
disposeScope: () => scopeFiber.dispose(),
|
|
}
|
|
await ctx.agents.register(agent)
|
|
return agent
|
|
}
|
|
|
|
/**
|
|
* Start one push-producer job whose `done` settles on demand; expose the
|
|
* handle's writers. A cancel settles the job as killed unless `slowStop`
|
|
* keeps it stopping until the test settles it.
|
|
*/
|
|
export function startJob(ctx: Context, options: { label?: string; owner?: SessionId; slowStop?: boolean } = {}) {
|
|
let settle!: (outcome: JobOutcome) => void
|
|
let handle!: JobHandle
|
|
const id = ctx.jobs.start({
|
|
kind: 'bash',
|
|
label: options.label ?? 'echo',
|
|
...options.owner !== undefined ? { owner: options.owner } : {},
|
|
run: (job) => {
|
|
handle = job
|
|
return {
|
|
cancel() { if (options.slowStop !== true) settle({ status: 'killed' }) },
|
|
done: new Promise<JobOutcome>((resolve) => { settle = resolve }),
|
|
}
|
|
},
|
|
})
|
|
return {
|
|
id,
|
|
append: (text: string, opts?: JobAppendOptions) => { handle.append(text, opts) },
|
|
progress: (line: string) => { handle.updateProgress(line) },
|
|
settle: async (outcome: JobOutcome) => {
|
|
settle(outcome)
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
},
|
|
}
|
|
}
|
|
|
|
/** Read frames until `count` arrive or the generator finishes, then abort. */
|
|
export async function collect<Frame>(
|
|
iterable: AsyncIterable<Frame>,
|
|
count: number,
|
|
abort: AbortController,
|
|
): Promise<Frame[]> {
|
|
const frames: Frame[] = []
|
|
const iterator = iterable[Symbol.asyncIterator]()
|
|
try {
|
|
while (frames.length < count) {
|
|
const step = await iterator.next()
|
|
if (step.done) return frames
|
|
frames.push(step.value)
|
|
}
|
|
} finally {
|
|
abort.abort()
|
|
await iterator.next().catch(() => undefined)
|
|
}
|
|
return frames
|
|
}
|
|
|
|
export const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
|
|
/** The next yielded frame, or undefined once the generator finished. */
|
|
export async function next<Frame>(iterator: AsyncIterator<Frame>): Promise<Frame | undefined> {
|
|
const step = await iterator.next()
|
|
return step.done === true ? undefined : step.value
|
|
}
|