diff --git a/packages/context/tmux-context/tests/fixtures/tmux-context-driver.ts b/packages/context/tmux-context/tests/fixtures/tmux-context-driver.ts new file mode 100644 index 0000000000..45a8aa147a --- /dev/null +++ b/packages/context/tmux-context/tests/fixtures/tmux-context-driver.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node +/** Test driver that sends two turns through one Headless Loader composition. */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ + +import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' +import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts' + +const configPath = process.argv[2] +if (configPath === undefined) throw new Error('tmux-context driver requires a config path') + +const ctx = await boot('tmux-context-e2e', resolveConfigPath(configPath, undefined)) +try { + await runOneShot(ctx, { task: 'first' }) + await runOneShot(ctx, { task: 'second' }) +} finally { + await ctx.fiber.dispose() +} diff --git a/packages/context/tmux-context/tests/fixtures/tmux-context-mock-bash.ts b/packages/context/tmux-context/tests/fixtures/tmux-context-mock-bash.ts new file mode 100644 index 0000000000..3e9540abff --- /dev/null +++ b/packages/context/tmux-context/tests/fixtures/tmux-context-mock-bash.ts @@ -0,0 +1,46 @@ +import type { Context } from 'cordis' +import { BashExecutor } from '@deepseek-ai/dsh-bash' +import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash' + +/** + * Deterministic `ctx.bash` for the tmux-context Loader fixture: any command + * (the plugin's `tmux display-message`) returns a fixed tab-delimited reading, + * so the injected tmux location is stable without a real tmux server. `start()` + * throws — tmux-context must never spawn a background process. + */ +class TmuxMockBash extends BashExecutor { + override resolve(request: BashExecRequest): BashExecSpec { + return { + command: request.command, + workdir: request.workdir ?? process.cwd(), + timeoutMs: request.timeoutMs ?? 60_000, + stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000, + signal: request.signal, + sandboxPolicy: request.sandboxPolicy, + } + } + + override run(_spec: BashExecSpec): Promise { + const line = ['work', '0', 'editor', '1', '%3', '1', '1', 'a1b2,80x24,0,0,4'].join('\\t') + return Promise.resolve({ + exitCode: 0, + signal: null, + timedOut: false, + aborted: false, + timeoutMs: 60_000, + stdout: { text: `${line}\n`, truncated: false }, + stderr: { text: '', truncated: false }, + }) + } + + override start(): BashProcess { + throw new Error('tmux-context must never start a background task') + } +} + +export const name = 'tmux-context-mock-bash' + +/** Register the deterministic `ctx.bash` executor for the fixture. */ +export function apply(ctx: Context): void { + ctx.plugin(TmuxMockBash) +} diff --git a/packages/context/tmux-context/tests/fixtures/tmux-context-mock-llm.ts b/packages/context/tmux-context/tests/fixtures/tmux-context-mock-llm.ts new file mode 100644 index 0000000000..2f6c7a6408 --- /dev/null +++ b/packages/context/tmux-context/tests/fixtures/tmux-context-mock-llm.ts @@ -0,0 +1,22 @@ +import type { Context } from 'cordis' +import { LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm' + +/** Deterministic one-step adapter for the tmux-context Loader fixture. */ +class TmuxContextMockAdapter extends LlmAdapter { + async * stream(): AsyncIterable { + const text = 'tmux context sampled' + yield { type: 'block-start', index: 0, blockType: 'text' } + yield { type: 'text-delta', index: 0, text } + yield { type: 'block-end', index: 0, block: { type: 'text', text } } + yield { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } } + yield { type: 'finish', reason: { kind: 'stop' } } + } +} + +export const name = 'tmux-context-mock-llm' +export const inject = ['llm'] + +/** Register the test-only `tmux-context-mock` adapter. */ +export function apply(ctx: Context): void { + ctx.llm.registerAdapter(['tmux-context-mock'], new TmuxContextMockAdapter()) +} diff --git a/packages/context/tmux-context/tests/fixtures/tmux-context.cordis.yml b/packages/context/tmux-context/tests/fixtures/tmux-context.cordis.yml new file mode 100644 index 0000000000..419922a0e3 --- /dev/null +++ b/packages/context/tmux-context/tests/fixtures/tmux-context.cordis.yml @@ -0,0 +1,21 @@ +# Test-only composition: keep tmux-context opt-in while exercising its real Loader/app path. +# A deterministic mock ctx.bash returns a fixed tmux reading, so the injected location +# is stable without a real tmux server on the test host. +- id: tmux-context-mock-llm + name: './tmux-context-mock-llm.ts' + +- id: bash + name: './tmux-context-mock-bash.ts' + +- id: tmux-context + name: '@deepseek-ai/dsh-tmux-context' + +- id: cli-agent + name: '@deepseek-ai/dsh-cli-demo' + config: + provider: tmux-context-mock + model: tmux-context-mock + persona: 'Test the tmux-context plugin.' + persistenceRoot: './.sessions' + persistenceCompression: 'none' + workspaceContext: false diff --git a/packages/context/tmux-context/tests/tmux-context.e2e.ts b/packages/context/tmux-context/tests/tmux-context.e2e.ts index 06e3c1f3f5..386a326624 100644 --- a/packages/context/tmux-context/tests/tmux-context.e2e.ts +++ b/packages/context/tmux-context/tests/tmux-context.e2e.ts @@ -5,16 +5,8 @@ import { describe, expect, it } from 'vitest' import { type SessionEvent } from '@deepseek-ai/dsh-session' import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' -// Keep the Loader config under examples so both modes exercise the same deployable -// topology: local fixture source plus bare plugins owned by the examples workspace. -const driver = fileURLToPath(new URL( - '../../../../examples/headless-agent/tests/fixtures/tmux-context-driver.ts', - import.meta.url, -)) -const configPath = fileURLToPath(new URL( - '../../../../examples/headless-agent/tests/fixtures/tmux-context.cordis.yml', - import.meta.url, -)) +const driver = fileURLToPath(new URL('./fixtures/tmux-context-driver.ts', import.meta.url)) +const configPath = fileURLToPath(new URL('./fixtures/tmux-context.cordis.yml', import.meta.url)) const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)) async function jsonlFiles(dir: string): Promise {