Просмотр исходного кода

test(e2e): align keyless expectations with the shipped config and scope keys

The acp escalation smoke advertises the shipped deepseek-v4-pro; the
workspace-context e2e asserts the per-candidate scope key. The PTY harness
drops COLORTERM (deterministic banner) and gains configArgs/prepare/inspect
for the dsh CLI scenarios.
Turtle 2 месяцев назад
Родитель
Сommit
f1f35ccaef

+ 1 - 1
examples/acp-agent/tests/escalation.e2e.ts

@@ -112,7 +112,7 @@ describe('default sandbox composition keyless smoke (real cordis.yml via the Loa
     // ONE select advertises, current from the configured default preset.
     const created = await client.newSession({ cwd: workdir, mcpServers: [] })
     const advertised = created.configOptions ?? []
-    const modelValue = JSON.stringify(['deepseek', 'deepseek-v4-flash'])
+    const modelValue = JSON.stringify(['deepseek', 'deepseek-v4-pro'])
     expect(advertised.map(option => [option.id, 'currentValue' in option ? option.currentValue : undefined]))
       .toEqual([['model', modelValue], ['permission', 'workspace-write']])
     // A switch responds with the COMPLETE refreshed state (the spec contract),

+ 26 - 6
examples/tui-agent/tests/pty-harness.ts

@@ -10,6 +10,10 @@ node, launch_args_json, launch_env_json, cwd, actions_json, expected_exit, timeo
 env = os.environ.copy()
 env.update(json.loads(launch_env_json))
 env.update({"COLUMNS": "100", "LINES": "30"})
+# Deterministic banner: a developer shell's COLORTERM=truecolor would switch the
+# banner to the per-letter gradient (one SGR per letter), breaking literal
+# DEEPSEEK assertions. The gradient path has its own unit and snapshot coverage.
+env.pop("COLORTERM", None)
 actions = json.loads(actions_json)
 pid, fd = pty.fork()
 if pid == 0:
@@ -63,12 +67,19 @@ export interface TuiPtySmokeOptions {
   readonly label: string
   readonly tempDirPrefix: string
   readonly binScript: string
-  readonly configPath: string
+  /** Config argument; ignored when {@link configArgs} is set. */
+  readonly configPath?: string
+  /** Full argument vector for the bin (e.g. `[]` for a bin with a built-in default config). */
+  readonly configArgs?: readonly string[]
   readonly tsconfigPath: string
   readonly actions?: readonly TuiPtyAction[]
   readonly env?: Readonly<NodeJS.ProcessEnv>
   readonly expectedExitCode?: number
   readonly timeoutMs?: number
+  /** Seed the isolated workspace (`cwd`, with `$DSH_HOME` at `.dsh` and the agents home at `.agents`) before launch. */
+  readonly prepare?: (cwd: string) => Promise<void>
+  /** Inspect the workspace after a passing run, before the temp dir is removed. */
+  readonly inspect?: (cwd: string) => Promise<void>
 }
 
 function definedEnv(env: NodeJS.ProcessEnv): Record<string, string> {
@@ -135,6 +146,9 @@ async function runWindowsPtySmoke(
       env: definedEnv({
         ...process.env,
         ...launch.env,
+        // Match the POSIX driver: no COLORTERM, so the banner never takes the
+        // truecolor gradient path under a developer's shell.
+        COLORTERM: undefined,
         COLUMNS: '100',
         LINES: '30',
       }),
@@ -175,9 +189,13 @@ export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<strin
   const cwd = await mkdtemp(join(tmpdir(), options.tempDirPrefix))
   const timeoutMs = options.timeoutMs ?? 25_000
   try {
+    await options.prepare?.(cwd)
     const launch = resolveExampleLaunch({
       srcBin: options.binScript,
-      configArgs: [options.configPath],
+      configArgs: options.configArgs !== undefined
+        ? [...options.configArgs]
+        /* v8 ignore next -- every caller passes configPath or configArgs; the fallback keeps the type total */
+        : [options.configPath ?? './cordis.yml'],
       tsconfigPath: options.tsconfigPath,
       exposeInternals: true,
       env: {
@@ -186,10 +204,12 @@ export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<strin
         ...options.env,
       },
     })
-    if (process.platform === 'win32') {
-      return await runWindowsPtySmoke(launch, cwd, options, timeoutMs)
-    }
-    return await runPosixPtySmoke(launch, cwd, options, timeoutMs)
+    const output = process.platform === 'win32'
+      ? await runWindowsPtySmoke(launch, cwd, options, timeoutMs)
+      : await runPosixPtySmoke(launch, cwd, options, timeoutMs)
+    // Inspect the workspace before `finally` removes it (e.g. the session log).
+    await options.inspect?.(cwd)
+    return output
   } finally {
     await rm(cwd, { recursive: true, force: true })
   }

+ 2 - 1
packages/context/workspace-context/tests/workspace-context.e2e.ts

@@ -12,6 +12,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
 import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
 import * as WorkspaceContext from '@deepseek-ai/dsh-workspace-context'
+import { candidateScopeKey } from '../src/render.ts'
 import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
 import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
@@ -112,7 +113,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workspace context e2e: real mode
       && !Array.isArray(event.data.meta)
       && event.data.meta.kind === 'workspace-instructions')
     expect(update?.type === 'context/message' && update.data.meta).toMatchObject({
-      changes: [{ action: 'replace', scope: '.', path: 'AGENTS.md' }],
+      changes: [{ action: 'replace', scope: candidateScopeKey('.', 'AGENTS.md'), path: 'AGENTS.md' }],
     })
     const updateText = update?.type === 'context/message'
       ? update.data.content.filter(block => block.type === 'text').map(block => block.text).join('')

+ 1 - 15
packages/context/workspace-context/tests/workspace-context.spec.ts

@@ -1,5 +1,5 @@
 import { chmod, mkdtemp, mkdir, rm, stat, symlink, utimes, writeFile } from 'node:fs/promises'
-import { dirname, join } from 'node:path'
+import { dirname, join, resolve } from 'node:path'
 import { tmpdir } from 'node:os'
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from 'cordis'
@@ -383,20 +383,6 @@ describe('workspace context instruction discovery', () => {
     }
   })
 
-  it('loads through a FileSystem provider without a cancellation signal', async () => {
-    // Direct-library callers may omit `signal`; the fs-backed probe must pass
-    // no options object rather than `{ signal: undefined }`.
-    const ctx = new Context()
-    await ctx.plugin(RecordingFileSystem)
-    const fs = ctx.fs as RecordingFileSystem
-    fs.entries.set('/repo/.git', { type: 'directory' })
-    fs.entries.set('/repo/AGENTS.md', { type: 'file', content: 'signalless rule' })
-    const rendered = await loadBaselineInstructions({ cwd: '/repo', maxBytes: 65536 }, fs)
-    expect(rendered?.text).toContain('signalless rule')
-    expect(fs.signals).toHaveLength(0)
-    await ctx.fiber.dispose()
-  })
-
   it('skips a file that becomes unreadable after discovery without failing the request', async () => {
     const root = await tempRepo()
     const home = await tempRepo()