fix(workspace-context): escape instruction metadata
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/support/acp-snapshot/README.md
|
||||
README.md: afbb23e2251932d41ac5d5d5b7d966f2750857fe
|
||||
README.zh.md: 67ebbff395881c811819bb9e3dcfa4faa4d39914
|
||||
README.md: 93998c20bed7a2542c23932bd659a64aec63a585
|
||||
README.zh.md: a355cf0f35b5e7bec41ab0d9063c932211a7200b
|
||||
|
||||
@@ -51,7 +51,7 @@ defineAcpSnapshotSuite({
|
||||
})
|
||||
```
|
||||
|
||||
A scenario booting a differently-composed tree sets its own `configPath` (an overlay whose basename still ends in `cordis.yml`, so the bin's replay swap finds the sibling `*cordis.snapshot.yml`) and, when that composition changes the request header, its own `headerClass` with its own pinning scenario — the acp-agent example's Code Mode and filesystem scenarios are templates. `workspaceParent` moves the generated cwd outside the platform temp area when temporary-directory grants are themselves under test; the harness still owns and removes only the generated child. Each pinning directory stores the normalized full prompt sequence in generated `system-prompt.expected.md` and the corresponding full tool-schema sequence in generated `tool-schemas.expected.json`; `session.jsonl` stores `"system":"{{system}}","tools":"{{tools}}"` while retaining config, reason, and any model-visible prefix. A pin with legitimate mid-run header changes declares `expectedHeaderChanges`, which fixes the length of both sidecar sequences.
|
||||
A scenario booting a differently-composed tree sets its own `configPath` (an overlay whose basename still ends in `cordis.yml`, so the bin's replay swap finds the sibling `*cordis.snapshot.yml`) and, when that composition changes the request header, its own `headerClass` with its own pinning scenario — the acp-agent example's Code Mode and filesystem scenarios are templates. `workspaceParent` moves the generated cwd outside the platform temp area when temporary-directory grants are themselves under test; the harness still owns and removes only the generated child. A scenario's committed `workspace/` is copied into that child first, then `prepareWorkspace` runs against the generated cwd before the agent starts. Reserve this hook for fixtures Git cannot represent portably, keep ordinary seeds in `workspace/`, and pair it with `posixOnly` when the generated paths are invalid on Windows. Each pinning directory stores the normalized full prompt sequence in generated `system-prompt.expected.md` and the corresponding full tool-schema sequence in generated `tool-schemas.expected.json`; `session.jsonl` stores `"system":"{{system}}","tools":"{{tools}}"` while retaining config, reason, and any model-visible prefix. A pin with legitimate mid-run header changes declares `expectedHeaderChanges`, which fixes the length of both sidecar sequences.
|
||||
|
||||
Every scenario compares `stdout.expected.jsonl` with cwd-rooted separators canonicalized to `/`. On Windows, `pinsNativeWindowsStdout` additionally compares the complete `stdout.expected.windows.jsonl` after the shared expected output and requires that sidecar exactly when enabled. A scenario whose driven behavior needs POSIX process semantics (e.g. cancelling a live bash call kills a detached process group) declares `posixOnly`, which skips its run test on Windows while the fixture guards keep covering its committed files everywhere.
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ defineAcpSnapshotSuite({
|
||||
})
|
||||
```
|
||||
|
||||
启动不同组合树的场景会设置自己的 `configPath`(一个 basename 仍以 `cordis.yml` 结尾的 overlay,使 bin 的回放交换可找到同级 `*cordis.snapshot.yml`);当该组合改变请求 header 时,还会设置自己的 `headerClass` 和 pin 场景,acp-agent 示例的 Code Mode 与文件系统场景是模板。当临时目录授权自身待测时,`workspaceParent` 将生成 cwd 移出平台临时区域;harness 仍只拥有并移除生成的子级。每个 pin 目录将规范化的完整提示词序列存入生成的 `system-prompt.expected.md`,将对应完整工具 schema 序列存入生成的 `tool-schemas.expected.json`;`session.jsonl` 存储 `"system":"{{system}}","tools":"{{tools}}"`,同时保留配置、原因和任何模型可见前缀。具有合法运行中 header 变更的 pin 声明 `expectedHeaderChanges`,用于固定两个 sidecar 序列的长度。
|
||||
启动不同组合树的场景会设置自己的 `configPath`(一个 basename 仍以 `cordis.yml` 结尾的 overlay,使 bin 的回放交换可找到同级 `*cordis.snapshot.yml`);当该组合改变请求 header 时,还会设置自己的 `headerClass` 和 pin 场景,acp-agent 示例的 Code Mode 与文件系统场景是模板。当临时目录授权自身待测时,`workspaceParent` 将生成 cwd 移出平台临时区域;harness 仍只拥有并移除生成的子级。场景签入的 `workspace/` 会先复制到该子级,随后 `prepareWorkspace` 在 agent 启动前针对生成 cwd 运行。此 hook 仅用于 Git 无法跨平台表示的 fixture;普通种子应留在 `workspace/` 中,而生成路径在 Windows 上无效时还必须搭配 `posixOnly`。每个 pin 目录将规范化的完整提示词序列存入生成的 `system-prompt.expected.md`,将对应完整工具 schema 序列存入生成的 `tool-schemas.expected.json`;`session.jsonl` 存储 `"system":"{{system}}","tools":"{{tools}}"`,同时保留配置、原因和任何模型可见前缀。具有合法运行中 header 变更的 pin 声明 `expectedHeaderChanges`,用于固定两个 sidecar 序列的长度。
|
||||
|
||||
每个场景都比较 `stdout.expected.jsonl`,其中以 cwd 为根的分隔符规范化为 `/`。在 Windows 上,`pinsNativeWindowsStdout` 还会在共享预期输出之后比较完整 `stdout.expected.windows.jsonl`,并在启用时精确要求该 sidecar。驱动行为需要 POSIX 进程语义的场景(例如取消实时 bash 调用会终止脱离进程组)声明 `posixOnly`,在 Windows 上跳过运行测试,但 fixture 保护仍在所有平台覆盖其已提交文件。
|
||||
|
||||
|
||||
@@ -155,6 +155,13 @@ export interface RunOptions {
|
||||
* start from an empty workspace.
|
||||
*/
|
||||
workspaceDir?: string
|
||||
/**
|
||||
* Optional final workspace preparation, run after {@link workspaceDir} is
|
||||
* copied and before the agent starts. This is for fixtures that cannot be
|
||||
* represented portably in Git (for example, a POSIX-only filename that is
|
||||
* invalid on Windows); ordinary seeded files belong in `workspaceDir`.
|
||||
*/
|
||||
prepareWorkspace?: (cwd: string) => void | Promise<void>
|
||||
/**
|
||||
* Parent directory for the generated session cwd. Defaults to
|
||||
* `os.tmpdir()`. A scenario that must distinguish its workspace from the
|
||||
@@ -221,6 +228,7 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise
|
||||
if (opts.workspaceDir !== undefined && existsSync(opts.workspaceDir)) {
|
||||
await cp(opts.workspaceDir, cwd, { recursive: true })
|
||||
}
|
||||
await opts.prepareWorkspace?.(cwd)
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...opts.env,
|
||||
DSH_SNAPSHOT: opts.mode,
|
||||
|
||||
@@ -114,6 +114,12 @@ export interface Scenario {
|
||||
* test and the scenario needs an independent project location.
|
||||
*/
|
||||
workspaceParent?: string
|
||||
/**
|
||||
* Optional final workspace preparation after the committed fixture is
|
||||
* copied. Reserve this for paths that Git cannot represent portably; normal
|
||||
* scenario files belong under the scenario's `workspace/` directory.
|
||||
*/
|
||||
prepareWorkspace?: (cwd: string) => void | Promise<void>
|
||||
/**
|
||||
* Whether Windows additionally compares stdout with native separators against
|
||||
* `stdout.expected.windows.jsonl`. The shared canonical stdout expected output is still
|
||||
@@ -849,6 +855,7 @@ export function defineAcpSnapshotSuite(options: SnapshotSuiteOptions): void {
|
||||
// replays from its own script. In RECORD they are harvested, not read.
|
||||
...!RECORDING && childFixtureFiles.length > 0 ? { childFiles: childFixtureFiles.map(file => join(dir, file)) } : {},
|
||||
...existsSync(workspaceDir) ? { workspaceDir } : {},
|
||||
...scenario.prepareWorkspace !== undefined ? { prepareWorkspace: scenario.prepareWorkspace } : {},
|
||||
...scenario.workspaceParent !== undefined ? { workspaceParent: scenario.workspaceParent } : {},
|
||||
// A scenario booting an overlay tree passes its own live config; the
|
||||
// bin's replay swap derives the sibling `*cordis.snapshot.yml` from it.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { once } from 'node:events'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join, relative, sep } from 'node:path'
|
||||
@@ -468,6 +468,30 @@ describe('runScenario', () => {
|
||||
expect(result.rawStdout).toContain('workspace:seeded.txt')
|
||||
})
|
||||
|
||||
it('prepares the generated workspace after copying committed fixtures', { timeout: 20_000 }, async () => {
|
||||
const { dir, fixtureFile } = await scenario({ echoWorkspace: true })
|
||||
const workspaceDir = join(dir, 'workspace')
|
||||
const { mkdir } = await import('node:fs/promises')
|
||||
await mkdir(workspaceDir, { recursive: true })
|
||||
await writeFile(join(workspaceDir, 'committed.txt'), 'committed')
|
||||
|
||||
const result = await runScenario(
|
||||
{ steps: [...boot, { op: 'prompt', text: 'ls' }] },
|
||||
{
|
||||
agent: AGENT,
|
||||
mode: 'replay',
|
||||
fixtureFile,
|
||||
workspaceDir,
|
||||
prepareWorkspace: async (cwd) => {
|
||||
expect(await readFile(join(cwd, 'committed.txt'), 'utf8')).toBe('committed')
|
||||
await writeFile(join(cwd, 'runtime.txt'), 'runtime')
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.rawStdout).toContain('workspace:committed.txt,runtime.txt')
|
||||
})
|
||||
|
||||
it('creates the generated workspace under an explicit parent', { timeout: 20_000 }, async () => {
|
||||
const { fixtureFile } = await scenario({})
|
||||
const workspaceParent = await mkdtemp(join(tmpdir(), 'acp-snap-parent-'))
|
||||
|
||||
@@ -65,6 +65,9 @@ const REPLAY_SCENARIOS: Scenario[] = [
|
||||
env: { DSH_PERMISSION_MODE: 'never' },
|
||||
configPath: AGENT.configPath,
|
||||
workspaceParent: tmpdir(),
|
||||
prepareWorkspace: (cwd) => {
|
||||
writeFileSync(join(cwd, 'seed.txt'), 'prepared at runtime')
|
||||
},
|
||||
},
|
||||
{ name: 'no-model', hasModelTurn: false, recorded: false, headerClass: 'main' },
|
||||
{ name: 'blocked-log', hasModelTurn: false, comparesLog: true, recorded: false, headerClass: 'main' },
|
||||
|
||||
Reference in New Issue
Block a user