Merge origin/master into codex/sandbox-policy-context
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/fs/tool-fs/README.md
|
||||
README.md: d8d75e251d6b565b402d97eed7bc8b135708e7c5
|
||||
README.zh.md: 515320ee85219a55904702dfde1777f53f047cd3
|
||||
README.md: a695d0ba8fb1d600689d2b68763e8423d1591da5
|
||||
README.zh.md: 5c600ab70b46da640637aec64efc1c0f0d0d54c0
|
||||
|
||||
@@ -34,7 +34,7 @@ All keys are optional; the defaults are the shipped read caps.
|
||||
|
||||
Field names are snake_case to match Claude Code and existing harness tool schemas.
|
||||
|
||||
Canonical successes are `read` → `{ path, offset, lines: [{ number, text }], totalLines }`, `write` → `{ path, operation: 'create' | 'update', before: string | null, after }`, and `edit` → `{ path, before, after }`. Native renderers preserve the line-numbered read and mutation acknowledgements below. Write/edit derive replayable diff-card metadata from these values; the values themselves are execution-local and are not added to `tool/result`.
|
||||
Canonical successes are `read` → `{ path, offset, lines: [{ number, text }], totalLines }`, `write` → `{ path, operation: 'create' | 'update', before: string | null, after }`, and `edit` → `{ path, before, after }`. Native renderers preserve the line-numbered read and mutation acknowledgements below. `write`/`edit` derive replayable diff-card metadata, and `read` derives a replayable read-card window `{ path, offset, lines, totalLines, lang? }`, from these canonical values; the canonical values themselves are execution-local and are not added to `tool/result`, only the derived presentation metadata is persisted.
|
||||
|
||||
## The tool is the executor; policy is an event gate
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ await ctx.plugin(ToolFs) // this package — re
|
||||
|
||||
字段名使用 snake_case,与 Claude Code 和现有 harness 工具 schema 一致。
|
||||
|
||||
规范成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。原生渲染器会保留下方带行号的读取结果和变更确认。写入/编辑从这些值派生可回放的 diff 卡片元数据;这些值本身仅限于本次执行,不会添加到 `tool/result`。
|
||||
规范成功值分别为:`read` → `{ path, offset, lines: [{ number, text }], totalLines }`,`write` → `{ path, operation: 'create' | 'update', before: string | null, after }`,`edit` → `{ path, before, after }`。原生渲染器会保留下方带行号的读取结果和变更确认。`write`/`edit` 从这些规范值派生可回放的 diff 卡片元数据,`read` 派生可回放的读取卡片窗口 `{ path, offset, lines, totalLines, lang? }`;规范值本身仅限于本次执行,不会添加到 `tool/result`,只有派生出的呈现元数据会被持久化。
|
||||
|
||||
## 工具就是执行器;策略是事件门禁
|
||||
|
||||
|
||||
@@ -168,3 +168,105 @@ export function formatReadOutput(displayPath: string, outcome: FileReadOutcome):
|
||||
${body}
|
||||
</content>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowercased file-extension to syntax-highlighting language hint. Keys are the
|
||||
* extension without its dot; a UI treats an absent key as plain text. The map is
|
||||
* intentionally small — common source, config, and markup extensions a
|
||||
* line-numbered code view benefits from highlighting — not an exhaustive registry.
|
||||
*/
|
||||
const LANG_BY_EXTENSION: Readonly<Record<string, string>> = {
|
||||
ts: 'ts', tsx: 'tsx', mts: 'ts', cts: 'ts',
|
||||
js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js',
|
||||
json: 'json', jsonc: 'json',
|
||||
py: 'py', rb: 'rb', go: 'go', rs: 'rs', java: 'java',
|
||||
c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', hpp: 'cpp', cxx: 'cpp',
|
||||
cs: 'cs', kt: 'kotlin', swift: 'swift', php: 'php',
|
||||
sh: 'sh', bash: 'sh', zsh: 'sh',
|
||||
yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini',
|
||||
md: 'md', markdown: 'md', mdx: 'mdx',
|
||||
html: 'html', htm: 'html', css: 'css', scss: 'scss', less: 'less',
|
||||
sql: 'sql', xml: 'xml', lua: 'lua',
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a syntax-highlighting language hint from a read path's file extension.
|
||||
* Pure and case-insensitive on the extension; a dotfile with no extension
|
||||
* (`.gitignore`) and an unknown extension both yield `undefined`.
|
||||
* @param path - the model-facing path the read reported.
|
||||
* @returns the language hint for {@link LANG_BY_EXTENSION}, or `undefined` when the extension maps to none.
|
||||
*/
|
||||
export function langFromPath(path: string): string | undefined {
|
||||
const base = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1)
|
||||
const dot = base.lastIndexOf('.')
|
||||
// A leading dot is a dotfile (no extension), not an empty extension.
|
||||
if (dot <= 0) return undefined
|
||||
const ext = base.slice(dot + 1).toLowerCase()
|
||||
// Own-property check only: a filename whose extension is an Object.prototype
|
||||
// key (`foo.constructor`, `foo.__proto__`) must map to no language, not to the
|
||||
// inherited member — otherwise a function would reach `lang` and fail the
|
||||
// tool-output JSON validation.
|
||||
return Object.hasOwn(LANG_BY_EXTENSION, ext) ? LANG_BY_EXTENSION[ext] : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The `read` tool's private `tool/result` `meta` payload: the structured
|
||||
* line-numbered window a capable UI renders as a code view. Attached opaquely (as
|
||||
* `unknown`) on the tool result and persisted with the session log — it must be
|
||||
* JSON-serializable (the session validates this at `append`), so `presentResult`
|
||||
* reproduces the read card on replay when the raw structured output is no longer
|
||||
* on the wire. The producing tool owns and narrows this opaque shape.
|
||||
*/
|
||||
export interface FsReadMeta {
|
||||
/** The read file's model-facing path. */
|
||||
path: string
|
||||
/** The 1-based first line the window requested, kept even when `lines` is empty. */
|
||||
offset: number
|
||||
/** The returned window's lines, each keeping its file line number. */
|
||||
lines: FileTextLine[]
|
||||
/** Exact total line count in the file. */
|
||||
totalLines: number
|
||||
/** Syntax-highlighting language hint from the extension, or omitted for plain text. */
|
||||
lang?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `value` is a valid {@link FileTextLine} (defensive narrowing from
|
||||
* opaque `meta`). `number` must be a 1-based integer line number, since a card
|
||||
* rendered from a zero, fractional, or non-finite line number would violate the
|
||||
* 1-based numbering contract the read window promises.
|
||||
*/
|
||||
function isFileTextLine(value: unknown): value is FileTextLine {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
||||
const { number, text } = value as Record<string, unknown>
|
||||
return typeof number === 'number' && Number.isInteger(number) && number >= 1 && typeof text === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow opaque live or replayed result metadata to a structured read window.
|
||||
* Malformed metadata returns `undefined` so presentation can fall back to the
|
||||
* generic text card instead of throwing during replay. Beyond shape, the
|
||||
* semantic contract of a read window is enforced against replayed JSON that is
|
||||
* well-typed but out of range: `offset` must be a 1-based integer, `totalLines`
|
||||
* must be a non-negative integer, each line number must be a 1-based integer no
|
||||
* less than `offset`, the line numbers must strictly increase, and no line number
|
||||
* may exceed `totalLines`. Any violation declines to the generic fallback rather
|
||||
* than emitting a card that misnumbers or overcounts.
|
||||
* @param meta - result metadata.
|
||||
* @returns the validated read window, or `undefined` for absent, malformed, or semantically invalid data.
|
||||
*/
|
||||
export function readMetaFromMeta(meta: unknown): FsReadMeta | undefined {
|
||||
if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined
|
||||
const { path, offset, lines, totalLines, lang } = meta as Record<string, unknown>
|
||||
if (typeof path !== 'string' || typeof totalLines !== 'number' || typeof offset !== 'number') return undefined
|
||||
if (!Number.isInteger(offset) || offset < 1) return undefined
|
||||
if (!Number.isInteger(totalLines) || totalLines < 0) return undefined
|
||||
if (!Array.isArray(lines) || !lines.every(isFileTextLine)) return undefined
|
||||
if (lang !== undefined && typeof lang !== 'string') return undefined
|
||||
let previous = offset - 1
|
||||
for (const { number } of lines) {
|
||||
if (number <= previous || number > totalLines) return undefined
|
||||
previous = number
|
||||
}
|
||||
return { path, offset, lines, totalLines, ...lang === undefined ? {} : { lang } }
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, GenericResultView, ToolResult } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView, ReadResultView, ToolResult } from '@deepseek-ai/dsh-tools'
|
||||
import { FsError } from '@deepseek-ai/dsh-fs'
|
||||
import type {} from '@deepseek-ai/dsh-fs'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { buildWindow, formatReadOutput } from './read-render.ts'
|
||||
import { buildWindow, formatReadOutput, langFromPath, readMetaFromMeta } from './read-render.ts'
|
||||
import { sessionResolveOptions } from './session-cwd.ts'
|
||||
|
||||
/** Default and maximum number of lines returned by one `read` call (the `readLimit` config). */
|
||||
@@ -118,6 +118,19 @@ export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
|
||||
}),
|
||||
}]
|
||||
},
|
||||
// Project the structured window into persisted `meta` so a UI's read card
|
||||
// survives replay: the raw canonical output object is not on the wire, only
|
||||
// the model-facing text, from which the line/lang data cannot be recovered.
|
||||
presentationMeta: (_args, value) => {
|
||||
const lang = langFromPath(value.path)
|
||||
return {
|
||||
path: value.path,
|
||||
offset: value.offset,
|
||||
lines: value.lines.map(({ number, text }) => ({ number, text })),
|
||||
totalLines: value.totalLines,
|
||||
...lang === undefined ? {} : { lang },
|
||||
}
|
||||
},
|
||||
},
|
||||
// Observation races fail closed because guarded mutations re-check the version in-lock.
|
||||
isConcurrencySafe: () => true,
|
||||
@@ -154,15 +167,32 @@ export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
|
||||
ctx.emit('fs/observed', target, info.version, exec)
|
||||
return outcome
|
||||
},
|
||||
presentResult(_args, result: ToolResult): GenericResultView | undefined {
|
||||
// Result-time display: a `read` card carrying the structured line window a
|
||||
// capable UI renders as a line-numbered, syntax-highlighted view. The
|
||||
// structured data is narrowed from the persisted `meta` (replay-safe); the
|
||||
// envelope-stripped model-facing text rides along as `content` so a UI without
|
||||
// the read capability still shows the file text. A malformed or absent meta,
|
||||
// or a result whose text is not the read envelope, declines to `undefined`
|
||||
// (the generic fallback), never throwing on replay of obsolete logged output.
|
||||
presentResult(_args, result: ToolResult): ReadResultView | undefined {
|
||||
if (result.isError) return undefined
|
||||
const meta = readMetaFromMeta(result.meta)
|
||||
if (meta === undefined) return undefined
|
||||
const only = result.content.length === 1 ? result.content[0] : undefined
|
||||
const text = only?.type === 'text' ? only.text : undefined
|
||||
if (text === undefined) return undefined
|
||||
// Group 1 always captures (possibly empty) when the envelope matches.
|
||||
const body = /^<path>[^\n]*<\/path>\n<type>file<\/type>\n<content>\n([\s\S]*)\n<\/content>$/u.exec(text)?.[1]
|
||||
if (body === undefined) return undefined
|
||||
return { card: 'generic', content: [{ type: 'text', text: body }] }
|
||||
return {
|
||||
card: 'read',
|
||||
path: meta.path,
|
||||
offset: meta.offset,
|
||||
lines: meta.lines,
|
||||
totalLines: meta.totalLines,
|
||||
...meta.lang === undefined ? {} : { lang: meta.lang },
|
||||
content: [{ type: 'text', text: body }],
|
||||
}
|
||||
},
|
||||
// Pure display: a generic card titled by the file with the read window appended (`Read
|
||||
// foo.txt (5 - 8)`), `read` kind (icon), and a follow-along location whose line is the
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildWindow, READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from '../src/read-render.ts'
|
||||
import { buildWindow, langFromPath, readMetaFromMeta, READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from '../src/read-render.ts'
|
||||
import type { ReadWindow } from '../src/read-render.ts'
|
||||
|
||||
const DEFAULT_CAPS = { maxLineLength: READ_MAX_LINE_LENGTH, maxBytes: READ_MAX_BYTES }
|
||||
@@ -116,3 +116,103 @@ describe('buildWindow', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('langFromPath', () => {
|
||||
it('maps a known extension to its language hint, case-insensitively', () => {
|
||||
expect(langFromPath('src/a.ts')).toBe('ts')
|
||||
expect(langFromPath('src/a.TSX')).toBe('tsx')
|
||||
expect(langFromPath('/abs/module.mjs')).toBe('js')
|
||||
expect(langFromPath('conf.yml')).toBe('yaml')
|
||||
expect(langFromPath('README.md')).toBe('md')
|
||||
})
|
||||
|
||||
it('reads the extension after the last path segment and last dot', () => {
|
||||
expect(langFromPath('a.py.bak')).toBeUndefined()
|
||||
expect(langFromPath('archive.tar.gz')).toBeUndefined()
|
||||
expect(langFromPath('/dir.py/plain')).toBeUndefined()
|
||||
expect(langFromPath('C:\\src\\main.rs')).toBe('rs')
|
||||
})
|
||||
|
||||
it('returns undefined for a dotfile, an extensionless name, and an unknown extension', () => {
|
||||
expect(langFromPath('.gitignore')).toBeUndefined()
|
||||
expect(langFromPath('/etc/hosts')).toBeUndefined()
|
||||
expect(langFromPath('data.unknownext')).toBeUndefined()
|
||||
expect(langFromPath('trailingdot.')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for a filename whose extension is an Object.prototype key', () => {
|
||||
// Own-property lookup only: these must not resolve to the inherited member
|
||||
// (a function/object), which would fail the tool-output JSON validation.
|
||||
expect(langFromPath('foo.constructor')).toBeUndefined()
|
||||
expect(langFromPath('foo.__proto__')).toBeUndefined()
|
||||
expect(langFromPath('foo.toString')).toBeUndefined()
|
||||
expect(langFromPath('foo.hasOwnProperty')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('readMetaFromMeta', () => {
|
||||
const good = { path: '/abs/a.ts', offset: 1, lines: [{ number: 1, text: 'x' }], totalLines: 1, lang: 'ts' }
|
||||
|
||||
it('narrows a well-formed read meta, with and without a lang hint', () => {
|
||||
expect(readMetaFromMeta(good)).toEqual(good)
|
||||
const noLang = { path: '/abs/a', offset: 1, lines: [], totalLines: 0 }
|
||||
expect(readMetaFromMeta(noLang)).toEqual(noLang)
|
||||
})
|
||||
|
||||
it('narrows an empty window at a positive offset (byte cap below the first selected line)', () => {
|
||||
const empty = { path: '/abs/a', offset: 5, lines: [], totalLines: 9 }
|
||||
expect(readMetaFromMeta(empty)).toEqual(empty)
|
||||
})
|
||||
|
||||
it('returns undefined for absent, non-object, or array meta', () => {
|
||||
expect(readMetaFromMeta(undefined)).toBeUndefined()
|
||||
expect(readMetaFromMeta(null)).toBeUndefined()
|
||||
expect(readMetaFromMeta('nope')).toBeUndefined()
|
||||
expect(readMetaFromMeta([good])).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when a field is missing or the wrong type (defensive narrowing)', () => {
|
||||
expect(readMetaFromMeta({ ...good, path: 5 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: '1' })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, totalLines: '1' })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: 'nope' })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: [{ number: '1', text: 'x' }] })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: [{ number: 1 }] })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: [null] })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lang: 5 })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects an offset that is not a 1-based integer', () => {
|
||||
expect(readMetaFromMeta({ ...good, offset: 0 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: 1.5 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: NaN })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: Infinity })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a first line number below offset', () => {
|
||||
expect(readMetaFromMeta({ ...good, offset: 2, lines: [{ number: 1, text: 'x' }], totalLines: 2 })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a line number that is not a 1-based integer', () => {
|
||||
expect(readMetaFromMeta({ ...good, lines: [{ number: 0, text: 'x' }], totalLines: 1 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: [{ number: 1.5, text: 'x' }], totalLines: 2 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: [{ number: NaN, text: 'x' }], totalLines: 1 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: [{ number: Infinity, text: 'x' }], totalLines: 1 })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a totalLines that is not a non-negative integer', () => {
|
||||
expect(readMetaFromMeta({ ...good, totalLines: -1 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, totalLines: 1.5 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, totalLines: NaN })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects lines that do not strictly increase or exceed totalLines', () => {
|
||||
const twoLines = { path: '/abs/a', offset: 1, lang: 'ts' }
|
||||
// Duplicate line numbers.
|
||||
expect(readMetaFromMeta({ ...twoLines, lines: [{ number: 1, text: 'a' }, { number: 1, text: 'b' }], totalLines: 2 })).toBeUndefined()
|
||||
// Out-of-order line numbers.
|
||||
expect(readMetaFromMeta({ ...twoLines, lines: [{ number: 2, text: 'b' }, { number: 1, text: 'a' }], totalLines: 2 })).toBeUndefined()
|
||||
// A line number past totalLines.
|
||||
expect(readMetaFromMeta({ ...twoLines, lines: [{ number: 3, text: 'c' }], totalLines: 2 })).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -320,6 +320,39 @@ describe('read tool', () => {
|
||||
expect(text(result)).toContain('Output capped.')
|
||||
})
|
||||
|
||||
it('attaches the structured window as presentation meta, and presentResult narrows it into a read card', async () => {
|
||||
const { ctx, fs } = await setup()
|
||||
fs.files.set('key:a.ts', 'const x = 1\nconst y = 2')
|
||||
const result = await call(ctx, 'read', { file_path: 'a.ts' })
|
||||
expect(result.isError).toBe(false)
|
||||
if (result.isError) throw new Error('expected read success')
|
||||
// The extension drives the lang hint; the window rides on persisted meta.
|
||||
expect(result.meta).toEqual({
|
||||
path: '/abs/a.ts',
|
||||
offset: 1,
|
||||
lines: [{ number: 1, text: 'const x = 1' }, { number: 2, text: 'const y = 2' }],
|
||||
totalLines: 2,
|
||||
lang: 'ts',
|
||||
})
|
||||
const view = ctx.tools.get('read')?.presentResult?.({ file_path: 'a.ts' }, result)
|
||||
expect(view).toEqual({
|
||||
card: 'read',
|
||||
path: '/abs/a.ts',
|
||||
offset: 1,
|
||||
lines: [{ number: 1, text: 'const x = 1' }, { number: 2, text: 'const y = 2' }],
|
||||
totalLines: 2,
|
||||
lang: 'ts',
|
||||
content: [{ type: 'text', text: '1: const x = 1\n2: const y = 2\n\n(End of file - total 2 lines)' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('omits the lang hint in meta for an extension that maps to no language', async () => {
|
||||
const { ctx, fs } = await setup()
|
||||
fs.files.set('key:notes', 'plain')
|
||||
const result = await call(ctx, 'read', { file_path: 'notes' })
|
||||
if (result.isError) throw new Error('expected read success')
|
||||
expect(result.meta).toEqual({ path: '/abs/notes', offset: 1, lines: [{ number: 1, text: 'plain' }], totalLines: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatReadOutput footer variants', () => {
|
||||
@@ -450,33 +483,72 @@ describe('tool-owned presentation (pure presentCall)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('read: completed presentation removes the model-facing XML envelope', async () => {
|
||||
expect(await presentResult('read', { file_path: 'a.txt' }, {
|
||||
content: [{ type: 'text', text: '<path>/tmp/a.txt</path>\n<type>file</type>\n<content>\n1: hello\n\n(End of file - total 1 lines)\n</content>' }],
|
||||
it('read: completed presentation is a read card carrying the structured window with the envelope stripped', async () => {
|
||||
// The structured line data rides on persisted meta (the raw output object is
|
||||
// not on the wire); presentResult narrows it and appends the stripped text as
|
||||
// the no-capability `content` fallback.
|
||||
const meta = { path: '/tmp/a.ts', offset: 1, lines: [{ number: 1, text: 'hello' }], totalLines: 1, lang: 'ts' }
|
||||
expect(await presentResult('read', { file_path: 'a.ts' }, {
|
||||
content: [{ type: 'text', text: '<path>/tmp/a.ts</path>\n<type>file</type>\n<content>\n1: hello\n\n(End of file - total 1 lines)\n</content>' }],
|
||||
isError: false,
|
||||
meta,
|
||||
})).toEqual({
|
||||
card: 'generic',
|
||||
card: 'read',
|
||||
path: '/tmp/a.ts',
|
||||
offset: 1,
|
||||
lines: [{ number: 1, text: 'hello' }],
|
||||
totalLines: 1,
|
||||
lang: 'ts',
|
||||
content: [{ type: 'text', text: '1: hello\n\n(End of file - total 1 lines)' }],
|
||||
})
|
||||
expect(await presentResult('read', { file_path: 'a.txt' }, {
|
||||
// A window whose extension maps to no language omits `lang` from the card.
|
||||
expect(await presentResult('read', { file_path: 'notes' }, {
|
||||
content: [{ type: 'text', text: '<path>/tmp/notes</path>\n<type>file</type>\n<content>\nbody\n</content>' }],
|
||||
isError: false,
|
||||
meta: { path: '/tmp/notes', offset: 1, lines: [{ number: 1, text: 'body' }], totalLines: 1 },
|
||||
})).toEqual({
|
||||
card: 'read',
|
||||
path: '/tmp/notes',
|
||||
offset: 1,
|
||||
lines: [{ number: 1, text: 'body' }],
|
||||
totalLines: 1,
|
||||
content: [{ type: 'text', text: 'body' }],
|
||||
})
|
||||
// Malformed envelope text with valid meta still declines (the fallback text is unavailable).
|
||||
expect(await presentResult('read', { file_path: 'a.ts' }, {
|
||||
content: [{ type: 'text', text: 'malformed replay' }],
|
||||
isError: false,
|
||||
meta,
|
||||
})).toBeUndefined()
|
||||
// Valid envelope but absent/malformed meta declines to the generic fallback.
|
||||
expect(await presentResult('read', { file_path: 'a.ts' }, {
|
||||
content: [{ type: 'text', text: '<path>/tmp/a.ts</path>\n<type>file</type>\n<content>\n1: hello\n</content>' }],
|
||||
isError: false,
|
||||
})).toBeUndefined()
|
||||
expect(await presentResult('read', { file_path: 'a.ts' }, {
|
||||
content: [{ type: 'text', text: '<path>/tmp/a.ts</path>\n<type>file</type>\n<content>\n1: hello\n</content>' }],
|
||||
isError: false,
|
||||
meta: { path: '/tmp/a.ts', lines: 'nope', totalLines: 1 },
|
||||
})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('read: completed presentation declines errors and non-single-text content', async () => {
|
||||
const envelope = '<path>/tmp/a.txt</path>\n<type>file</type>\n<content>\nbody\n</content>'
|
||||
const meta = { path: '/tmp/a.txt', offset: 1, lines: [{ number: 1, text: 'body' }], totalLines: 1 }
|
||||
expect(await presentResult('read', { file_path: 'a.txt' }, {
|
||||
content: [{ type: 'text', text: envelope }],
|
||||
isError: true,
|
||||
meta,
|
||||
})).toBeUndefined()
|
||||
expect(await presentResult('read', { file_path: 'a.txt' }, {
|
||||
content: [{ type: 'text', text: envelope }, { type: 'text', text: 'second' }],
|
||||
isError: false,
|
||||
meta,
|
||||
})).toBeUndefined()
|
||||
expect(await presentResult('read', { file_path: 'a.txt' }, {
|
||||
content: [{ type: 'reasoning', text: envelope }],
|
||||
isError: false,
|
||||
meta,
|
||||
})).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user