Expose audited hardcoded tunables as plugin config
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
This commit is contained in:
@@ -6,10 +6,11 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildWindow, READ_MAX_LINE_LENGTH } from '@deepseek-ai/dsh-tool-fs'
|
||||
import { buildWindow, READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from '@deepseek-ai/dsh-tool-fs'
|
||||
import type { ReadWindow } from '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
const READ_ALL: ReadWindow = { offset: 1, limit: 2000 }
|
||||
const DEFAULT_CAPS = { maxLineLength: READ_MAX_LINE_LENGTH, maxBytes: READ_MAX_BYTES }
|
||||
const READ_ALL: ReadWindow = { offset: 1, limit: 2000, ...DEFAULT_CAPS }
|
||||
|
||||
/** Yield `text` as one chunk (whole-file read shape). */
|
||||
async function* whole(text: string): AsyncIterable<string> {
|
||||
@@ -34,7 +35,7 @@ describe('buildWindow', () => {
|
||||
})
|
||||
|
||||
it('applies offset/limit', async () => {
|
||||
const result = await buildWindow(whole('one\ntwo\nthree\nfour'), { offset: 2, limit: 2 }, 'f')
|
||||
const result = await buildWindow(whole('one\ntwo\nthree\nfour'), { offset: 2, limit: 2, ...DEFAULT_CAPS }, 'f')
|
||||
expect(result.lines.map(l => l.number)).toEqual([2, 3])
|
||||
expect(result.totalLines).toBe(4)
|
||||
})
|
||||
@@ -62,7 +63,7 @@ describe('buildWindow', () => {
|
||||
})
|
||||
|
||||
it('rejects an offset past EOF', async () => {
|
||||
await expect(buildWindow(whole('one\ntwo'), { offset: 9, limit: 1 }, 'f')).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
|
||||
await expect(buildWindow(whole('one\ntwo'), { offset: 9, limit: 1, ...DEFAULT_CAPS }, 'f')).rejects.toMatchObject({ code: 'FS_NOT_FOUND' })
|
||||
})
|
||||
|
||||
it('flushes a final line with no trailing newline', async () => {
|
||||
@@ -76,9 +77,22 @@ describe('buildWindow', () => {
|
||||
expect(result.totalLines).toBe(2)
|
||||
})
|
||||
|
||||
describe('caps are per-request (the plugin config reaches the window)', () => {
|
||||
it('truncates lines at a custom maxLineLength and names it in the suffix', async () => {
|
||||
const result = await buildWindow(whole('abcdefghij'), { offset: 1, limit: 10, maxLineLength: 5, maxBytes: READ_MAX_BYTES }, 'f')
|
||||
expect(result.lines[0]?.text).toBe('abcde... (line truncated to 5 chars)')
|
||||
})
|
||||
|
||||
it('caps output at a custom maxBytes', async () => {
|
||||
const result = await buildWindow(whole('aaaa\nbbbb\ncccc'), { offset: 1, limit: 10, maxLineLength: 2000, maxBytes: 9 }, 'f')
|
||||
expect(result.lines.map(l => l.text)).toEqual(['aaaa', 'bbbb'])
|
||||
expect(result.truncatedByBytes).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('chunked input (streamed read shape)', () => {
|
||||
it('windows identically when text arrives in small chunks', async () => {
|
||||
const result = await buildWindow(chunked('one\ntwo\nthree', 2), { offset: 2, limit: 1 }, 'f')
|
||||
const result = await buildWindow(chunked('one\ntwo\nthree', 2), { offset: 2, limit: 1, ...DEFAULT_CAPS }, 'f')
|
||||
expect(result.lines).toEqual([{ number: 2, text: 'two' }])
|
||||
expect(result.totalLines).toBe(3)
|
||||
})
|
||||
|
||||
@@ -495,3 +495,70 @@ describe('result-time contextual diff (meta + presentResult)', () => {
|
||||
expect(view).toEqual({ card: 'diff', title: 'Write a.txt', diffs: [{ path: 'a.txt', oldText: null, newText: 'y' }] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('read caps are plugin config', () => {
|
||||
async function setupWith(config: ToolFs.Config) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(FakeFs)
|
||||
await ctx.plugin(FsPolicy)
|
||||
await ctx.plugin(ToolFs, config)
|
||||
return { ctx, fs: ctx.fs as FakeFs }
|
||||
}
|
||||
|
||||
it('a configured readLimit is both the default and the cap, and the schema names it', async () => {
|
||||
const { ctx, fs } = await setupWith({ readLimit: 2 })
|
||||
fs.files.set('key:a.txt', 'one\ntwo\nthree\nfour')
|
||||
const result = await call(ctx, 'read', { file_path: 'a.txt' })
|
||||
expect(text(result)).toContain('(Showing lines 1-2 of 4. Use offset=3 to continue.)')
|
||||
const overCap = await call(ctx, 'read', { file_path: 'a.txt', limit: 3 })
|
||||
expect(overCap.isError).toBe(true)
|
||||
expect(text(overCap)).toContain('less than or equal to 2')
|
||||
const readSchema = ctx.tools.schemas().find(s => s.name === 'read')
|
||||
expect(JSON.stringify(readSchema)).toContain('Defaults to 2.')
|
||||
})
|
||||
|
||||
it('a configured readMaxLineLength truncates lines at the configured length', async () => {
|
||||
const { ctx, fs } = await setupWith({ readMaxLineLength: 4 })
|
||||
fs.files.set('key:a.txt', 'abcdefgh')
|
||||
const result = await call(ctx, 'read', { file_path: 'a.txt' })
|
||||
expect(text(result)).toContain('1: abcd... (line truncated to 4 chars)')
|
||||
})
|
||||
|
||||
it('a configured readMaxBytes caps the window at the configured bytes', async () => {
|
||||
const { ctx, fs } = await setupWith({ readMaxBytes: 9 })
|
||||
fs.files.set('key:a.txt', 'aaaa\nbbbb\ncccc')
|
||||
const result = await call(ctx, 'read', { file_path: 'a.txt' })
|
||||
expect(text(result)).toContain('Output capped.')
|
||||
expect(text(result)).not.toContain('cccc')
|
||||
})
|
||||
|
||||
it('a configured readStreamMinSize routes smaller files to the streaming path', async () => {
|
||||
const { ctx, fs } = await setupWith({ readStreamMinSize: 5 })
|
||||
fs.files.set('key:a.txt', 'alpha\nbeta')
|
||||
const readSpy = vi.spyOn(fs, 'readText')
|
||||
const streamSpy = vi.spyOn(fs, 'streamText')
|
||||
const result = await call(ctx, 'read', { file_path: 'a.txt' })
|
||||
expect(result.isError).toBe(false)
|
||||
expect(streamSpy).toHaveBeenCalled()
|
||||
expect(readSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['readLimit', { readLimit: 0 }],
|
||||
['readMaxLineLength', { readMaxLineLength: -1 }],
|
||||
['readMaxBytes', { readMaxBytes: Number.NaN }],
|
||||
['readStreamMinSize', { readStreamMinSize: 0 }],
|
||||
] as const)('rejects a non-positive %s at load', async (name, config) => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(FakeFs)
|
||||
await expect(ctx.plugin(ToolFs, config)).rejects.toThrow(new RegExp(`tool-fs: ${name} must be a positive finite number`))
|
||||
})
|
||||
|
||||
it('has no default export (namespace plugin export shape)', () => {
|
||||
expect('default' in ToolFs).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user