refactor(fs): prune write-only fields and the dead routing knob from the seam

The fs seam split left four pieces of pre-split surface populated on
every call and read by nobody:

- STREAM_MIN_SIZE + FsIoInternals.streamMinSize in dsh-fs-local: the
  backend has no read routing (readWholeText/streamWholeText are
  separate primitives the caller picks), and the real 10 MiB routing
  constant lives in dsh-tool-fs's read tool. Delete the dead mirror and
  the knob whose JSDoc claimed an override that did not exist; the
  remaining FsIoInternals knobs stay (the atomic-write tests use them).
- FsTarget.inputPath: a "diagnostics only" field every backend and test
  fake had to fabricate, with zero production readers (policy and error
  messages use targetKey/displayPath). listDir gave children the bare
  entry name, which was nobody's input.
- FsEditOutcome.replacements/.replaceAll: replacements had no reader
  (the single-match policy is enforced by the FS_AMBIGUOUS_EDIT /
  FS_EDIT_NOT_FOUND throws, whose message keeps the internal count);
  replaceAll only echoed the replace_all argument back to
  formatEditOutput, which now takes it from the parsed args. The
  outcome shrinks to { version, before, after }, parallel to
  FsWriteOutcome's backend-discovered fields. Emitted text is unchanged
  for both branches (no snapshot churn).
- FileReadOutcome.limit/.version: formatReadOutput renders
  offset/lines/totalLines/truncatedByBytes only, and the fs/observed
  emit uses info.version directly.

Backends shed four fabrication obligations and gain none. Doc pastes
(core-data-structures/filesystem.md), the dsh-fs README resolve row,
and the test fakes shrink with the types. RFC moved to
implemented/simplification and amended to the shipped shape
(FsEditSpec -> FsEditRequest name fix; manifest rows needed no change).
This commit is contained in:
Tianyi Cui
2026-07-04 15:37:43 +08:00
parent 226a8b5e4c
commit e64623ebfd
14 changed files with 34 additions and 67 deletions

View File

@@ -12,7 +12,6 @@ Every operation resolves a user-supplied path to an opaque backend target first.
```ts type-equiv
interface FsTarget {
inputPath: string
targetKey: FsTargetKey
displayPath: string
}
@@ -81,8 +80,6 @@ interface FsEditRequest {
```ts type-equiv
interface FsEditOutcome {
replacements: number
replaceAll: boolean
version: FsVersion
before: string
after: string
@@ -109,16 +106,14 @@ interface FsPolicyExec {
## Read outcome (consumer / read rendering)
A text read is bounded by line window, byte cap, and backend limits. The outcome the model-facing `read` tool renders carries the file's version at read time; there is no `full`/`partial` view — authorization is freshness-based, so any windowed read can authorize a later write/edit when the file is unchanged. Read windowing and this outcome shape live in `dsh-tool-fs` (the executor that owns the read), not in the policy plugin.
A text read is bounded by line window, byte cap, and backend limits. The outcome the model-facing `read` tool renders is purely presentational; there is no `full`/`partial` view — authorization is freshness-based (the tool emits `fs/observed` with the stat's version directly), so any windowed read can authorize a later write/edit when the file is unchanged. Read windowing and this outcome shape live in `dsh-tool-fs` (the executor that owns the read), not in the policy plugin.
```ts type-equiv
interface FileReadOutcome {
offset: number
limit: number
lines: FileTextLine[]
totalLines: number
truncatedByBytes?: true
version: FsVersion
}
```

View File

@@ -58,7 +58,6 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
| [Fold the stdio UI helper into the stdio app](proposed/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 |
| [Prune dead core-spine surface — `SurfaceManager.invalidate()`, the loop-internal exports, `ToolExecutionResult.callId`](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 |
| [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](proposed/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 |
| [Prune write-only fields and a dead routing knob from the fs seam](proposed/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 |
| [Remove the `agent/steering` mirror emit](proposed/simplification/2026-07-04-remove-agent-steering-mirror.md) | 2026-07-04 |
| [Share the app bins' boot glue instead of maintaining twin copies](proposed/simplification/2026-07-04-share-app-bin-boot-glue.md) | 2026-07-04 |
| [Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics](proposed/simplification/2026-07-04-tighten-hook-protocol-contract.md) | 2026-07-04 |
@@ -120,6 +119,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
| [Stop mirroring durable boundaries as agent events](implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md) | 2026-06-20 |
| [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 |
| [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 |
| [Prune write-only fields and a dead routing knob from the fs seam](implemented/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 |
### Architecture

View File

@@ -1,27 +1,27 @@
# RFC: Prune write-only fields and a dead routing knob from the fs seam
Status: proposed
Status: implemented (proposed and accepted 2026-07-04)
## Problem
The [fs seam split](../../implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) moved read routing and policy out of the backend into `dsh-tool-fs` and `dsh-fs-policy`. Four pieces of surface kept the pre-split shape — populated on every call, read by nobody:
The [fs seam split](2026-06-26-fsspec-style-fs-seam.md) moved read routing and policy out of the backend into `dsh-tool-fs` and `dsh-fs-policy`. Four pieces of surface kept the pre-split shape — populated on every call, read by nobody:
1. **`STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize` in `dsh-fs-local`** (`packages/fs/fs-local/src/fsio.ts`, re-exported from `packages/fs/fs-local/src/index.ts`): zero readers anywhere, including fs-local's own source and tests. The backend has no read routing — `readWholeText`/`streamWholeText` are separate primitives the caller chooses between — and the real routing constant lives in the consumer (`packages/fs/tool-fs/src/read.ts`, compared against `info.size`). Two mirrors of the 10 MiB fact; the backend's is dead, and the knob's JSDoc claims a "read routing" override that does not exist.
2. **`FsTarget.inputPath`** (`packages/fs/fs/src/types.ts`): every backend and every test fake must fabricate a "diagnostics only" value with zero production readers — the policy plugin and every error message use `targetKey`/`displayPath`. The `listDir` producer exposes the semantic wobble: directory children get the bare entry name, which was nobody's "input".
3. **`FsEditOutcome.replacements` + `.replaceAll`** (`packages/fs/fs/src/types.ts`): `replacements` has zero production readers (the single-match policy itself stays — it is enforced by the `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` throws inside the backend, whose error message keeps the internal count); `replaceAll` is read only by `formatEditOutput` in `packages/fs/tool-fs/src/edit.ts` — as an echo of the `replace_all` argument the tool already holds. Shrunk, `FsEditOutcome` becomes `{ version, before, after }`, parallel to `FsWriteOutcome`'s genuinely backend-discovered fields.
4. **`FileReadOutcome.limit` + `.version`** (`packages/fs/tool-fs/src/read-render.ts`): populated by the read tool, but `formatReadOutput` renders `offset`/`lines`/`totalLines`/`truncatedByBytes` only, and the `fs/observed` emit uses `info.version` directly rather than the outcome copy.
1. **`STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize` in `dsh-fs-local`** (`packages/fs/fs-local/src/fsio.ts`, re-exported from `packages/fs/fs-local/src/index.ts`): zero readers anywhere, including fs-local's own source and tests. The backend has no read routing — `readWholeText`/`streamWholeText` are separate primitives the caller chooses between — and the real routing constant lives in the consumer (`packages/fs/tool-fs/src/read.ts`, compared against `info.size`). Two mirrors of the 10 MiB fact; the backend's was dead, and the knob's JSDoc claimed a "read routing" override that did not exist.
2. **`FsTarget.inputPath`** (`packages/fs/fs/src/types.ts`): every backend and every test fake had to fabricate a "diagnostics only" value with zero production readers — the policy plugin and every error message use `targetKey`/`displayPath`. The `listDir` producer exposed the semantic wobble: directory children got the bare entry name, which was nobody's "input".
3. **`FsEditOutcome.replacements` + `.replaceAll`** (`packages/fs/fs/src/types.ts`): `replacements` had zero production readers (the single-match policy itself stays — it is enforced by the `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` throws inside the backend, whose error message keeps the internal count); `replaceAll` was read only by `formatEditOutput` in `packages/fs/tool-fs/src/edit.ts` — as an echo of the `replace_all` argument the tool already holds. Shrunk, `FsEditOutcome` is `{ version, before, after }`, parallel to `FsWriteOutcome`'s genuinely backend-discovered fields.
4. **`FileReadOutcome.limit` + `.version`** (`packages/fs/tool-fs/src/read-render.ts`): populated by the read tool, but `formatReadOutput` renders `offset`/`lines`/`totalLines`/`truncatedByBytes` only, and the `fs/observed` emit uses `info.version` directly rather than an outcome copy.
## Proposal
## Decision
Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. Update the [filesystem.md](../../../core-data-structures/filesystem.md) pastes, the type-equiv manifest, `packages/fs/fs/README.md`, and the test fakes that currently must fabricate the removed fields.
Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. The [filesystem.md](../../../core-data-structures/filesystem.md) pastes, `packages/fs/fs/README.md`, and the test fakes that had to fabricate the removed fields shrink with the types.
## Why not keep them?
A future permission/containment layer might want the pre-resolution path for error text — but it would want the *request*, which every call site still holds. "N occurrences replaced" might become model-facing text — a behavior change to design when wanted, and the backend-internal count survives for its error message. A read footer might display `limit` — everything the footer shows already derives from `lines`/`totalLines`. Meanwhile every current and future backend (remote, native) must fabricate wire fields nobody consumes, and every test fake must satisfy them.
A future permission/containment layer might want the pre-resolution path for error text — but it would want the *request*, which every call site still holds. "N occurrences replaced" might become model-facing text — a behavior change to design when wanted, and the backend-internal count survives for its error message. A read footer might display `limit` — everything the footer shows already derives from `lines`/`totalLines`. Meanwhile every current and future backend (remote, native) would have to fabricate wire fields nobody consumes, and every test fake would have to satisfy them.
## Acceptance criteria
- The removed surfaces are gone — `STREAM_MIN_SIZE`/`streamMinSize` in `dsh-fs-local`, `FsTarget.inputPath`, `FsEditOutcome.replacements`/`.replaceAll`, and `FileReadOutcome.limit`/`.version` — while the request-side `replaceAll` (`FsEditSpec`) and the version fields on the other outcome types are untouched; doc pastes and the manifest in sync; the suite is green with the shrunk fakes.
- The removed surfaces are gone — `STREAM_MIN_SIZE`/`streamMinSize` in `dsh-fs-local`, `FsTarget.inputPath`, `FsEditOutcome.replacements`/`.replaceAll`, and `FileReadOutcome.limit`/`.version` — while the request-side `replaceAll` (`FsEditRequest`) and the version fields on the other outcome types are untouched; doc pastes and the manifest in sync; the suite is green with the shrunk fakes.
- `formatEditOutput`'s emitted text is unchanged for both `replace_all` branches, so no snapshot golden churns.
## Risks

View File

@@ -26,9 +26,6 @@ import { basename, dirname, join, resolve } from 'node:path'
import { TextDecoder } from 'node:util'
import { FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
/** Files at or above this size stream their text; smaller files read whole. */
export const STREAM_MIN_SIZE = 10 * 1024 * 1024
const BINARY_SAMPLE_BYTES = 8192
function isENOENT(error: unknown): boolean {
@@ -85,13 +82,11 @@ function versionOf(info: Stats): FsVersion {
}
/**
* Test seam: lets specs force the streaming read path (via a small
* `streamMinSize`) and pin the temp-file name (to prove exclusive-open
* behavior) without a 10 MB fixture or a name race.
* Test seam: lets specs pin the atomic-write temp names (to prove
* exclusive-open behavior without a name race) and observe the staged temp
* file before it is renamed over the target.
*/
export interface FsIoInternals {
/** Override {@link STREAM_MIN_SIZE} for read routing. */
streamMinSize?: number
/** Override the generated private staging-dir name (relative to the target dir). */
tempDirName?: (writePath: string) => string
/** Override the generated temp-file name (relative to the private staging dir). */

View File

@@ -41,7 +41,6 @@ import {
import type { FsIoInternals } from './fsio.ts'
export {
STREAM_MIN_SIZE,
applyLiteralEdit,
listDirectory,
probe,
@@ -105,7 +104,7 @@ export class LocalFileSystem extends FileSystem {
override async resolve(path: string, opts?: { cwd?: string }): Promise<FsTarget> {
const local = await resolveLocalTarget(opts?.cwd ?? this.config.cwd, path)
return { inputPath: path, targetKey: local.targetKey, displayPath: local.displayPath }
return { targetKey: local.targetKey, displayPath: local.displayPath }
}
override async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {
@@ -128,7 +127,7 @@ export class LocalFileSystem extends FileSystem {
return entries.map(entry => ({
name: entry.name,
type: entry.type,
target: { inputPath: entry.name, targetKey: entry.target.targetKey, displayPath: entry.target.displayPath },
target: { targetKey: entry.target.targetKey, displayPath: entry.target.displayPath },
...(entry.version !== undefined ? { version: entry.version } : {}),
...(entry.size !== undefined ? { size: entry.size } : {}),
}))
@@ -211,8 +210,6 @@ export class LocalFileSystem extends FileSystem {
const after = await probe(target.targetKey)
return {
replacements: edited.replacements,
replaceAll: edit.replaceAll,
version: this.versionAfterWrite(after, target),
// The LF-normalized before/after text (the applied-hunk diff basis);
// line-ending restoration is a storage detail the diff ignores.

View File

@@ -138,12 +138,6 @@ describe('listDir', () => {
join(dir, 'skills', 'dir-skill'),
join(dir, 'skills', 'zeta.md'),
])
expect(entries.map(entry => entry.target.inputPath)).toEqual([
'alpha.md',
'broken-link',
'dir-skill',
'zeta.md',
])
const materializedEntries = entries.filter(entry => entry.version !== undefined)
expect(materializedEntries.map(entry => entry.target.targetKey))
.toEqual(await Promise.all(materializedEntries.map(entry => realpath(entry.target.displayPath))))
@@ -335,7 +329,7 @@ describe('editText', () => {
await writeFile(join(dir, 'a.txt'), 'hello world')
const target = await fs.resolve('a.txt')
const outcome = await fs.editText(target, { oldString: 'world', newString: 'there', replaceAll: false }, { version: await versionOf(target) })
expect(outcome.replacements).toBe(1)
expect(outcome.after).toBe('hello there')
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('hello there')
})
@@ -365,7 +359,7 @@ describe('editText', () => {
const target = await fs.resolve('a.txt')
// No version guard: any current content is edited, regardless of version.
const outcome = await fs.editText(target, { oldString: 'world', newString: 'there', replaceAll: false })
expect(outcome.replacements).toBe(1)
expect(outcome.after).toBe('hello there')
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('hello there')
})
@@ -411,7 +405,7 @@ describe('editText', () => {
await writeFile(join(dir, 'a.txt'), 'a a a')
const target = await fs.resolve('a.txt')
const outcome = await fs.editText(target, { oldString: 'a', newString: 'b', replaceAll: true }, { version: await versionOf(target) })
expect(outcome.replacements).toBe(3)
expect(outcome.after).toBe('b b b')
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('b b b')
})
@@ -457,7 +451,7 @@ describe('editText', () => {
// The version the first edit returned is a valid guard for a second edit —
// no intervening re-stat needed.
const second = await fs.editText(target, { oldString: 'two', newString: 'TWO', replaceAll: false }, { version: first.version })
expect(second.replacements).toBe(1)
expect(second.after).toBe('ONE TWO')
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('ONE TWO')
})

View File

@@ -18,7 +18,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
import type { FsPolicyExec } from '@deepseek-ai/dsh-fs-policy'
function target(path: string): FsTarget {
return { inputPath: path, targetKey: FsTargetKey(path), displayPath: path }
return { targetKey: FsTargetKey(path), displayPath: path }
}
const ownerExec = (session: object): FsPolicyExec => ({ agent: { session } })

View File

@@ -19,7 +19,7 @@ A backend subclasses `FileSystem` and implements seven primitives.
| Member | Semantics |
|---|---|
| `resolve(path, opts?)` | Resolve a path into a stable `FsTarget` (`inputPath`, opaque `targetKey`, `displayPath`). `opts.cwd` is the base a relative `path` resolves against (a caller supplies its session workspace; absolute paths ignore it; omitted ⇒ the backend default). Async — a remote backend may need I/O. The same file via different paths must yield the same `targetKey`. |
| `resolve(path, opts?)` | Resolve a path into a stable `FsTarget` (opaque `targetKey`, `displayPath`). `opts.cwd` is the base a relative `path` resolves against (a caller supplies its session workspace; absolute paths ignore it; omitted ⇒ the backend default). Async — a remote backend may need I/O. The same file via different paths must yield the same `targetKey`. |
| `stat(target, signal?)` | Return `FsInfo` metadata (`version`, `type`, optional `size`), or `undefined` when the target is absent. Never content. |
| `readText(target, signal?)` | Read the whole regular text file as one decoded string. Owns regular-file checks, UTF-8 decoding, binary/NUL rejection (`FS_NOT_TEXT`). |
| `streamText(target, signal?)` | Stream the same text as decoded chunks for large files (cross-chunk UTF-8 decoding stays here). |

View File

@@ -52,8 +52,6 @@ export function FsVersion(v: string): FsVersion {
* this; every other operation takes it.
*/
export interface FsTarget {
/** The original model/plugin-supplied path, for diagnostics only. */
inputPath: string
/** Opaque key for stale guards and target lookup. */
targetKey: FsTargetKey
/**
@@ -142,10 +140,6 @@ export interface FsEditRequest {
/** Outcome of a literal edit. */
export interface FsEditOutcome {
/** Number of literal replacements applied. */
replacements: number
/** Whether every match was replaced. */
replaceAll: boolean
/** Opaque version of the file after the edit. */
version: FsVersion
/**

View File

@@ -23,7 +23,7 @@ class FakeFileSystem extends FileSystem {
files = new Map<string, string>()
override async resolve(path: string): Promise<FsTarget> {
return { inputPath: path, targetKey: FsTargetKey(path), displayPath: path }
return { targetKey: FsTargetKey(path), displayPath: path }
}
override async stat(target: FsTarget): Promise<FsInfo | undefined> {
const content = this.files.get(target.targetKey)
@@ -45,7 +45,7 @@ class FakeFileSystem extends FileSystem {
{
name: 'alpha.md',
type: 'file',
target: { inputPath: 'skills/alpha.md', targetKey: FsTargetKey('skills/alpha.md'), displayPath: 'skills/alpha.md' },
target: { targetKey: FsTargetKey('skills/alpha.md'), displayPath: 'skills/alpha.md' },
size: 2,
version: FsVersion('v1'),
},
@@ -60,7 +60,7 @@ class FakeFileSystem extends FileSystem {
const content = this.files.get(target.targetKey) ?? ''
const after = content.split(edit.oldString).join(edit.newString)
this.files.set(target.targetKey, after)
return { replacements: 1, replaceAll: edit.replaceAll, version: FsVersion('v3'), before: content, after }
return { version: FsVersion('v3'), before: content, after }
}
}
@@ -108,7 +108,7 @@ describe('FileSystem provider seam', () => {
expect(entries).toEqual([{
name: 'alpha.md',
type: 'file',
target: { inputPath: 'skills/alpha.md', targetKey: 'skills/alpha.md', displayPath: 'skills/alpha.md' },
target: { targetKey: 'skills/alpha.md', displayPath: 'skills/alpha.md' },
size: 2,
version: 'v1',
}])

View File

@@ -16,7 +16,6 @@ import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { DiffCallView, DiffResultView, ToolResult } from '@deepseek-ai/dsh-tools'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { FsEditOutcome } from '@deepseek-ai/dsh-fs'
import type {} from '@deepseek-ai/dsh-fs'
import type {} from '@deepseek-ai/dsh-system-prompt'
import { computeHunkDiffs, diffsFromMeta, type FsDiffMeta } from './diff.ts'
@@ -43,9 +42,9 @@ export function parseEditArgs(args: { file_path: string; old_string: string; new
}
}
/** Format an edit outcome as a Claude-style model-facing success message. */
export function formatEditOutput(displayPath: string, outcome: FsEditOutcome): string {
return outcome.replaceAll
/** Format an edit success (single-match or replace-all) as a Claude-style model-facing message. */
export function formatEditOutput(displayPath: string, replaceAll: boolean): string {
return replaceAll
? `The file ${displayPath} has been updated. All occurrences were successfully replaced.`
: `The file ${displayPath} has been updated successfully.`
}
@@ -91,7 +90,7 @@ export function applyEditTool(ctx: Context): void {
// relativizes it).
const diffs = computeHunkDiffs(input.filePath, outcome.before, outcome.after)
return {
content: [{ type: 'text', text: formatEditOutput(target.displayPath, outcome) }],
content: [{ type: 'text', text: formatEditOutput(target.displayPath, input.replaceAll) }],
meta: { diffs },
}
},

View File

@@ -17,7 +17,6 @@
*/
import { FsError } from '@deepseek-ai/dsh-fs'
import type { FsVersion } from '@deepseek-ai/dsh-fs'
/** Maximum characters returned for a single line. */
export const READ_MAX_LINE_LENGTH = 2000
@@ -58,16 +57,12 @@ export interface WindowResult {
export interface FileReadOutcome {
/** 1-based first line requested. */
offset: number
/** Maximum number of lines requested. */
limit: number
/** Returned lines, already numbered. */
lines: FileTextLine[]
/** Total line count in the file, unless `truncatedByBytes` stopped scanning early. */
totalLines: number
/** Whether selected output hit the byte cap before EOF or the requested limit. */
truncatedByBytes?: true
/** Opaque version of the file at read time. */
version: FsVersion
}
interface WindowAccumulator {

View File

@@ -90,10 +90,8 @@ export function applyReadTool(ctx: Context): void {
const outcome: FileReadOutcome = {
offset: input.offset,
limit: input.limit,
lines: window.lines,
totalLines: window.totalLines,
version: info.version,
...window.truncatedByBytes ? { truncatedByBytes: true } : {},
}
// Record the observed version (a no-op when no policy plugin listens). The

View File

@@ -40,7 +40,7 @@ class FakeFs extends FileSystem {
}
override async resolve(path: string): Promise<FsTarget> {
return { inputPath: path, targetKey: FsTargetKey(`key:${path}`), displayPath: `/abs/${path}` }
return { targetKey: FsTargetKey(`key:${path}`), displayPath: `/abs/${path}` }
}
override async stat(target: FsTarget): Promise<FsInfo | undefined> {
this.throwIfArmed()
@@ -71,7 +71,7 @@ class FakeFs extends FileSystem {
const content = this.files.get(target.targetKey) ?? ''
const after = content.split(edit.oldString).join(edit.newString)
this.files.set(target.targetKey, after)
return { replacements: 1, replaceAll: edit.replaceAll, version: FsVersion('v3'), before: content, after }
return { version: FsVersion('v3'), before: content, after }
}
}
@@ -252,7 +252,7 @@ describe('read tool', () => {
})
describe('formatReadOutput footer variants', () => {
const base: FileReadOutcome = { offset: 1, limit: 2000, lines: [{ number: 1, text: 'x' }], totalLines: 1, version: FsVersion('v') }
const base: FileReadOutcome = { offset: 1, lines: [{ number: 1, text: 'x' }], totalLines: 1 }
it('reports a byte-capped read', () => {
const out = formatReadOutput('/f', { ...base, totalLines: 99, truncatedByBytes: true })