test(e2e): drive the TUI keyless smoke through the cross-platform PTY harness
The smoke's inline Python pty driver only ran on POSIX (no termios on Windows). Rebuild every scenario — banner sweep, scripted conversation with model switch, /skill:, Code Mode overlay, resume failure, and the dsh CLI suite (default boot, personal overlay, invalid overlay, --resume flag, source-path prompt) — as marker-gated action lists on pty-harness.ts, which drives ConPTY via node-pty on Windows and the Python driver elsewhere. The harness gains configArgs (bins with built-in default configs), prepare (workspace seeding), and inspect (post-run log assertions); examples/ declares the session-title provider the shipped cordis.yml now mounts.
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
"@deepseek-ai/dsh-spill-local": "workspace:*",
|
"@deepseek-ai/dsh-spill-local": "workspace:*",
|
||||||
"@deepseek-ai/dsh-spill-policy": "workspace:*",
|
"@deepseek-ai/dsh-spill-policy": "workspace:*",
|
||||||
"@deepseek-ai/dsh-tui-demo": "workspace:*",
|
"@deepseek-ai/dsh-tui-demo": "workspace:*",
|
||||||
|
"@deepseek-ai/dsh-session-title-first-message-llm": "workspace:*",
|
||||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||||
"@deepseek-ai/dsh-subagent-acp": "workspace:*",
|
"@deepseek-ai/dsh-subagent-acp": "workspace:*",
|
||||||
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
|
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { spawn } from 'node:child_process'
|
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
||||||
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
||||||
import { tmpdir } from 'node:os'
|
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
|
import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
|
||||||
|
import { runTuiPtySmoke, type TuiPtySmokeOptions } from './pty-harness.ts'
|
||||||
|
|
||||||
const binScript = fileURLToPath(new URL('../../../packages/examples/tui-demo/src/bin.ts', import.meta.url))
|
const binScript = fileURLToPath(new URL('../../../packages/examples/tui-demo/src/bin.ts', import.meta.url))
|
||||||
const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
|
const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
|
||||||
@@ -13,204 +12,25 @@ const codeModeConfigPath = fileURLToPath(new URL('../code-mode.cordis.yml', impo
|
|||||||
const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis.yml', import.meta.url))
|
const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis.yml', import.meta.url))
|
||||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||||
|
|
||||||
const PTY_DRIVER = String.raw`
|
/**
|
||||||
import errno, json, os, pty, select, signal, sys, time
|
* Seed the harness workspace: personal files land in the isolated Harness home
|
||||||
node, launch_args_json, launch_env_json, cwd, resume_session_id, scenario, boot_marker = sys.argv[1:]
|
* (`.dsh`), skill bundles under the agents home's `skills/` root — the same
|
||||||
env = os.environ.copy()
|
* trees `$DSH_HOME` / `$DSH_AGENTS_HOME` point the child at.
|
||||||
env.update(json.loads(launch_env_json))
|
*/
|
||||||
env.update({
|
function seedWorkspace(
|
||||||
"COLUMNS": "100",
|
files: { personal?: Record<string, string>; skills?: Record<string, string> },
|
||||||
"LINES": "30",
|
): (cwd: string) => Promise<void> {
|
||||||
})
|
return async (cwd) => {
|
||||||
# Deterministic banner: a developer shell's COLORTERM=truecolor would switch the
|
for (const [name, content] of Object.entries(files.personal ?? {})) {
|
||||||
# banner to the per-letter gradient (one SGR per letter), breaking the literal
|
const file = join(cwd, '.dsh', name)
|
||||||
# DEEPSEEK assertions. The gradient path has its own unit and snapshot coverage.
|
await mkdir(dirname(file), { recursive: true })
|
||||||
env.pop("COLORTERM", None)
|
await writeFile(file, content)
|
||||||
if resume_session_id:
|
}
|
||||||
env["RESUME_SESSION_ID"] = resume_session_id
|
for (const [name, content] of Object.entries(files.skills ?? {})) {
|
||||||
pid, fd = pty.fork()
|
const file = join(cwd, '.agents', 'skills', name)
|
||||||
if pid == 0:
|
|
||||||
os.chdir(cwd)
|
|
||||||
os.execvpe(node, [node, *json.loads(launch_args_json)], env)
|
|
||||||
|
|
||||||
output = bytearray()
|
|
||||||
answered_question = False
|
|
||||||
opened_selector = False
|
|
||||||
selected_model = False
|
|
||||||
sent_prompt = False
|
|
||||||
sent_exit = False
|
|
||||||
deadline = time.monotonic() + 25
|
|
||||||
status = None
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
ready, _, _ = select.select([fd], [], [], 0.05)
|
|
||||||
if ready:
|
|
||||||
try:
|
|
||||||
chunk = os.read(fd, 65536)
|
|
||||||
except OSError as error:
|
|
||||||
if error.errno != errno.EIO:
|
|
||||||
raise
|
|
||||||
chunk = b""
|
|
||||||
if chunk:
|
|
||||||
output.extend(chunk)
|
|
||||||
if scenario == "conversation" and not opened_selector and b"scripted TUI ready." in output:
|
|
||||||
os.write(fd, b"/model\r")
|
|
||||||
opened_selector = True
|
|
||||||
if scenario == "conversation" and opened_selector and not selected_model and b"Select model" in output:
|
|
||||||
os.write(fd, b"\x1b[B\r")
|
|
||||||
selected_model = True
|
|
||||||
if scenario == "conversation" and selected_model and not sent_prompt and b"Model selected: tui-scripted/tui-scripted-model-pro." in output:
|
|
||||||
os.write(fd, b"exercise the TUI\r")
|
|
||||||
sent_prompt = True
|
|
||||||
if scenario == "conversation" and sent_prompt and not answered_question and b"How should the scripted run proceed?" in output:
|
|
||||||
os.write(fd, b"\r")
|
|
||||||
answered_question = True
|
|
||||||
if scenario == "conversation" and answered_question and not sent_exit and b"Decision received. Scripted TUI run complete." in output:
|
|
||||||
os.write(fd, b"/exit\r")
|
|
||||||
sent_exit = True
|
|
||||||
if scenario == "skill" and not selected_model and b"scripted TUI ready." in output:
|
|
||||||
os.write(fd, b"/model tui-scripted/tui-scripted-model-pro\r")
|
|
||||||
selected_model = True
|
|
||||||
if scenario == "skill" and selected_model and not sent_prompt and b"Model selected: tui-scripted/tui-scripted-model-pro." in output:
|
|
||||||
os.write(fd, b"/skill:scripted-skill\r")
|
|
||||||
sent_prompt = True
|
|
||||||
if scenario == "skill" and sent_prompt and not sent_exit and b"Scripted skill body received." in output:
|
|
||||||
os.write(fd, b"/exit\r")
|
|
||||||
sent_exit = True
|
|
||||||
if scenario == "boot" and not sent_exit and boot_marker.encode() in output:
|
|
||||||
os.write(fd, b"/exit\r")
|
|
||||||
sent_exit = True
|
|
||||||
waited, candidate = os.waitpid(pid, os.WNOHANG)
|
|
||||||
if waited == pid:
|
|
||||||
status = candidate
|
|
||||||
break
|
|
||||||
|
|
||||||
if status is None:
|
|
||||||
os.kill(pid, signal.SIGKILL)
|
|
||||||
_, status = os.waitpid(pid, 0)
|
|
||||||
sys.stdout.buffer.write(output)
|
|
||||||
if scenario == "resume-failure":
|
|
||||||
if b'ui-tui: session "missing-session" failed to start:' not in output:
|
|
||||||
sys.stderr.write("TUI did not render the startup failure before timeout\n")
|
|
||||||
sys.exit(126)
|
|
||||||
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 1:
|
|
||||||
sys.stderr.write("TUI startup failure did not exit with status 1\n")
|
|
||||||
sys.exit(127)
|
|
||||||
elif scenario == "conversation":
|
|
||||||
if not sent_prompt:
|
|
||||||
sys.stderr.write("TUI did not render the scripted welcome marker before timeout\n")
|
|
||||||
sys.exit(128)
|
|
||||||
if not answered_question:
|
|
||||||
sys.stderr.write("TUI did not render the user-question dialog before timeout\n")
|
|
||||||
sys.exit(129)
|
|
||||||
if not sent_exit:
|
|
||||||
sys.stderr.write("TUI did not finish the scripted tool round-trip before timeout\n")
|
|
||||||
sys.exit(130)
|
|
||||||
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
|
|
||||||
sys.stderr.write("TUI scripted conversation did not exit cleanly\n")
|
|
||||||
sys.exit(131)
|
|
||||||
elif scenario == "skill":
|
|
||||||
if not sent_prompt:
|
|
||||||
sys.stderr.write("TUI did not render the scripted welcome marker before typing /skill:\n")
|
|
||||||
sys.exit(132)
|
|
||||||
if b"Scripted skill body received." not in output:
|
|
||||||
sys.stderr.write("TUI did not deliver the loaded skill body to the model before timeout\n")
|
|
||||||
sys.exit(133)
|
|
||||||
if not sent_exit:
|
|
||||||
sys.stderr.write("TUI did not reach idle to accept /exit after the skill turn\n")
|
|
||||||
sys.exit(134)
|
|
||||||
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
|
|
||||||
sys.stderr.write("TUI skill scenario did not exit cleanly\n")
|
|
||||||
sys.exit(135)
|
|
||||||
else:
|
|
||||||
if not sent_exit:
|
|
||||||
sys.stderr.write("TUI did not render its welcome marker before timeout\n")
|
|
||||||
sys.exit(124)
|
|
||||||
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
|
|
||||||
sys.stderr.write("TUI child did not exit cleanly\n")
|
|
||||||
sys.exit(125)
|
|
||||||
`
|
|
||||||
|
|
||||||
interface TuiLoaderSmokeOptions {
|
|
||||||
config?: string
|
|
||||||
resumeSessionId?: string
|
|
||||||
scenario?: 'boot' | 'conversation' | 'resume-failure' | 'skill'
|
|
||||||
/** Welcome text the boot scenario waits for before sending `/exit`. */
|
|
||||||
bootMarker?: string
|
|
||||||
/** Bin to boot; defaults to the tui-demo bin (the dsh CLI tests override). */
|
|
||||||
srcBin?: string
|
|
||||||
/** Argument vector for the bin; defaults to `[config]`. */
|
|
||||||
configArgs?: string[]
|
|
||||||
/** Files written into the isolated Harness home (`$DSH_HOME`) before launch. */
|
|
||||||
personalFiles?: Record<string, string>
|
|
||||||
/** Skill bundles written under the isolated agents home (`.agents/skills/`) before launch, keyed by path below that root. */
|
|
||||||
skillFiles?: Record<string, string>
|
|
||||||
/** Runs against the workspace `cwd` after a clean exit, before it is removed. */
|
|
||||||
inspect?: (cwd: string) => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTuiLoaderSmoke(options: TuiLoaderSmokeOptions = {}): Promise<string> {
|
|
||||||
const cwd = await mkdtemp(join(tmpdir(), 'tui-agent-smoke-'))
|
|
||||||
try {
|
|
||||||
// Personal config is always isolated from the developer's real ~/.dsh;
|
|
||||||
// a test opts into an overlay by supplying files under the Harness home.
|
|
||||||
const dshHome = join(cwd, '.dsh')
|
|
||||||
for (const [name, content] of Object.entries(options.personalFiles ?? {})) {
|
|
||||||
await mkdir(dshHome, { recursive: true })
|
|
||||||
await writeFile(join(dshHome, name), content)
|
|
||||||
}
|
|
||||||
// The child chdirs to this cwd and the scripted config roots fs-local here,
|
|
||||||
// so a skill dropped under DSH_AGENTS_HOME's `skills/` root is discoverable
|
|
||||||
// and its body readable through the same tree the model-facing stack uses.
|
|
||||||
const skillsRoot = join(cwd, '.agents', 'skills')
|
|
||||||
for (const [name, content] of Object.entries(options.skillFiles ?? {})) {
|
|
||||||
const file = join(skillsRoot, name)
|
|
||||||
await mkdir(dirname(file), { recursive: true })
|
await mkdir(dirname(file), { recursive: true })
|
||||||
await writeFile(file, content)
|
await writeFile(file, content)
|
||||||
}
|
}
|
||||||
const launch = resolveExampleLaunch({
|
|
||||||
srcBin: options.srcBin ?? binScript,
|
|
||||||
configArgs: options.configArgs ?? [options.config ?? configPath],
|
|
||||||
tsconfigPath,
|
|
||||||
exposeInternals: true,
|
|
||||||
env: {
|
|
||||||
DEEPSEEK_API_KEY: 'keyless-tui-no-call',
|
|
||||||
DSH_HOME: dshHome,
|
|
||||||
DSH_AGENTS_HOME: join(cwd, '.agents'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const child = spawn('python3', [
|
|
||||||
'-c',
|
|
||||||
PTY_DRIVER,
|
|
||||||
launch.command,
|
|
||||||
JSON.stringify(launch.args),
|
|
||||||
JSON.stringify(launch.env),
|
|
||||||
cwd,
|
|
||||||
options.resumeSessionId ?? '',
|
|
||||||
options.scenario ?? 'boot',
|
|
||||||
// With no configured welcome the borderless banner sweeps in; its
|
|
||||||
// detail line's session id (`main-session-<uuid>`) renders only once
|
|
||||||
// the sweep reaches it, so it marks a settled banner.
|
|
||||||
options.bootMarker ?? 'main-session-',
|
|
||||||
], { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
||||||
let stdout = ''
|
|
||||||
let stderr = ''
|
|
||||||
child.stdout.setEncoding('utf8')
|
|
||||||
child.stdout.on('data', (chunk: string) => { stdout += chunk })
|
|
||||||
child.stderr.setEncoding('utf8')
|
|
||||||
child.stderr.on('data', (chunk: string) => { stderr += chunk })
|
|
||||||
child.once('error', reject)
|
|
||||||
child.once('exit', (code) => {
|
|
||||||
if (code !== 0) {
|
|
||||||
reject(new Error(`TUI PTY smoke exited ${String(code)}. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Inspect the workspace before `finally` removes it (e.g. the session log).
|
|
||||||
void (options.inspect?.(cwd) ?? Promise.resolve()).then(() => { resolve(stdout) }, reject)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
await rm(cwd, { recursive: true, force: true })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,12 +49,35 @@ async function readLoggedSystemPrompt(cwd: string): Promise<string> {
|
|||||||
throw new Error(`session log ${logRelPath} has no request/header event`)
|
throw new Error(`session log ${logRelPath} has no request/header event`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Shared defaults: the keyless key, the tui-demo bin, and the live cordis.yml. */
|
||||||
|
function smoke(overrides: Partial<TuiPtySmokeOptions> & { label: string }): Promise<string> {
|
||||||
|
return runTuiPtySmoke({
|
||||||
|
tempDirPrefix: 'tui-agent-smoke-',
|
||||||
|
binScript,
|
||||||
|
configPath,
|
||||||
|
tsconfigPath,
|
||||||
|
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The scripted conversation switches to the pro model first: the scripted
|
||||||
|
// adapter proves routing + prompt variables by rejecting tool-ful calls on any
|
||||||
|
// other route (see fixtures/tui-scripted-llm.ts).
|
||||||
|
const SELECT_PRO_MODEL = [
|
||||||
|
{ waitFor: 'scripted TUI ready.', send: '/model\r' },
|
||||||
|
{ waitFor: 'Select model', send: '\x1b[B\r' },
|
||||||
|
] as const
|
||||||
|
|
||||||
describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||||
it('boots pi-tui, sweeps the borderless banner in, accepts /exit, and restores the terminal', async () => {
|
it('boots pi-tui, sweeps the borderless banner in, accepts /exit, and restores the terminal', async () => {
|
||||||
const output = await runTuiLoaderSmoke()
|
|
||||||
// With no configured welcome the borderless banner sweeps in left-to-right;
|
// With no configured welcome the borderless banner sweeps in left-to-right;
|
||||||
// the boot scenario waits for the detail line's session id, which renders
|
// the detail line's session id (`main-session-<uuid>`) renders only once
|
||||||
// only once the sweep reaches it.
|
// the sweep reaches it, so it marks a settled banner.
|
||||||
|
const output = await smoke({
|
||||||
|
label: 'tui-agent boot',
|
||||||
|
actions: [{ waitFor: 'main-session-', send: '/exit\r' }],
|
||||||
|
})
|
||||||
expect(output).toContain('DEEPSEEK')
|
expect(output).toContain('DEEPSEEK')
|
||||||
expect(output).toContain('HARNESS')
|
expect(output).toContain('HARNESS')
|
||||||
expect(output).toContain('main-session-')
|
expect(output).toContain('main-session-')
|
||||||
@@ -244,8 +87,24 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
|||||||
expect(output).toContain('\u001B[?2004l')
|
expect(output).toContain('\u001B[?2004l')
|
||||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||||
|
|
||||||
it('streams a response, answers a user-question dialog, completes the tool round-trip, and exits cleanly', async () => {
|
it('switches models, streams a response, answers a user-question dialog, and exits cleanly', async () => {
|
||||||
const output = await runTuiLoaderSmoke({ config: scriptedConfigPath, scenario: 'conversation' })
|
const output = await smoke({
|
||||||
|
label: 'tui-agent conversation',
|
||||||
|
tempDirPrefix: 'tui-agent-conversation-',
|
||||||
|
configPath: scriptedConfigPath,
|
||||||
|
actions: [
|
||||||
|
...SELECT_PRO_MODEL,
|
||||||
|
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' },
|
||||||
|
{ waitFor: 'How should the scripted run proceed?', send: '\r' },
|
||||||
|
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '' },
|
||||||
|
// Session title: the first user message drives the first-message-llm
|
||||||
|
// provider's tool-less title call; the scripted adapter answers it, the
|
||||||
|
// accepted title lands in the log, and the TUI renders the terminal
|
||||||
|
// window title as `<session title> — <configured title>` via OSC 0.
|
||||||
|
// Gating /exit on it keeps the assertion race-free.
|
||||||
|
{ waitFor: 'scripted session title — DeepSeek Harness', send: '/exit\r' },
|
||||||
|
],
|
||||||
|
})
|
||||||
expect(output).toContain('I need one decision before I continue.')
|
expect(output).toContain('I need one decision before I continue.')
|
||||||
expect(output).toContain(String.raw`\x1b]2;MODEL_CONTROLLED\x07`)
|
expect(output).toContain(String.raw`\x1b]2;MODEL_CONTROLLED\x07`)
|
||||||
expect(output).toContain(String.raw`\x1b[999CMODEL_CURSOR`)
|
expect(output).toContain(String.raw`\x1b[999CMODEL_CURSOR`)
|
||||||
@@ -253,13 +112,7 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
|||||||
expect(output).not.toContain('\u001B]2;MODEL_CONTROLLED\u0007')
|
expect(output).not.toContain('\u001B]2;MODEL_CONTROLLED\u0007')
|
||||||
expect(output).not.toContain('\u001B[999CMODEL_CURSOR')
|
expect(output).not.toContain('\u001B[999CMODEL_CURSOR')
|
||||||
expect(output).not.toContain('\u009B31mMODEL_C1')
|
expect(output).not.toContain('\u009B31mMODEL_C1')
|
||||||
expect(output).toContain('How should the scripted run proceed?')
|
|
||||||
expect(output).toContain('Safe')
|
expect(output).toContain('Safe')
|
||||||
expect(output).toContain('Decision received. Scripted TUI run complete.')
|
|
||||||
// Session title: the first user message drives the first-message-llm
|
|
||||||
// provider's tool-less title call; the scripted adapter answers it, the
|
|
||||||
// accepted title lands in the log, and the TUI renders the terminal window
|
|
||||||
// title as `<session title> — <configured title>` via OSC 0.
|
|
||||||
expect(output).toContain('\u001B]0;scripted session title — DeepSeek Harness\u0007')
|
expect(output).toContain('\u001B]0;scripted session title — DeepSeek Harness\u0007')
|
||||||
expect(output).toContain('\u001B[?2004l')
|
expect(output).toContain('\u001B[?2004l')
|
||||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||||
@@ -270,20 +123,28 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
|||||||
// the local provider loads `scripted-skill` from the agents home, and the
|
// the local provider loads `scripted-skill` from the agents home, and the
|
||||||
// rendered `<skill name="…">` block reaches the model — proven by the
|
// rendered `<skill name="…">` block reaches the model — proven by the
|
||||||
// scripted adapter echoing the fixture's body marker only when it arrives.
|
// scripted adapter echoing the fixture's body marker only when it arrives.
|
||||||
const output = await runTuiLoaderSmoke({
|
const output = await smoke({
|
||||||
config: scriptedConfigPath,
|
label: 'tui-agent skill',
|
||||||
scenario: 'skill',
|
tempDirPrefix: 'tui-agent-skill-',
|
||||||
skillFiles: {
|
configPath: scriptedConfigPath,
|
||||||
'scripted-skill/SKILL.md': [
|
prepare: seedWorkspace({
|
||||||
'---',
|
skills: {
|
||||||
'name: scripted-skill',
|
'scripted-skill/SKILL.md': [
|
||||||
'description: Keyless PTY proof that the skill command loads a local skill into the conversation.',
|
'---',
|
||||||
'---',
|
'name: scripted-skill',
|
||||||
'',
|
'description: Keyless PTY proof that the skill command loads a local skill into the conversation.',
|
||||||
'SCRIPTED SKILL BODY MARKER',
|
'---',
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
'SCRIPTED SKILL BODY MARKER',
|
||||||
},
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actions: [
|
||||||
|
...SELECT_PRO_MODEL,
|
||||||
|
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: '/skill:scripted-skill\r' },
|
||||||
|
{ waitFor: 'Scripted skill body received.', send: '/exit\r' },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
expect(output).toContain('Scripted skill body received.')
|
expect(output).toContain('Scripted skill body received.')
|
||||||
expect(output).toContain('\u001B[?2004l')
|
expect(output).toContain('\u001B[?2004l')
|
||||||
@@ -292,23 +153,39 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
|||||||
it('boots the Code Mode overlay tree, renders its banner, and exits cleanly', async () => {
|
it('boots the Code Mode overlay tree, renders its banner, and exits cleanly', async () => {
|
||||||
// The overlay's only keyless composition proof: the include+patch tree,
|
// The overlay's only keyless composition proof: the include+patch tree,
|
||||||
// worker code runtime, and one-tool registry all mount before the banner.
|
// worker code runtime, and one-tool registry all mount before the banner.
|
||||||
const output = await runTuiLoaderSmoke({
|
const output = await smoke({
|
||||||
config: codeModeConfigPath,
|
label: 'tui-agent code mode',
|
||||||
bootMarker: 'TUI Code Mode ready.',
|
tempDirPrefix: 'tui-agent-code-mode-',
|
||||||
|
configPath: codeModeConfigPath,
|
||||||
|
actions: [{ waitFor: 'TUI Code Mode ready.', send: '/exit\r' }],
|
||||||
})
|
})
|
||||||
expect(output).toContain('TUI Code Mode ready.')
|
expect(output).toContain('TUI Code Mode ready.')
|
||||||
expect(output).toContain('\u001B[?2004l')
|
expect(output).toContain('\u001B[?2004l')
|
||||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||||
|
|
||||||
it('prints a config-resume failure and exits instead of leaving a blank terminal', async () => {
|
it('prints a config-resume failure and exits instead of leaving a blank terminal', async () => {
|
||||||
const output = await runTuiLoaderSmoke({ resumeSessionId: 'missing-session', scenario: 'resume-failure' })
|
const output = await smoke({
|
||||||
|
label: 'tui-agent resume failure',
|
||||||
|
tempDirPrefix: 'tui-agent-resume-',
|
||||||
|
env: {
|
||||||
|
DEEPSEEK_API_KEY: 'keyless-tui-no-call',
|
||||||
|
RESUME_SESSION_ID: 'missing-session',
|
||||||
|
},
|
||||||
|
expectedExitCode: 1,
|
||||||
|
})
|
||||||
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
|
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
|
||||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
||||||
it('boots the shipped default config with no arguments and no personal overlay', async () => {
|
it('boots the shipped default config with no arguments and no personal overlay', async () => {
|
||||||
const output = await runTuiLoaderSmoke({ srcBin: dshBinScript, configArgs: [] })
|
const output = await smoke({
|
||||||
|
label: 'dsh default boot',
|
||||||
|
tempDirPrefix: 'dsh-default-boot-',
|
||||||
|
binScript: dshBinScript,
|
||||||
|
configArgs: [],
|
||||||
|
actions: [{ waitFor: 'main-session-', send: '/exit\r' }],
|
||||||
|
})
|
||||||
expect(output).toContain('DEEPSEEK')
|
expect(output).toContain('DEEPSEEK')
|
||||||
expect(output).toContain('main-session-')
|
expect(output).toContain('main-session-')
|
||||||
expect(output).not.toContain('╭')
|
expect(output).not.toContain('╭')
|
||||||
@@ -320,45 +197,55 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
|||||||
// The whole personal-config chain in one boot: the personal .env supplies
|
// The whole personal-config chain in one boot: the personal .env supplies
|
||||||
// the variable, config.yaml patches the tui-agent entry with a `!!js`
|
// the variable, config.yaml patches the tui-agent entry with a `!!js`
|
||||||
// reference to it, and the banner renders the patched welcome verbatim.
|
// reference to it, and the banner renders the patched welcome verbatim.
|
||||||
const output = await runTuiLoaderSmoke({
|
const output = await smoke({
|
||||||
srcBin: dshBinScript,
|
label: 'dsh personal overlay',
|
||||||
|
tempDirPrefix: 'dsh-personal-overlay-',
|
||||||
|
binScript: dshBinScript,
|
||||||
configArgs: [],
|
configArgs: [],
|
||||||
bootMarker: 'PERSONAL OVERLAY READY.',
|
prepare: seedWorkspace({
|
||||||
personalFiles: {
|
personal: {
|
||||||
'.env': 'DSH_PERSONAL_WELCOME=PERSONAL OVERLAY READY.\n',
|
'.env': 'DSH_PERSONAL_WELCOME=PERSONAL OVERLAY READY.\n',
|
||||||
'config.yaml': [
|
'config.yaml': [
|
||||||
'- id: tui-agent',
|
'- id: tui-agent',
|
||||||
" name: '@deepseek-ai/dsh-tui-demo'",
|
" name: '@deepseek-ai/dsh-tui-demo'",
|
||||||
' config:',
|
' config:',
|
||||||
' provider: deepseek',
|
' provider: deepseek',
|
||||||
' model: deepseek-v4-flash',
|
' model: deepseek-v4-flash',
|
||||||
' workspaceContext: false',
|
' workspaceContext: false',
|
||||||
' welcome: !!js process.env.DSH_PERSONAL_WELCOME',
|
' welcome: !!js process.env.DSH_PERSONAL_WELCOME',
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
actions: [{ waitFor: 'PERSONAL OVERLAY READY.', send: '/exit\r' }],
|
||||||
})
|
})
|
||||||
expect(output).toContain('PERSONAL OVERLAY READY.')
|
expect(output).toContain('PERSONAL OVERLAY READY.')
|
||||||
expect(output).toContain('\u001B[?2004l')
|
expect(output).toContain('\u001B[?2004l')
|
||||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||||
|
|
||||||
it('fails loud instead of booting when the personal config.yaml is invalid', async () => {
|
it('fails loud instead of booting when the personal config.yaml is invalid', async () => {
|
||||||
await expect(runTuiLoaderSmoke({
|
const output = await smoke({
|
||||||
srcBin: dshBinScript,
|
label: 'dsh invalid personal config',
|
||||||
|
tempDirPrefix: 'dsh-invalid-personal-',
|
||||||
|
binScript: dshBinScript,
|
||||||
configArgs: [],
|
configArgs: [],
|
||||||
personalFiles: { 'config.yaml': 'id: not-a-list\n' },
|
prepare: seedWorkspace({ personal: { 'config.yaml': 'id: not-a-list\n' } }),
|
||||||
})).rejects.toThrow('must be a top-level YAML array of loader patch entries')
|
expectedExitCode: 1,
|
||||||
|
})
|
||||||
|
expect(output).toContain('must be a top-level YAML array of loader patch entries')
|
||||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||||
|
|
||||||
it('routes the --resume flag into the config resume intake, failing loud on a missing id', async () => {
|
it('routes the --resume flag into the config resume intake, failing loud on a missing id', async () => {
|
||||||
// The flag path end to end: apps/cli parses `--resume missing-session` and
|
// The flag path end to end: apps/cli parses `--resume missing-session` and
|
||||||
// sets RESUME_SESSION_ID (the PTY driver does NOT here), the shipped
|
// sets RESUME_SESSION_ID, the shipped config's `!!js` reads it, and the
|
||||||
// config's `!!js` reads it, and the resume fails loud — proving the printed
|
// resume fails loud — proving the printed `dsh --resume <id>` hint reaches
|
||||||
// `dsh --resume <id>` hint reaches the same intake as the env var.
|
// the same intake as the env var.
|
||||||
const output = await runTuiLoaderSmoke({
|
const output = await smoke({
|
||||||
srcBin: dshBinScript,
|
label: 'dsh resume flag failure',
|
||||||
|
tempDirPrefix: 'dsh-resume-flag-',
|
||||||
|
binScript: dshBinScript,
|
||||||
configArgs: ['--resume', 'missing-session'],
|
configArgs: ['--resume', 'missing-session'],
|
||||||
scenario: 'resume-failure',
|
expectedExitCode: 1,
|
||||||
})
|
})
|
||||||
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
|
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
|
||||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||||
@@ -368,10 +255,17 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
|||||||
// this test file sits an equal depth under the same root, so the same hop applies.
|
// this test file sits an equal depth under the same root, so the same hop applies.
|
||||||
const sourceRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
const sourceRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
||||||
let loggedSystem = ''
|
let loggedSystem = ''
|
||||||
await runTuiLoaderSmoke({
|
await smoke({
|
||||||
srcBin: dshBinScript,
|
label: 'dsh source-path prompt',
|
||||||
|
tempDirPrefix: 'dsh-source-path-',
|
||||||
|
binScript: dshBinScript,
|
||||||
configArgs: [scriptedConfigPath],
|
configArgs: [scriptedConfigPath],
|
||||||
scenario: 'conversation',
|
actions: [
|
||||||
|
...SELECT_PRO_MODEL,
|
||||||
|
{ waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' },
|
||||||
|
{ waitFor: 'How should the scripted run proceed?', send: '\r' },
|
||||||
|
{ waitFor: 'Decision received. Scripted TUI run complete.', send: '/exit\r' },
|
||||||
|
],
|
||||||
inspect: async (cwd) => { loggedSystem = await readLoggedSystemPrompt(cwd) },
|
inspect: async (cwd) => { loggedSystem = await readLoggedSystemPrompt(cwd) },
|
||||||
})
|
})
|
||||||
expect(loggedSystem).toContain(`Your own source code is the checkout at ${sourceRoot}; you can read it there to learn how dsh works and how to extend it.`)
|
expect(loggedSystem).toContain(`Your own source code is the checkout at ${sourceRoot}; you can read it there to learn how dsh works and how to extend it.`)
|
||||||
|
|||||||
@@ -278,8 +278,8 @@ describe('workspace context instruction discovery', () => {
|
|||||||
'$DSH_HOME/AGENTS.md',
|
'$DSH_HOME/AGENTS.md',
|
||||||
'AGENTS.md',
|
'AGENTS.md',
|
||||||
'CLAUDE.md',
|
'CLAUDE.md',
|
||||||
'packages/CLAUDE.md',
|
join('packages', 'CLAUDE.md'),
|
||||||
'packages/app/AGENTS.md',
|
join('packages', 'app', 'AGENTS.md'),
|
||||||
])
|
])
|
||||||
expect(files.map(file => file.absolutePath)).toContain(join(root, 'CLAUDE.md'))
|
expect(files.map(file => file.absolutePath)).toContain(join(root, 'CLAUDE.md'))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -304,8 +304,8 @@ describe('workspace context instruction discovery', () => {
|
|||||||
expect(files.map(file => file.displayPath)).toEqual([
|
expect(files.map(file => file.displayPath)).toEqual([
|
||||||
'AGENTS.md',
|
'AGENTS.md',
|
||||||
'AGENTS.local.md',
|
'AGENTS.local.md',
|
||||||
'pkg/CLAUDE.md',
|
join('pkg', 'CLAUDE.md'),
|
||||||
'pkg/CLAUDE.local.md',
|
join('pkg', 'CLAUDE.local.md'),
|
||||||
])
|
])
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
@@ -855,7 +855,7 @@ describe('workspace context request injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('no-fs-post-execute'),
|
callId: CallId('no-fs-post-execute'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/file.txt' },
|
arguments: { file_path: join('pkg', 'file.txt') },
|
||||||
agent: stubAgent('/virtual/repo'),
|
agent: stubAgent('/virtual/repo'),
|
||||||
}), {
|
}), {
|
||||||
isError: false,
|
isError: false,
|
||||||
@@ -891,7 +891,7 @@ describe('workspace context request injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-blocked-post-execute'),
|
callId: CallId('read-blocked-post-execute'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/file.txt' },
|
arguments: { file_path: join('pkg', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
const result = {
|
const result = {
|
||||||
@@ -989,7 +989,7 @@ describe('workspace context request injection', () => {
|
|||||||
await composeBaselinePrefix(ctx, agent)
|
await composeBaselinePrefix(ctx, agent)
|
||||||
|
|
||||||
expect(derivedText(agent)).toContain('omitted AGENTS.md')
|
expect(derivedText(agent)).toContain('omitted AGENTS.md')
|
||||||
expect(derivedText(agent)).toContain('Instructions from: pkg/AGENTS.md\n\npackage rule')
|
expect(derivedText(agent)).toContain(`Instructions from: ${join('pkg', 'AGENTS.md')}\n\npackage rule`)
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
await rm(home, { recursive: true, force: true })
|
await rm(home, { recursive: true, force: true })
|
||||||
@@ -1479,7 +1479,7 @@ describe('workspace context request injection', () => {
|
|||||||
await composeBaselinePrefix(ctx, agent)
|
await composeBaselinePrefix(ctx, agent)
|
||||||
|
|
||||||
expect(derivedText(agent)).toContain('Instructions from: AGENTS.md\n\nroot schema default rule')
|
expect(derivedText(agent)).toContain('Instructions from: AGENTS.md\n\nroot schema default rule')
|
||||||
expect(derivedText(agent)).toContain('Instructions from: child/AGENTS.md\n\nchild schema default rule')
|
expect(derivedText(agent)).toContain(`Instructions from: ${join('child', 'AGENTS.md')}\n\nchild schema default rule`)
|
||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
@@ -1680,7 +1680,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
{ type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('abort-after-read'), name: 'abort_step', arguments: '{}' } },
|
{ type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('abort-after-read'), name: 'abort_step', arguments: '{}' } },
|
||||||
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
||||||
] satisfies StreamChunk[],
|
] satisfies StreamChunk[],
|
||||||
toolCallResponse('read-after-abort', 'read', { file_path: 'pkg/deep/file.txt' }),
|
toolCallResponse('read-after-abort', 'read', { file_path: join('pkg', 'deep', 'file.txt') }),
|
||||||
textResponse('done'),
|
textResponse('done'),
|
||||||
])
|
])
|
||||||
await ctx.plugin(LlmService)
|
await ctx.plugin(LlmService)
|
||||||
@@ -1757,7 +1757,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
const exec = stubToolExecution({
|
const exec = stubToolExecution({
|
||||||
callId: CallId('cancelled-dynamic-read'),
|
callId: CallId('cancelled-dynamic-read'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/file.txt' },
|
arguments: { file_path: join('pkg', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
@@ -1792,7 +1792,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-nested'),
|
callId: CallId('read-nested'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1804,7 +1804,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
changes: [{
|
changes: [{
|
||||||
action: 'set',
|
action: 'set',
|
||||||
scope: sk('pkg', 'AGENTS.md'),
|
scope: sk('pkg', 'AGENTS.md'),
|
||||||
path: 'pkg/AGENTS.md',
|
path: join('pkg', 'AGENTS.md'),
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
const meta = workspaceContextOf(result)?.meta
|
const meta = workspaceContextOf(result)?.meta
|
||||||
@@ -1852,16 +1852,16 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-configured-nested-candidate'),
|
callId: CallId('read-configured-nested-candidate'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
const text = blocksText(workspaceContextOf(result)?.content)
|
const text = blocksText(workspaceContextOf(result)?.content)
|
||||||
expect(text).toContain('Additional instructions from: pkg/CLAUDE.local.md')
|
expect(text).toContain(`Additional instructions from: ${join('pkg', 'CLAUDE.local.md')}`)
|
||||||
expect(text).toContain('local package rule')
|
expect(text).toContain('local package rule')
|
||||||
expect(text).toContain('Additional instructions from: pkg/AGENTS.md')
|
expect(text).toContain(`Additional instructions from: ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(text).toContain('native package rule')
|
expect(text).toContain('native package rule')
|
||||||
expect(text.indexOf('pkg/CLAUDE.local.md')).toBeLessThan(text.indexOf('pkg/AGENTS.md'))
|
expect(text.indexOf(join('pkg', 'CLAUDE.local.md'))).toBeLessThan(text.indexOf(join('pkg', 'AGENTS.md')))
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
await rm(home, { recursive: true, force: true })
|
await rm(home, { recursive: true, force: true })
|
||||||
@@ -1884,7 +1884,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-nested-overlay'),
|
callId: CallId('read-nested-overlay'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1893,13 +1893,13 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
? meta.changes
|
? meta.changes
|
||||||
: []
|
: []
|
||||||
expect(changes).toEqual(expect.arrayContaining([
|
expect(changes).toEqual(expect.arrayContaining([
|
||||||
expect.objectContaining({ action: 'set', path: 'pkg/AGENTS.md' }),
|
expect.objectContaining({ action: 'set', path: join('pkg', 'AGENTS.md') }),
|
||||||
expect.objectContaining({ action: 'set', path: 'pkg/AGENTS.local.md' }),
|
expect.objectContaining({ action: 'set', path: join('pkg', 'AGENTS.local.md') }),
|
||||||
]))
|
]))
|
||||||
const text = blocksText(workspaceContextOf(result)?.content)
|
const text = blocksText(workspaceContextOf(result)?.content)
|
||||||
expect(text).toContain('Additional instructions from: pkg/AGENTS.md')
|
expect(text).toContain(`Additional instructions from: ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(text).toContain('nested base rule')
|
expect(text).toContain('nested base rule')
|
||||||
expect(text).toContain('Additional instructions from: pkg/AGENTS.local.md')
|
expect(text).toContain(`Additional instructions from: ${join('pkg', 'AGENTS.local.md')}`)
|
||||||
expect(text).toContain('nested local rule')
|
expect(text).toContain('nested local rule')
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
@@ -1926,13 +1926,13 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-nested-overlay-disabled'),
|
callId: CallId('read-nested-overlay-disabled'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
const text = blocksText(workspaceContextOf(result)?.content)
|
const text = blocksText(workspaceContextOf(result)?.content)
|
||||||
expect(text).toContain('Additional instructions from: pkg/AGENTS.md')
|
expect(text).toContain(`Additional instructions from: ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(text).not.toContain('pkg/AGENTS.local.md')
|
expect(text).not.toContain(join('pkg', 'AGENTS.local.md'))
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
await rm(home, { recursive: true, force: true })
|
await rm(home, { recursive: true, force: true })
|
||||||
@@ -1954,14 +1954,14 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-nested-1'),
|
callId: CallId('read-nested-1'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
const second = await ctx.tools.execute({
|
const second = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-nested-2'),
|
callId: CallId('read-nested-2'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1992,12 +1992,12 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-version-fast-path'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-version-fast-path'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
const second = await ctx.tools.execute({
|
const second = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-with-version-fast-path'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-with-version-fast-path'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(first.additionalContexts).toBeDefined()
|
expect(first.additionalContexts).toBeDefined()
|
||||||
@@ -2029,17 +2029,17 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-same-digest-version-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-same-digest-version-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
fs.entries.set(instructionPath, { type: 'file', content: 'same package rule', version: FsVersion('revision-2') })
|
fs.entries.set(instructionPath, { type: 'file', content: 'same package rule', version: FsVersion('revision-2') })
|
||||||
const afterVersionChange = await ctx.tools.execute({
|
const afterVersionChange = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-same-digest-version-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-same-digest-version-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
const afterRefresh = await ctx.tools.execute({
|
const afterRefresh = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-version-cache-refresh'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-version-cache-refresh'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(afterVersionChange.additionalContexts).toBeUndefined()
|
expect(afterVersionChange.additionalContexts).toBeUndefined()
|
||||||
@@ -2070,11 +2070,11 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-from-first-session'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent: stubAgent(root),
|
callId: CallId('read-from-first-session'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
const second = await ctx.tools.execute({
|
const second = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-from-second-session'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent: stubAgent(root),
|
callId: CallId('read-from-second-session'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(first.additionalContexts).toBeDefined()
|
expect(first.additionalContexts).toBeDefined()
|
||||||
@@ -2100,18 +2100,18 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
await write(join(root, 'pkg/AGENTS.md'), 'new package rule with more detail')
|
await write(join(root, 'pkg/AGENTS.md'), 'new package rule with more detail')
|
||||||
const changed = await ctx.tools.execute({
|
const changed = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(changed)?.meta).toMatchObject({
|
expect(workspaceContextOf(changed)?.meta).toMatchObject({
|
||||||
kind: 'workspace-instructions',
|
kind: 'workspace-instructions',
|
||||||
changes: [{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(changed)?.content)).toBe([
|
expect(blocksText(workspaceContextOf(changed)?.content)).toBe([
|
||||||
'<system-reminder>',
|
'<system-reminder>',
|
||||||
@@ -2142,7 +2142,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-both-siblings'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-both-siblings'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
const firstText = blocksText(workspaceContextOf(first)?.content)
|
const firstText = blocksText(workspaceContextOf(first)?.content)
|
||||||
expect(firstText).toContain('native package rule')
|
expect(firstText).toContain('native package rule')
|
||||||
@@ -2151,14 +2151,14 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
await rm(join(root, 'pkg/AGENTS.md'))
|
await rm(join(root, 'pkg/AGENTS.md'))
|
||||||
const removed = await ctx.tools.execute({
|
const removed = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-one-sibling-removed'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-one-sibling-removed'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Removing one candidate only removes its own scope; the sibling scope is untouched.
|
// Removing one candidate only removes its own scope; the sibling scope is untouched.
|
||||||
expect(workspaceContextOf(removed)?.meta).toMatchObject({
|
expect(workspaceContextOf(removed)?.meta).toMatchObject({
|
||||||
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(removed)?.content)).toContain('Instructions removed: pkg/AGENTS.md')
|
expect(blocksText(workspaceContextOf(removed)?.content)).toContain(`Instructions removed: ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(blocksText(workspaceContextOf(removed)?.content)).not.toContain('sibling package rule')
|
expect(blocksText(workspaceContextOf(removed)?.content)).not.toContain('sibling package rule')
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
@@ -2180,16 +2180,16 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const result = await ctx.tools.execute({
|
const result = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-nested-dup-siblings'), name: 'read', arguments: { file_path: 'pkg/deep/file.txt' }, agent,
|
callId: CallId('read-nested-dup-siblings'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(result)?.meta).toMatchObject({
|
expect(workspaceContextOf(result)?.meta).toMatchObject({
|
||||||
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
const text = blocksText(workspaceContextOf(result)?.content)
|
const text = blocksText(workspaceContextOf(result)?.content)
|
||||||
expect(text.match(/nested rule/g)).toHaveLength(1)
|
expect(text.match(/nested rule/g)).toHaveLength(1)
|
||||||
expect(text).toContain('Additional instructions from: pkg/AGENTS.md')
|
expect(text).toContain(`Additional instructions from: ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(text).not.toContain('pkg/CLAUDE.md')
|
expect(text).not.toContain(join('pkg', 'CLAUDE.md'))
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
await rm(home, { recursive: true, force: true })
|
await rm(home, { recursive: true, force: true })
|
||||||
@@ -2210,7 +2210,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-dup-convergence'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-dup-convergence'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
const firstText = blocksText(workspaceContextOf(first)?.content)
|
const firstText = blocksText(workspaceContextOf(first)?.content)
|
||||||
expect(firstText).toContain('canonical nested rule')
|
expect(firstText).toContain('canonical nested rule')
|
||||||
@@ -2219,13 +2219,13 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
await write(join(root, 'pkg/CLAUDE.md'), 'canonical nested rule')
|
await write(join(root, 'pkg/CLAUDE.md'), 'canonical nested rule')
|
||||||
const converged = await ctx.tools.execute({
|
const converged = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-dup-convergence'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-dup-convergence'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(converged)?.meta).toMatchObject({
|
expect(workspaceContextOf(converged)?.meta).toMatchObject({
|
||||||
changes: [{ action: 'remove', scope: sk('pkg', 'CLAUDE.md'), path: 'pkg/CLAUDE.md' }],
|
changes: [{ action: 'remove', scope: sk('pkg', 'CLAUDE.md'), path: join('pkg', 'CLAUDE.md') }],
|
||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(converged)?.content)).toContain('Instructions removed: pkg/CLAUDE.md')
|
expect(blocksText(workspaceContextOf(converged)?.content)).toContain(`Instructions removed: ${join('pkg', 'CLAUDE.md')}`)
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
await rm(home, { recursive: true, force: true })
|
await rm(home, { recursive: true, force: true })
|
||||||
@@ -2246,25 +2246,25 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-earlier-converges'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-earlier-converges'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
// Only the earlier candidate changes; the sibling stays byte-identical but now duplicates it.
|
// Only the earlier candidate changes; the sibling stays byte-identical but now duplicates it.
|
||||||
await write(join(root, 'pkg/AGENTS.md'), 'secondary nested rule')
|
await write(join(root, 'pkg/AGENTS.md'), 'secondary nested rule')
|
||||||
const converged = await ctx.tools.execute({
|
const converged = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-earlier-converges'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-earlier-converges'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(converged)?.meta).toMatchObject({
|
expect(workspaceContextOf(converged)?.meta).toMatchObject({
|
||||||
changes: [
|
changes: [
|
||||||
{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' },
|
{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') },
|
||||||
{ action: 'remove', scope: sk('pkg', 'CLAUDE.md'), path: 'pkg/CLAUDE.md' },
|
{ action: 'remove', scope: sk('pkg', 'CLAUDE.md'), path: join('pkg', 'CLAUDE.md') },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
const text = blocksText(workspaceContextOf(converged)?.content)
|
const text = blocksText(workspaceContextOf(converged)?.content)
|
||||||
expect(text).toContain('Instructions removed: pkg/CLAUDE.md')
|
expect(text).toContain(`Instructions removed: ${join('pkg', 'CLAUDE.md')}`)
|
||||||
expect(text).toContain('Updated instructions from: pkg/AGENTS.md')
|
expect(text).toContain(`Updated instructions from: ${join('pkg', 'AGENTS.md')}`)
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
await rm(home, { recursive: true, force: true })
|
await rm(home, { recursive: true, force: true })
|
||||||
@@ -2284,19 +2284,19 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-remove'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-remove'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
await rm(join(root, 'pkg/AGENTS.md'))
|
await rm(join(root, 'pkg/AGENTS.md'))
|
||||||
const removed = await ctx.tools.execute({
|
const removed = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-remove'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-remove'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(removed)?.meta).toEqual({
|
expect(workspaceContextOf(removed)?.meta).toEqual({
|
||||||
kind: 'workspace-instructions',
|
kind: 'workspace-instructions',
|
||||||
version: 1,
|
version: 1,
|
||||||
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(removed)?.content)).toBe([
|
expect(blocksText(workspaceContextOf(removed)?.content)).toBe([
|
||||||
'<system-reminder>',
|
'<system-reminder>',
|
||||||
@@ -2324,7 +2324,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-symlink-dir'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-symlink-dir'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
expect(blocksText(workspaceContextOf(first)?.content)).toContain('package rule')
|
expect(blocksText(workspaceContextOf(first)?.content)).toContain('package rule')
|
||||||
@@ -2337,13 +2337,13 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
await symlink(join(root, 'pkg/elsewhere'), join(root, 'pkg/AGENTS.md'))
|
await symlink(join(root, 'pkg/elsewhere'), join(root, 'pkg/AGENTS.md'))
|
||||||
const removed = await ctx.tools.execute({
|
const removed = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-symlink-dir'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-symlink-dir'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(removed)?.meta).toMatchObject({
|
expect(workspaceContextOf(removed)?.meta).toMatchObject({
|
||||||
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(removed)?.content)).toContain('Instructions removed: pkg/AGENTS.md')
|
expect(blocksText(workspaceContextOf(removed)?.content)).toContain(`Instructions removed: ${join('pkg', 'AGENTS.md')}`)
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
await rm(home, { recursive: true, force: true })
|
await rm(home, { recursive: true, force: true })
|
||||||
@@ -2363,26 +2363,26 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
await rm(join(root, 'pkg/AGENTS.md'))
|
await rm(join(root, 'pkg/AGENTS.md'))
|
||||||
const removed = await ctx.tools.execute({
|
const removed = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-to-create-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-to-create-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, removed)
|
appendAdditionalContexts(agent, removed)
|
||||||
await write(join(root, 'pkg/AGENTS.md'), 'restored package rule')
|
await write(join(root, 'pkg/AGENTS.md'), 'restored package rule')
|
||||||
|
|
||||||
const restored = await ctx.tools.execute({
|
const restored = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-after-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(workspaceContextOf(restored)?.meta).toMatchObject({
|
expect(workspaceContextOf(restored)?.meta).toMatchObject({
|
||||||
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('Additional instructions from: pkg/AGENTS.md')
|
expect(blocksText(workspaceContextOf(restored)?.content)).toContain(`Additional instructions from: ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('restored package rule')
|
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('restored package rule')
|
||||||
} finally {
|
} finally {
|
||||||
await rm(root, { recursive: true, force: true })
|
await rm(root, { recursive: true, force: true })
|
||||||
@@ -2408,13 +2408,13 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-provider-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-before-provider-failure'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
fs.throwOnStat.add(join(root, 'pkg/AGENTS.md'))
|
fs.throwOnStat.add(join(root, 'pkg/AGENTS.md'))
|
||||||
const duringFailure = await ctx.tools.execute({
|
const duringFailure = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-during-provider-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('read-during-provider-failure'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(first.additionalContexts).toBeDefined()
|
expect(first.additionalContexts).toBeDefined()
|
||||||
@@ -2440,7 +2440,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-resume'),
|
callId: CallId('read-before-resume'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
@@ -2453,7 +2453,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-resume'),
|
callId: CallId('read-after-resume'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: resumed,
|
agent: resumed,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2477,7 +2477,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
const original = stubAgent(root)
|
const original = stubAgent(root)
|
||||||
const first = await ctx.tools.execute({
|
const first = await ctx.tools.execute({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-offline-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent: original,
|
callId: CallId('read-before-offline-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: original,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(original, first)
|
appendAdditionalContexts(original, first)
|
||||||
await write(join(root, 'pkg/AGENTS.md'), 'new nested rule after resume')
|
await write(join(root, 'pkg/AGENTS.md'), 'new nested rule after resume')
|
||||||
@@ -2487,7 +2487,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const update = resumed.session.events.findLast(event => event.type === 'context/message')
|
const update = resumed.session.events.findLast(event => event.type === 'context/message')
|
||||||
expect(update?.type === 'context/message' && update.data.meta).toMatchObject({
|
expect(update?.type === 'context/message' && update.data.meta).toMatchObject({
|
||||||
changes: [{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
expect(update?.type === 'context/message' && blocksText(update.data.content)).toContain('new nested rule after resume')
|
expect(update?.type === 'context/message' && blocksText(update.data.content)).toContain('new nested rule after resume')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2510,7 +2510,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-before-compact'),
|
callId: CallId('read-before-compact'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
const contextSeq = appendAdditionalContexts(agent, first)!
|
const contextSeq = appendAdditionalContexts(agent, first)!
|
||||||
@@ -2518,7 +2518,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-while-visible'),
|
callId: CallId('read-while-visible'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2534,7 +2534,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-compact'),
|
callId: CallId('read-after-compact'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2564,7 +2564,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-package'),
|
callId: CallId('read-package'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/file.txt' },
|
arguments: { file_path: join('pkg', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
@@ -2573,7 +2573,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-subtree'),
|
callId: CallId('read-subtree'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/sub/file.txt' },
|
arguments: { file_path: join('pkg', 'sub', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2601,7 +2601,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-subtree-omitting-parent'),
|
callId: CallId('read-subtree-omitting-parent'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/sub/file.txt' },
|
arguments: { file_path: join('pkg', 'sub', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
appendAdditionalContexts(agent, first)
|
appendAdditionalContexts(agent, first)
|
||||||
@@ -2610,13 +2610,13 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-parent-after-omit'),
|
callId: CallId('read-parent-after-omit'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/other.txt' },
|
arguments: { file_path: join('pkg', 'other.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
const firstText = blocksText(workspaceContextOf(first)?.content)
|
const firstText = blocksText(workspaceContextOf(first)?.content)
|
||||||
expect(firstText).toContain('omitted pkg/AGENTS.md')
|
expect(firstText).toContain(`omitted ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(firstText).not.toContain('## pkg/AGENTS.md')
|
expect(firstText).not.toContain(`## ${join('pkg', 'AGENTS.md')}`)
|
||||||
expect(firstText).toContain('subtree rule')
|
expect(firstText).toContain('subtree rule')
|
||||||
expect(blocksText(workspaceContextOf(second)?.content)).toContain('parent rule')
|
expect(blocksText(workspaceContextOf(second)?.content)).toContain('parent rule')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2646,9 +2646,9 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
version: 1,
|
version: 1,
|
||||||
changes: [
|
changes: [
|
||||||
null,
|
null,
|
||||||
{ action: 'unknown', scope: 'pkg', path: 'pkg/AGENTS.md' },
|
{ action: 'unknown', scope: 'pkg', path: join('pkg', 'AGENTS.md') },
|
||||||
{ action: 'set', scope: 'pkg', path: 42 },
|
{ action: 'set', scope: 'pkg', path: 42 },
|
||||||
{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md', digest: 42 },
|
{ action: 'set', scope: 'pkg', path: join('pkg', 'AGENTS.md'), digest: 42 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}, { surfaceOp: 'append' })
|
}, { surfaceOp: 'append' })
|
||||||
@@ -2663,7 +2663,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
meta: {
|
meta: {
|
||||||
kind: 'workspace-instructions',
|
kind: 'workspace-instructions',
|
||||||
version: 1,
|
version: 1,
|
||||||
changes: [{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md', digest: 'spoof' }],
|
changes: [{ action: 'set', scope: 'pkg', path: join('pkg', 'AGENTS.md'), digest: 'spoof' }],
|
||||||
},
|
},
|
||||||
}, { surfaceOp: 'append' })
|
}, { surfaceOp: 'append' })
|
||||||
|
|
||||||
@@ -2671,7 +2671,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-spoofed-state'),
|
callId: CallId('read-after-spoofed-state'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2736,13 +2736,13 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
|
|
||||||
const failedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
|
const failedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('provider-stat-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('provider-stat-failure'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
}), result, async () => ({ kind: 'accept' as const }))
|
}), result, async () => ({ kind: 'accept' as const }))
|
||||||
fs.throwOnStat.clear()
|
fs.throwOnStat.clear()
|
||||||
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'directory' })
|
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'directory' })
|
||||||
const mismatchedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
|
const mismatchedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
|
||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('provider-stat-mismatch'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
|
callId: CallId('provider-stat-mismatch'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent,
|
||||||
}), result, async () => ({ kind: 'accept' as const }))
|
}), result, async () => ({ kind: 'accept' as const }))
|
||||||
|
|
||||||
expect(failedStat).toEqual({ kind: 'accept' })
|
expect(failedStat).toEqual({ kind: 'accept' })
|
||||||
@@ -2770,7 +2770,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-with-unreadable-nested-instruction'),
|
callId: CallId('read-with-unreadable-nested-instruction'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2805,7 +2805,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-with-downstream'),
|
callId: CallId('read-with-downstream'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2814,7 +2814,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
expect(workspaceContextOf(result)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
|
expect(workspaceContextOf(result)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
|
||||||
expect(workspaceContextOf(result)?.meta).toMatchObject({
|
expect(workspaceContextOf(result)?.meta).toMatchObject({
|
||||||
kind: 'workspace-instructions',
|
kind: 'workspace-instructions',
|
||||||
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: 'pkg/AGENTS.md' }],
|
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
|
||||||
})
|
})
|
||||||
expect(blocksText(workspaceContextOf(result)?.content)).toContain('nested package rule')
|
expect(blocksText(workspaceContextOf(result)?.content)).toContain('nested package rule')
|
||||||
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')
|
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')
|
||||||
@@ -2850,7 +2850,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-blocked-downstream'),
|
callId: CallId('read-blocked-downstream'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2891,7 +2891,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('outer-block-first'),
|
callId: CallId('outer-block-first'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
shouldBlock = false
|
shouldBlock = false
|
||||||
@@ -2899,7 +2899,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('outer-block-retry'),
|
callId: CallId('outer-block-retry'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent,
|
agent,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2935,7 +2935,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId(`${exec.callId}:nested`),
|
callId: CallId(`${exec.callId}:nested`),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
...exec.agent === undefined ? {} : { agent: exec.agent },
|
...exec.agent === undefined ? {} : { agent: exec.agent },
|
||||||
parent: exec.token,
|
parent: exec.token,
|
||||||
...exec.signal === undefined ? {} : { signal: exec.signal },
|
...exec.signal === undefined ? {} : { signal: exec.signal },
|
||||||
@@ -3026,8 +3026,8 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
isError: false,
|
isError: false,
|
||||||
}
|
}
|
||||||
const cases = [
|
const cases = [
|
||||||
{ name: 'read', arguments: { file_path: 'pkg/deep/file.txt' }, agent: undefined },
|
{ name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent: undefined },
|
||||||
{ name: 'bash', arguments: { file_path: 'pkg/deep/file.txt' }, agent },
|
{ name: 'bash', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent },
|
||||||
{ name: 'read', arguments: null, agent },
|
{ name: 'read', arguments: null, agent },
|
||||||
{ name: 'read', arguments: {}, agent },
|
{ name: 'read', arguments: {}, agent },
|
||||||
{ name: 'read', arguments: { file_path: 1 }, agent },
|
{ name: 'read', arguments: { file_path: 1 }, agent },
|
||||||
@@ -3064,7 +3064,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-with-disabled-budget'),
|
callId: CallId('read-with-disabled-budget'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3089,7 +3089,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-missing'),
|
callId: CallId('read-missing'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/missing.txt' },
|
arguments: { file_path: join('pkg', 'missing.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3116,7 +3116,7 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
signal: testToolSignal,
|
signal: testToolSignal,
|
||||||
callId: CallId('read-after-dispose'),
|
callId: CallId('read-after-dispose'),
|
||||||
name: 'read',
|
name: 'read',
|
||||||
arguments: { file_path: 'pkg/deep/file.txt' },
|
arguments: { file_path: join('pkg', 'deep', 'file.txt') },
|
||||||
agent: stubAgent(root),
|
agent: stubAgent(root),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3159,7 +3159,7 @@ describe('workspace context pending state', () => {
|
|||||||
const [change] = commitPendingInstructionContexts(agent, [workspaceChangeContext('pkg', 'one')], pending)
|
const [change] = commitPendingInstructionContexts(agent, [workspaceChangeContext('pkg', 'one')], pending)
|
||||||
expect(change).toBeDefined()
|
expect(change).toBeDefined()
|
||||||
versions.set(agent.session, new Map([['pkg', {
|
versions.set(agent.session, new Map([['pkg', {
|
||||||
path: 'pkg/AGENTS.md', version: FsVersion('v1'), digest: 'one', trimmedDigest: 'one',
|
path: join('pkg', 'AGENTS.md'), version: FsVersion('v1'), digest: 'one', trimmedDigest: 'one',
|
||||||
}]]))
|
}]]))
|
||||||
|
|
||||||
const unrelated = agent.session.append('context/message', {
|
const unrelated = agent.session.append('context/message', {
|
||||||
@@ -3196,7 +3196,7 @@ describe('workspace context pending state', () => {
|
|||||||
agent.session.append('step/start', { turn: 1, step: 1 })
|
agent.session.append('step/start', { turn: 1, step: 1 })
|
||||||
commitPendingInstructionContexts(agent, [workspaceChangeContext('pkg', 'one')], pending)
|
commitPendingInstructionContexts(agent, [workspaceChangeContext('pkg', 'one')], pending)
|
||||||
versions.set(agent.session, new Map([['pkg', {
|
versions.set(agent.session, new Map([['pkg', {
|
||||||
path: 'pkg/AGENTS.md', version: FsVersion('v1'), digest: 'one', trimmedDigest: 'one',
|
path: join('pkg', 'AGENTS.md'), version: FsVersion('v1'), digest: 'one', trimmedDigest: 'one',
|
||||||
}]]))
|
}]]))
|
||||||
|
|
||||||
const ended = agent.session.append('step/end', { turn: 1, step: 1 })
|
const ended = agent.session.append('step/end', { turn: 1, step: 1 })
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface TuiHarnessOptions {
|
|||||||
beforeMount?: (session: Session) => void
|
beforeMount?: (session: Session) => void
|
||||||
cwd?: string | null
|
cwd?: string | null
|
||||||
formatCwd?: TuiRuntime['formatCwd']
|
formatCwd?: TuiRuntime['formatCwd']
|
||||||
/** Fake-agent creation options; auto-title resolves its target from `provider`/`model`. */
|
/** Fake-agent creation options (`provider`/`model` seed the model selector's initial target). */
|
||||||
agentOptions?: AgentOptions
|
agentOptions?: AgentOptions
|
||||||
contextWindow?: number
|
contextWindow?: number
|
||||||
contextTokens?: number
|
contextTokens?: number
|
||||||
@@ -94,8 +94,8 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
|||||||
} else {
|
} else {
|
||||||
await options.configureContext(ctx)
|
await options.configureContext(ctx)
|
||||||
}
|
}
|
||||||
// A configureContext may mount the real LlmService (e.g. the auto-title
|
// A configureContext may mount the real LlmService; only fill the
|
||||||
// suites); only fill the advisory-catalog stub when none was provided.
|
// advisory-catalog stub when none was provided.
|
||||||
if (ctx.get('llm') === undefined) {
|
if (ctx.get('llm') === undefined) {
|
||||||
ctx.provide('llm', {
|
ctx.provide('llm', {
|
||||||
listProviders() {
|
listProviders() {
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -188,6 +188,9 @@ importers:
|
|||||||
'@deepseek-ai/dsh-session-persistence-jsonl':
|
'@deepseek-ai/dsh-session-persistence-jsonl':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/session-persistence/session-persistence-jsonl
|
version: link:../packages/session-persistence/session-persistence-jsonl
|
||||||
|
'@deepseek-ai/dsh-session-title-first-message-llm':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/session-title/session-title-first-message-llm
|
||||||
'@deepseek-ai/dsh-spill-local':
|
'@deepseek-ai/dsh-spill-local':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/spill/spill-local
|
version: link:../packages/spill/spill-local
|
||||||
|
|||||||
Reference in New Issue
Block a user