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.
This commit is contained in:
@@ -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.
|
// ONE select advertises, current from the configured default preset.
|
||||||
const created = await client.newSession({ cwd: workdir, mcpServers: [] })
|
const created = await client.newSession({ cwd: workdir, mcpServers: [] })
|
||||||
const advertised = created.configOptions ?? []
|
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]))
|
expect(advertised.map(option => [option.id, 'currentValue' in option ? option.currentValue : undefined]))
|
||||||
.toEqual([['model', modelValue], ['permission', 'workspace-write']])
|
.toEqual([['model', modelValue], ['permission', 'workspace-write']])
|
||||||
// A switch responds with the COMPLETE refreshed state (the spec contract),
|
// A switch responds with the COMPLETE refreshed state (the spec contract),
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ node, launch_args_json, launch_env_json, cwd, actions_json, expected_exit, timeo
|
|||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update(json.loads(launch_env_json))
|
env.update(json.loads(launch_env_json))
|
||||||
env.update({"COLUMNS": "100", "LINES": "30"})
|
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)
|
actions = json.loads(actions_json)
|
||||||
pid, fd = pty.fork()
|
pid, fd = pty.fork()
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
@@ -63,12 +67,19 @@ export interface TuiPtySmokeOptions {
|
|||||||
readonly label: string
|
readonly label: string
|
||||||
readonly tempDirPrefix: string
|
readonly tempDirPrefix: string
|
||||||
readonly binScript: 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 tsconfigPath: string
|
||||||
readonly actions?: readonly TuiPtyAction[]
|
readonly actions?: readonly TuiPtyAction[]
|
||||||
readonly env?: Readonly<NodeJS.ProcessEnv>
|
readonly env?: Readonly<NodeJS.ProcessEnv>
|
||||||
readonly expectedExitCode?: number
|
readonly expectedExitCode?: number
|
||||||
readonly timeoutMs?: 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> {
|
function definedEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
||||||
@@ -135,6 +146,9 @@ async function runWindowsPtySmoke(
|
|||||||
env: definedEnv({
|
env: definedEnv({
|
||||||
...process.env,
|
...process.env,
|
||||||
...launch.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',
|
COLUMNS: '100',
|
||||||
LINES: '30',
|
LINES: '30',
|
||||||
}),
|
}),
|
||||||
@@ -175,9 +189,13 @@ export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<strin
|
|||||||
const cwd = await mkdtemp(join(tmpdir(), options.tempDirPrefix))
|
const cwd = await mkdtemp(join(tmpdir(), options.tempDirPrefix))
|
||||||
const timeoutMs = options.timeoutMs ?? 25_000
|
const timeoutMs = options.timeoutMs ?? 25_000
|
||||||
try {
|
try {
|
||||||
|
await options.prepare?.(cwd)
|
||||||
const launch = resolveExampleLaunch({
|
const launch = resolveExampleLaunch({
|
||||||
srcBin: options.binScript,
|
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,
|
tsconfigPath: options.tsconfigPath,
|
||||||
exposeInternals: true,
|
exposeInternals: true,
|
||||||
env: {
|
env: {
|
||||||
@@ -186,10 +204,12 @@ export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<strin
|
|||||||
...options.env,
|
...options.env,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (process.platform === 'win32') {
|
const output = process.platform === 'win32'
|
||||||
return await runWindowsPtySmoke(launch, cwd, options, timeoutMs)
|
? await runWindowsPtySmoke(launch, cwd, options, timeoutMs)
|
||||||
}
|
: await runPosixPtySmoke(launch, cwd, options, timeoutMs)
|
||||||
return 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 {
|
} finally {
|
||||||
await rm(cwd, { recursive: true, force: true })
|
await rm(cwd, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
|
|||||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||||
import * as WorkspaceContext from '@deepseek-ai/dsh-workspace-context'
|
import * as WorkspaceContext from '@deepseek-ai/dsh-workspace-context'
|
||||||
|
import { candidateScopeKey } from '../src/render.ts'
|
||||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
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)
|
&& !Array.isArray(event.data.meta)
|
||||||
&& event.data.meta.kind === 'workspace-instructions')
|
&& event.data.meta.kind === 'workspace-instructions')
|
||||||
expect(update?.type === 'context/message' && update.data.meta).toMatchObject({
|
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'
|
const updateText = update?.type === 'context/message'
|
||||||
? update.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
? update.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { chmod, mkdtemp, mkdir, rm, stat, symlink, utimes, writeFile } from 'node:fs/promises'
|
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 { tmpdir } from 'node:os'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
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 () => {
|
it('skips a file that becomes unreadable after discovery without failing the request', async () => {
|
||||||
const root = await tempRepo()
|
const root = await tempRepo()
|
||||||
const home = await tempRepo()
|
const home = await tempRepo()
|
||||||
|
|||||||
Reference in New Issue
Block a user