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 'cordis' import Loader from '@cordisjs/plugin-loader' import Include from '@cordisjs/plugin-include' import { CallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import PtyService from '@deepseek-ai/dsh-pty' import SandboxProvider from '@deepseek-ai/dsh-sandbox' import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' import * as PtyLocal from '@deepseek-ai/dsh-pty-local' import * as ToolPty from '@deepseek-ai/dsh-tool-pty' 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 }) class PassthroughSandbox extends SandboxProvider { confine(argv: readonly string[], _policy: SandboxPolicy): ConfinedArgv { return { argv: [...argv], enforcement: 'full', denialSignatures: [], runnerFailureSignatures: [] } } } function agent(ctx: Context): Agent { const scope = ctx.plugin(() => {}) const id = SessionId('pty-loader-agent') const value: Agent = { id, options: {}, session: new Session(id), status: 'idle', ctx: scope.ctx, followup: () => AgentMessageId('stub'), queue: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), inject: () => AgentMessageId('stub'), send: () => AgentMessageId('stub'), cancel() {}, whenIdle: () => Promise.resolve(), } ctx.agents.register(value) return value } function resultText(result: { content: { type: string; text?: string }[] }): string { return result.content.filter(block => block.type === 'text').map(block => block.text).join('') } const suite = process.platform === 'linux' || process.platform === 'darwin' ? describe : describe.skip suite('terminal real Loader composition through cordis.yml', () => { it('boots cordis.yml and preserves shell state across real tool calls', async () => { root = await mkdtemp(join(tmpdir(), 'dsh-pty-loader-')) const configPath = join(root, 'cordis.yml') await writeFile(configPath, [ "- name: '@deepseek-ai/dsh-agent'", "- name: '@deepseek-ai/dsh-system-prompt'", "- name: '@deepseek-ai/dsh-tools'", "- name: '@deepseek-ai/dsh-pty'", "- name: '@deepseek-ai/dsh-test-sandbox'", "- name: '@deepseek-ai/dsh-sandbox-policy'", ' config:', ' mode: danger-full-access', ` workspaceRoot: ${JSON.stringify(root)}`, "- name: '@deepseek-ai/dsh-pty-local'", ' config:', ' pollIntervalMs: 10', ' exactProbeAfterMs: 20', ' idleSilenceMs: 250', ' timeoutMs: 2000', ' disposeGraceMs: 500', "- name: '@deepseek-ai/dsh-tool-pty'", '', ].join('\n')) context = new Context() context.baseUrl = pathToFileURL(root).href + '/' await context.plugin(Loader) context.loader.builtins.include = Include const modules = new Map([ ['@deepseek-ai/dsh-agent', AgentRegistry], ['@deepseek-ai/dsh-system-prompt', SystemPrompt], ['@deepseek-ai/dsh-tools', ToolRegistry], ['@deepseek-ai/dsh-pty', PtyService], ['@deepseek-ai/dsh-test-sandbox', PassthroughSandbox], ['@deepseek-ai/dsh-sandbox-policy', SandboxPolicyService], ['@deepseek-ai/dsh-pty-local', PtyLocal], ['@deepseek-ai/dsh-tool-pty', ToolPty], ]) 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 await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } }) await context.loader.await() const owner = agent(context) const signal = new AbortController().signal const spawn = await context.tools.execute({ signal, callId: CallId('spawn'), name: 'terminal_open', arguments: { type: 'shell', name: 'main', cwd: root }, agent: owner, }) expect(resultText(spawn)).toContain('started terminal session pty-1 (main)') await context.tools.execute({ signal, callId: CallId('state'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'export KEEP=loader; cd /' }, agent: owner, }) const read = await context.tools.execute({ signal, callId: CallId('read'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"' }, agent: owner, }) expect(resultText(read)).toContain('cwd=/ keep=loader') expect(context.pty.list(owner)).toHaveLength(1) }, 15_000) })