Files
bowenliang123 a3f572244b feat(client): drop the collapsed file row's error dot
Failures now surface only on the expanded op rows. The collapsed row's
aggregate tally added little on its own (its edit ops already flag the failing
tool call), so FileEntry.errs — which existed only for that dot — and the
files.errs strings go with it; each op keeps its own err flag for the expanded
dots.
2026-10-19 10:39:53 +08:00

863 lines
39 KiB
TypeScript

// File activity derivation (src/client/fileActivity.ts) — the pure fold behind the File Activity card.
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { absPathOf, activityOf, activityOfOps, displayPathOf, formOf, glyphOf, locateStepOf, previewAddressOf } from '../../src/client/fileActivity'
import { kindOfCall, kindOfTool, linesOf, pathOfArgs } from '../../src/shared/fileOps'
import type { FileActivity, FileOp } from '../../src/client/fileActivity'
import type { ConversationNodeLike } from '../../src/client/services'
import type { RequestRecord, SurfaceNode } from '../../src/shared/types'
const T0 = 1700000000000
/** A tool-result surface node plus its conversation join carrying the call args. */
function op(
seq: number,
tool: string,
args: Record<string, unknown> | null,
extra: Partial<SurfaceNode> = {},
convExtra: Partial<ConversationNodeLike> = {},
): { node: SurfaceNode; conv: ConversationNodeLike } {
return {
node: { seq, cat: 'tool', tokens: 5, tool, time: T0 + seq, ...extra },
conv: {
kind: 'tool',
seq,
call: args !== null ? { name: tool, argsRaw: JSON.stringify(args) } : null,
...convExtra,
},
}
}
function run(ops: { node: SurfaceNode; conv: ConversationNodeLike }[], before: number | null = null): FileActivity {
const bySeq = new Map(ops.map(o => [o.node.seq, o.conv]))
return activityOf(ops.map(o => o.node), seq => bySeq.get(seq), before)
}
describe('kindOfTool', () => {
test('maps the known file tools to purposes; everything else is not a file op', () => {
assert.equal(kindOfTool('read'), 'read')
assert.equal(kindOfTool('read_image'), 'read')
assert.equal(kindOfTool('write'), 'write')
assert.equal(kindOfTool('edit'), 'write')
assert.equal(kindOfTool('grep'), 'search')
assert.equal(kindOfTool('glob'), 'search')
assert.equal(kindOfTool('bash'), null)
assert.equal(kindOfTool(undefined), null)
})
})
describe('pathOfArgs', () => {
test('null or target-less args yield no path', () => {
assert.equal(pathOfArgs('read', null), null)
assert.equal(pathOfArgs('read', {}), null)
assert.equal(pathOfArgs('read', { file_path: '' }), null)
assert.equal(pathOfArgs('read', { file_path: 42 }), null)
})
test('read/write tools take the first path-ish argument', () => {
assert.equal(pathOfArgs('read', { file_path: '/a.ts' }), '/a.ts')
assert.equal(pathOfArgs('edit', { filePath: '/b.ts' }), '/b.ts')
assert.equal(pathOfArgs('write', { path: '/c.ts' }), '/c.ts')
})
test('searches prefer the narrowing path and fall back to the pattern', () => {
assert.equal(pathOfArgs('grep', { pattern: 'foo', path: '/src' }), '/src')
assert.equal(pathOfArgs('grep', { pattern: 'foo' }), 'foo')
assert.equal(pathOfArgs('grep', { pattern: 'foo', path: '' }), 'foo')
assert.equal(pathOfArgs('glob', { pattern: '**/*.ts' }), '**/*.ts')
assert.equal(pathOfArgs('glob', {}), null)
})
})
describe('linesOf', () => {
test('counts rendered lines; a trailing newline closes its own line', () => {
assert.equal(linesOf(''), 0)
assert.equal(linesOf('a'), 1)
assert.equal(linesOf('a\n'), 1)
assert.equal(linesOf('a\nb'), 2)
})
})
describe('formOf', () => {
test('multimodal reads and image extensions scan apart; trailing slashes mark directories', () => {
assert.equal(formOf('read_image', '/x.bin'), 'image')
assert.equal(formOf('read', '/pic.PNG'), 'image')
assert.equal(formOf('grep', '/src/'), 'dir')
assert.equal(formOf('read', '/a.ts'), 'text')
})
})
describe('activityOf', () => {
test('aggregates reads, writes, and searches per file with counts and line deltas', () => {
const a = run([
op(3, 'read', { file_path: '/src/a.ts' }),
op(5, 'edit', { file_path: '/src/a.ts', old_string: 'x\ny\nz', new_string: 'x\nq' }),
op(7, 'read', { file_path: '/src/a.ts' }),
op(9, 'write', { file_path: '/src/b.ts', content: 'one\ntwo\n' }),
op(11, 'grep', { pattern: 'needle', path: '/src' }),
op(13, 'glob', { pattern: '**/*.md' }),
])
assert.equal(a.entries.length, 4)
// Most-recently-touched first: **/*.md (seq 13) leads, /src/b.ts (9) follows.
assert.deepEqual(a.entries.map(e => e.path), ['**/*.md', '/src', '/src/b.ts', '/src/a.ts'])
const file = a.entries[3]
assert.equal(file.reads, 2)
assert.equal(file.writes, 1)
assert.equal(file.searches, 0)
assert.equal(file.added, 2) // edit new_string: 'x\nq'
assert.equal(file.removed, 3) // edit old_string: 'x\ny\nz'
// Ops newest first inside the entry.
assert.deepEqual(file.ops.map(o => o.seq), [7, 5, 3])
assert.equal(file.ops[0].kind, 'read')
assert.equal(file.ops[0].time, T0 + 7)
const written = a.entries[2]
assert.equal(written.added, 2) // write content: 'one\ntwo\n'
assert.equal(written.removed, 0)
// A search naming both a path and a pattern keeps the pattern as the op detail.
const searched = a.entries[1]
assert.equal(searched.searches, 1)
assert.equal(searched.ops[0].detail, 'needle')
// A pattern-only search's target IS the pattern, with no separate detail.
assert.equal(a.entries[0].form, 'text')
assert.equal(a.entries[0].ops[0].detail, undefined)
assert.deepEqual(a.totals, {
read: { files: 1, ops: 2 },
write: { files: 2, ops: 2 },
search: { files: 2, ops: 2 },
image: { files: 0, ops: 0 },
added: 4,
removed: 3,
})
})
test('flags multimodal reads and directory targets, and totals image ops', () => {
const a = run([
op(3, 'read_image', { file_path: '/shots/ui.png' }),
op(5, 'read', { file_path: '/shots/logo.svg' }),
op(7, 'grep', { pattern: 'x', path: '/src/' }),
])
assert.equal(a.entries[2].form, 'image') // read_image
assert.equal(a.entries[1].form, 'image') // .svg extension
assert.equal(a.entries[0].form, 'dir') // trailing slash
assert.deepEqual(a.totals.image, { files: 2, ops: 2 })
})
test('stamps the error flag from the fold or the snapshot', () => {
const a = run([
op(3, 'read', { file_path: '/a.ts' }, { err: true }),
op(5, 'read', { file_path: '/a.ts' }, {}, { isError: true }),
op(7, 'read', { file_path: '/b.ts' }),
])
assert.deepEqual(a.entries[1].ops.map(o => o.err), [true, true])
assert.equal(a.entries[0].ops[0].err, false)
})
test('carries the archive removal stamp onto the op for locate targeting', () => {
const a = run([op(3, 'read', { file_path: '/a.ts' }, { gone: 20 })])
assert.equal(a.entries[0].ops[0].gone, 20)
})
test('the before bound excludes later ops; null serves everything', () => {
const ops = [op(3, 'read', { file_path: '/a.ts' }), op(9, 'read', { file_path: '/b.ts' })]
assert.equal(run(ops, 9).entries.length, 1)
assert.equal(run(ops, 9).entries[0].path, '/a.ts')
assert.equal(run(ops, null).entries.length, 2)
})
test('non-file tools and non-tool nodes never enter the fold', () => {
const a = run([op(3, 'bash', { command: 'ls', description: 'List files' })])
const withNoise = activityOf(
[
{ seq: 1, cat: 'user', tokens: 5 },
{ seq: 2, cat: 'assistant', tokens: 5 },
{ seq: 3, cat: 'tool', tokens: 5 }, // no tool name at all
{ seq: 4, cat: 'tool', tokens: 5, tool: 'todo_write' },
],
() => undefined,
null,
)
assert.equal(a.entries.length, 0)
assert.deepEqual(withNoise.entries, [])
assert.equal(withNoise.totals.read.ops, 0)
})
test('file-kind ops without a resolvable path are skipped — never counted, never rowed', () => {
const a = run([
op(3, 'read', null), // the conversation join aged out
op(5, 'edit', {}), // args survived but name no target
op(7, 'read', { file_path: '/a.ts' }),
])
assert.equal(a.entries.length, 1)
assert.deepEqual(a.entries[0].ops.map(o => o.seq), [7])
assert.equal(a.totals.read.ops, 1)
assert.equal(a.totals.write.ops, 0)
assert.equal(a.totals.read.files, 1)
})
test('a file-kind node the join lost entirely skips without throwing', () => {
// The regression this locks: with the conv node gone (aged window), the
// fold must skip the node — not dereference it and take the tab down.
const a = activityOf(
[
{ seq: 3, cat: 'tool', tokens: 5, tool: 'read', time: T0 },
{ seq: 5, cat: 'tool', tokens: 5, tool: 'run_code', time: T0 },
],
() => undefined,
null,
)
assert.deepEqual(a.entries, [])
})
test('one hostile join node cannot take the walk down', () => {
const hostile: ConversationNodeLike = { kind: 'tool', seq: 3 }
Object.defineProperty(hostile, 'call', { get(): never { throw new Error('boom') } })
const healthy = op(5, 'read', { file_path: '/a.ts' })
const bySeq = new Map<number, ConversationNodeLike>([[3, hostile], [5, healthy.conv]])
const a = activityOf(
[
{ seq: 3, cat: 'tool', tokens: 5, tool: 'read', time: T0 },
healthy.node,
],
seq => bySeq.get(seq),
null,
)
// The throwing node drops; the walk carries on with the next node.
assert.deepEqual(a.entries.map(e => e.path), ['/a.ts'])
assert.equal(a.totals.read.ops, 1)
})
test('an edit with missing strings contributes no line delta', () => {
const a = run([op(3, 'edit', { file_path: '/a.ts', new_string: 'only\nadded' })])
assert.equal(a.entries[0].added, 2)
assert.equal(a.entries[0].removed, 0)
const b = run([op(3, 'edit', { file_path: '/a.ts', old_string: 'gone' })])
assert.equal(b.entries[0].added, 0)
assert.equal(b.entries[0].removed, 1)
const c = run([op(3, 'write', { file_path: '/a.ts' })])
assert.equal(c.entries[0].added, 0)
})
test('reads report the exact window off the result meta, else the limit estimate', () => {
const window = Array.from({ length: 30 }, (_, i) => ({ number: 41 + i, text: 'x' }))
const a = run([
op(3, 'read', { file_path: '/a.ts', offset: 41, limit: 80 }, {}, {
meta: { path: '/a.ts', offset: 41, totalLines: 500, lines: window },
}),
op(5, 'read', { file_path: '/b.ts', limit: 80 }),
op(7, 'read', { file_path: '/c.ts' }),
op(9, 'read', { file_path: '/d.ts', limit: 0 }),
op(11, 'read', { file_path: '/e.ts', limit: 'many' }),
op(13, 'read', { file_path: '/f.ts', limit: 80 }, {}, { meta: { shape: 'nope' } }),
op(15, 'read', { file_path: '/g.ts', limit: 80 }, {}, { meta: { path: '/g.ts', offset: 1, lines: [] } }),
op(17, 'read', { file_path: '/h.ts', limit: 80 }, {}, { meta: { path: '/h.ts', offset: 0, lines: window } }),
])
// Entries are newest-first: h(17) g(15) f(13) e(11) d(9) c(7) b(5) a(3).
assert.deepEqual(a.entries[7].ops[0].read, { start: 41, count: 30 })
for (const i of [0, 1, 2, 6]) assert.deepEqual(a.entries[i].ops[0].read, { count: 80, est: true })
for (const i of [3, 4, 5]) assert.equal(a.entries[i].ops[0].read, undefined)
// A nested read has no result meta in the join — it estimates from the limit.
const nested = run([op(20, 'run_code', null, {}, { subCalls: [sub(15, 'read', { file_path: '/n.ts', limit: 50.9 })] })])
assert.deepEqual(nested.entries[0].ops[0].read, { count: 50, est: true })
})
test('nodes without a timestamp carry ops without one', () => {
const a = run([op(3, 'read', { file_path: '/a.ts' }, { time: undefined })])
assert.equal(a.entries[0].ops[0].time, undefined)
})
test('a search with a path but no pattern carries no detail', () => {
const a = run([
op(3, 'grep', { path: '/src' }),
// An empty path narrows nothing: the target falls back to the pattern.
op(5, 'grep', { pattern: 'y', path: '' }),
])
assert.equal(a.entries[1].path, '/src')
assert.equal(a.entries[1].ops[0].detail, undefined)
assert.equal(a.entries[0].path, 'y')
assert.equal(a.entries[0].ops[0].detail, undefined)
})
})
describe('locateStepOf', () => {
const requests = [{ seq: 10 }, { seq: 20 }, { seq: 30 }] as RequestRecord[]
test('the first request dispatched after the op — while alive — shows it', () => {
assert.equal(locateStepOf(requests, 5, undefined), 10)
assert.equal(locateStepOf(requests, 15, 25), 20)
// Removed before a step's dispatch: that step's surface never held it.
assert.equal(locateStepOf(requests, 5, 10), null)
assert.equal(locateStepOf(requests, 5, 8), null)
})
test('a live node no request has consumed reveals on the live surface', () => {
assert.equal(locateStepOf(requests, 35, undefined), 'live')
assert.equal(locateStepOf([], 1, undefined), 'live')
})
test('a re-ordered wire still resolves the first request logged after the node', () => {
assert.equal(locateStepOf([{ seq: 30 }, { seq: 10 }, { seq: 20 }] as RequestRecord[], 5, undefined), 10)
})
})
/** One settled nested sub-call block, as the conversation join delivers it. */
function sub(seq: number, name: string, args: Record<string, unknown> | string, over: Record<string, unknown> = {}): Record<string, unknown> {
return {
kind: 'tool-result',
seq,
time: T0 + seq,
call: { name, argsRaw: typeof args === 'string' ? args : JSON.stringify(args) },
isError: false,
subCalls: [],
...over,
}
}
describe('kindOfCall', () => {
test('str_replace_editor reads on view and writes otherwise; every other tool keeps its static kind', () => {
assert.equal(kindOfCall('str_replace_editor', { command: 'view' }), 'read')
assert.equal(kindOfCall('str_replace_editor', { command: 'create' }), 'write')
assert.equal(kindOfCall('str_replace_editor', { command: 'str_replace' }), 'write')
assert.equal(kindOfCall('str_replace_editor', { command: 'insert' }), 'write')
assert.equal(kindOfCall('str_replace_editor', { command: 'explode' }), 'write')
assert.equal(kindOfCall('str_replace_editor', null), 'write')
assert.equal(kindOfCall('read', null), 'read')
assert.equal(kindOfCall('bash', { command: 'ls' }), null)
})
})
describe('activityOf — str_replace_editor', () => {
test('view reads; create, str_replace, and insert write with their own line deltas', () => {
const a = run([
op(3, 'str_replace_editor', { command: 'view', path: '/a.py' }),
op(5, 'str_replace_editor', { command: 'create', path: '/b.py', file_text: 'one\ntwo\n' }),
op(7, 'str_replace_editor', { command: 'str_replace', path: '/b.py', old_str: 'one\ntwo', new_str: 'one' }),
op(9, 'str_replace_editor', { command: 'insert', path: '/c.py', insert_line: 1, new_str: 'x\ny' }),
])
assert.deepEqual(a.entries.map(e => e.path), ['/c.py', '/b.py', '/a.py'])
const b = a.entries[1]
assert.equal(b.reads, 0)
assert.equal(b.writes, 2)
assert.equal(b.added, 3) // create 'one\ntwo\n' + str_replace 'one'
assert.equal(b.removed, 2) // str_replace 'one\ntwo'
assert.equal(a.entries[2].reads, 1)
assert.equal(a.entries[0].added, 2) // insert 'x\ny'
assert.deepEqual(a.totals.read, { files: 1, ops: 1 })
// Writes: /b.py (create + str_replace) and /c.py (insert).
assert.deepEqual(a.totals.write, { files: 2, ops: 3 })
})
test('a stringless call rows as a failed-looking but zero-delta write', () => {
const a = run([
op(3, 'str_replace_editor', { command: 'str_replace', path: '/a.py' }),
op(5, 'str_replace_editor', { path: '/b.py' }),
])
assert.ok(a.entries.every(e => e.writes === 1 && e.added === 0 && e.removed === 0))
})
})
describe('activityOf — nested Code-Mode (PTC) calls', () => {
test('a run_code result folds its settled sub-dispatches into ops attributed to the nested tools', () => {
const a = run([
op(30, 'run_code', { code: 'await tools.read(…)', description: 'Read failing test and its fixture' }, {}, {
subCalls: [
sub(20, 'read', { file_path: '/src/a.ts' }),
sub(22, 'edit', { file_path: '/src/a.ts', old_string: 'x\ny', new_string: 'z' }),
sub(24, 'write', { file_path: '/src/b.ts', content: 'one\ntwo\n' }),
sub(26, 'grep', { pattern: 'needle', path: '/src' }),
sub(28, 'bash', { command: 'ls' }),
// A minimal settled block: no time, no error flag, no nested tree.
{ kind: 'tool-result', seq: 27, call: { name: 'read', argsRaw: JSON.stringify({ file_path: '/src/a.ts' }) } },
],
}),
])
// /src/a.ts leads: its newest op is the minimal read at seq 27.
assert.deepEqual(a.entries.map(e => e.path), ['/src/a.ts', '/src', '/src/b.ts'])
const file = a.entries[0]
assert.equal(file.reads, 2)
assert.equal(file.writes, 1)
const read = file.ops.find(o => o.seq === 20)
assert.equal(read?.parent, 30)
assert.equal(read?.program, 'Read failing test and its fixture')
assert.equal(read?.time, T0 + 20)
const minimal = file.ops.find(o => o.seq === 27)
assert.equal(minimal?.time, undefined)
assert.equal(minimal?.err, false)
assert.deepEqual(a.totals, {
read: { files: 1, ops: 2 },
write: { files: 2, ops: 2 },
search: { files: 1, ops: 1 },
image: { files: 0, ops: 0 },
added: 3,
removed: 2,
})
})
test('a program without a description rows its nested ops without one', () => {
const a = run([op(30, 'run_code', { code: 'x' }, {}, { subCalls: [sub(20, 'read', { file_path: '/a.ts' })] })])
assert.equal(a.entries[0].ops[0].program, undefined)
})
test('the sub-call error flag and the parent removal stamp ride the op', () => {
const a = run([op(30, 'run_code', { code: 'x' }, { gone: 40 }, {
subCalls: [
sub(20, 'read', { file_path: '/a.ts' }, { isError: true }),
sub(22, 'read', { file_path: '/b.ts' }),
],
})])
assert.deepEqual(a.entries.map(e => e.path), ['/b.ts', '/a.ts'])
assert.equal(a.entries[1].ops[0].err, true)
// The parent's stamp locates the op; a failed program does not fail its earlier successful reads.
const ok = a.entries[0].ops[0]
assert.equal(ok.err, false)
assert.equal(ok.gone, 40)
const parentOnly = run([op(30, 'run_code', { code: 'x' }, { err: true }, { isError: true, subCalls: [sub(20, 'read', { file_path: '/c.ts' })] })])
assert.equal(parentOnly.entries[0].ops[0].err, false)
})
test('the parent seq gates nested ops at the before bound', () => {
const runCode = op(30, 'run_code', { code: 'x' }, {}, { subCalls: [sub(20, 'read', { file_path: '/a.ts' })] })
assert.equal(run([runCode], 30).entries.length, 0)
assert.equal(run([runCode], 31).entries.length, 1)
})
test('nested searches keep the pattern detail only when the path narrows', () => {
const a = run([op(30, 'run_code', null, {}, { subCalls: [
sub(20, 'grep', { pattern: 'needle', path: '/src' }),
sub(21, 'glob', { pattern: '**/*.md' }),
] })])
assert.equal(a.entries[0].ops[0].detail, undefined) // the pattern IS the row path
assert.equal(a.entries[1].ops[0].detail, 'needle')
})
test('a nested image read keeps the image form and totals', () => {
const a = run([op(30, 'run_code', null, {}, { subCalls: [sub(20, 'read_image', { file_path: '/ui.png' })] })])
assert.equal(a.entries[0].form, 'image')
assert.deepEqual(a.totals.image, { files: 1, ops: 1 })
})
test('running and malformed sub-call blocks drop out instead of throwing', () => {
const a = run([op(30, 'run_code', null, {}, {
subCalls: [
'nope',
null,
42,
{ call: { name: 'read', argsRaw: '{}' }, seq: 21 }, // no kind — still running
{ kind: 'tool-result', seq: 22, call: null }, // unpaired result
{ kind: 'tool-result', seq: 23, call: 'x' }, // call not an object
{ kind: 'tool-result', seq: 24, call: { name: 'read' } }, // argsRaw missing
{ kind: 'tool-result', seq: 25, call: { name: 9, argsRaw: '{}' } }, // name not a string
{ kind: 'tool-result', call: { name: 'read', argsRaw: '{}' } }, // seq missing
{ kind: 'tool-result', seq: Number.NaN, call: { name: 'read', argsRaw: '{}' } }, // non-finite seq
sub(26, 'read', 'not-json'), // unparseable args
sub(27, 'read', {}), // parsed but target-less
sub(28, 'read', { file_path: '/ok.ts' }),
],
})])
assert.deepEqual(a.entries.map(e => e.path), ['/ok.ts'])
assert.equal(a.totals.read.ops, 1)
})
test('the depth cap and the cycle guard bound defensive trees', () => {
// A 10-deep chain books only the first eight levels.
let chain = sub(99, 'read', { file_path: '/deep.ts' })
for (let s = 98; s >= 90; s--) chain = { ...sub(s, 'read', { file_path: '/deep.ts' }), subCalls: [chain] }
const deep = run([op(100, 'run_code', null, {}, { subCalls: [chain] })])
assert.equal(deep.entries[0].reads, 8)
// A cyclic join terminates and books each block once.
const x = sub(80, 'read', { file_path: '/cyc.ts' })
const y = sub(81, 'read', { file_path: '/cyc.ts' })
y.subCalls = [x]
x.subCalls = [y]
const cyclic = run([op(82, 'run_code', null, {}, { subCalls: [x] })])
assert.equal(cyclic.entries[0].reads, 2)
})
test('a join without subCalls — aged out, or an older client — folds nothing extra', () => {
const a = run([op(30, 'run_code', { code: 'x' })])
assert.deepEqual(a.entries, [])
})
})
describe('activityOf — search meta attribution', () => {
test('a complete grep meta rows per matched file with hit counts and the pattern detail', () => {
const a = run([
op(10, 'grep', { pattern: 'needle', path: '/src' }, {}, {
meta: { shape: 'matches', truncated: false, total: 3, files: [
{ path: '/src/a.ts', matches: [{ lineNumber: 1, line: 'needle' }, { lineNumber: 2, line: 'needle!' }] },
{ path: '/src/b.ts', matches: [{ lineNumber: 5, line: 'needle' }] },
] },
}),
// A capped meta names a partial file set: the call falls back to its own target.
op(12, 'grep', { pattern: 'broad' }, {}, {
meta: { shape: 'matches', truncated: true, total: 500, files: [{ path: '/src/a.ts', matches: [{ lineNumber: 1, line: 'x' }] }] },
}),
// An archived, timeless search still stamps its meta rows with the parent's removal.
op(14, 'grep', { pattern: 'old' }, { gone: 20, time: undefined }, {
meta: { shape: 'paths', truncated: false, total: 1, paths: ['/old.ts'] },
}),
])
// The searched target rows too: '/src' (the narrowed path) and the two pathless patterns ride ahead of their hit files.
assert.deepEqual(a.entries.map(e => e.path), ['old', '/old.ts', 'broad', '/src', '/src/a.ts', '/src/b.ts'])
const byPath = new Map(a.entries.map(e => [e.path, e]))
const srcA = byPath.get('/src/a.ts')!
assert.equal(srcA.searches, 1)
assert.equal(srcA.ops[0].hits, 2)
assert.equal(srcA.ops[0].detail, 'needle')
assert.equal(byPath.get('/src/b.ts')!.ops[0].hits, 1)
// The searched directory's row carries the pattern detail.
assert.equal(byPath.get('/src')!.ops[0].detail, 'needle')
// The archived, timeless meta row carries the node's stamp (and no time).
assert.equal(byPath.get('/old.ts')!.ops[0].gone, 20)
assert.equal(byPath.get('/old.ts')!.ops[0].time, undefined)
assert.deepEqual(a.totals.search, { files: 6, ops: 6 })
})
test('an include filter joins the detail; glob metas row without hit counts', () => {
const a = run([
op(10, 'grep', { pattern: 'needle', include: '*.ts' }, {}, {
meta: { shape: 'matches', truncated: false, total: 1, files: [{ path: '/a.ts', matches: [{ lineNumber: 1, line: 'x' }] }] },
}),
op(12, 'glob', { pattern: '**/*.md' }, {}, {
meta: { shape: 'paths', truncated: false, total: 2, paths: ['/r.md', '/o.md'] },
}),
])
// Each call rows its target (the pattern) plus the hit files.
assert.deepEqual(a.entries.map(e => e.path), ['**/*.md', '/r.md', '/o.md', 'needle', '/a.ts'])
const byPath = new Map(a.entries.map(e => [e.path, e]))
assert.equal(byPath.get('/a.ts')!.ops[0].detail, 'needle (*.ts)')
assert.equal(byPath.get('/o.md')!.ops[0].hits, undefined) // a listed path matched no lines
assert.equal(byPath.get('/o.md')!.ops[0].detail, '**/*.md')
assert.equal(byPath.get('needle')!.ops[0].pattern, true, 'the pathless target is a pattern row')
})
test('malformed or empty metas fall back to the pattern row', () => {
const metas: unknown[] = [
'junk',
42,
{ truncated: false, paths: ['/a.ts'] }, // shape missing
{ shape: 'other', truncated: false, paths: ['/a.ts'] }, // unknown shape
{ shape: 'matches', truncated: false }, // files missing
{ shape: 'matches', truncated: false, files: 'x' }, // files not a list
{ shape: 'matches', truncated: false, files: [] }, // nothing retained
{ shape: 'matches', truncated: false, files: [null, 7, { path: '' }, { path: '/a.ts' }, { path: '/a.ts', matches: 'x' }] }, // all groups malformed
{ shape: 'paths', truncated: false }, // paths missing
{ shape: 'paths', truncated: false, paths: [] }, // nothing listed
]
for (const [i, meta] of metas.entries()) {
const a = run([op(10, 'grep', { pattern: 'needle', path: '/src' }, {}, { meta })])
assert.deepEqual(a.entries.map(e => e.path), ['/src'], `meta case ${i} must fall back`)
}
})
test('one valid path among malformed siblings still attributes', () => {
const a = run([op(10, 'grep', { pattern: 'needle', path: '/src' }, {}, {
meta: { shape: 'paths', truncated: false, total: 2, paths: ['/a.ts', ''] },
})])
// The valid sibling attributes; the searched path rows as the call's target.
assert.deepEqual(a.entries.map(e => e.path), ['/src', '/a.ts'])
})
test('a truncated flag is required — a missing one falls back', () => {
const a = run([op(10, 'grep', { pattern: 'n', path: '/src' }, {}, {
meta: { shape: 'paths', paths: ['/a.ts'] },
})])
assert.deepEqual(a.entries.map(e => e.path), ['/src'])
})
test('meta attribution survives an aged-out call head — no args, no pattern detail', () => {
const a = run([op(10, 'grep', null, {}, {
meta: { shape: 'paths', truncated: false, total: 1, paths: ['/only.md'] },
})])
assert.deepEqual(a.entries.map(e => e.path), ['/only.md'])
assert.equal(a.entries[0].ops[0].detail, undefined)
})
})
describe('glyphOf', () => {
test('image form wins over everything the path says', () => {
assert.deepEqual(glyphOf('/a/data.csv', 'image'), { glyph: '🖼', tip: 'files.form.image' })
})
test('directories match their last segment, case-insensitively', () => {
const cases: [string, string, string][] = [
['/', '🏠', 'files.glyph.root'],
['.', '🏠', 'files.glyph.root'],
['/repo/tests/', '🧪', 'files.glyph.tests'],
['/repo/TESTS/', '🧪', 'files.glyph.tests'],
['/repo/packages/a/tests/', '🧪', 'files.glyph.tests'],
['/docs/', '📚', 'files.glyph.docs'],
['/repo/node_modules/', '📦', 'files.glyph.deps'],
['/repo/dist/', '🏗️', 'files.glyph.build'],
['/repo/scripts/', '🛠️', 'files.glyph.scripts'],
['/repo/config/', '⚙️', 'files.glyph.config'],
['/repo/assets/', '🎨', 'files.glyph.assets'],
['/repo/.github/', '🗄️', 'files.glyph.hidden'],
['/repo/src/', '📁', 'files.form.dir'],
]
for (const [path, glyph, tip] of cases) assert.deepEqual(glyphOf(path, 'dir'), { glyph, tip }, path)
})
test('a Windows-style path keys on its backslash leaf too', () => {
assert.deepEqual(glyphOf('C:\\repo\\.gitignore', 'text'), { glyph: '🚫', tip: 'files.glyph.ignore' })
})
test('files resolve special names before extensions, and buckets before the fallback', () => {
const cases: [string, string, string][] = [
['pnpm-lock.yaml', '🔒', 'files.glyph.lock'],
['package-lock.json', '🔒', 'files.glyph.lock'],
['Cargo.lock', '🔒', 'files.glyph.lock'],
['a.test.ts', '🧪', 'files.glyph.tests'],
['foo_test.py', '🧪', 'files.glyph.tests'],
['test_foo.py', '🧪', 'files.glyph.tests'],
['Dockerfile', '🐳', 'files.glyph.docker'],
['Dockerfile.dev', '🐳', 'files.glyph.docker'],
['Makefile', '🛠️', 'files.glyph.scripts'],
['CMakeLists.txt', '🛠️', 'files.glyph.scripts'],
['.gitignore', '🚫', 'files.glyph.ignore'],
['LICENSE.md', '⚖️', 'files.glyph.license'],
['COPYING', '⚖️', 'files.glyph.license'],
['.env', '⚙️', 'files.glyph.config'],
['tsconfig.yaml', '⚙️', 'files.glyph.config'],
['config.env', '⚙️', 'files.glyph.config'],
['manifest.json', '🧾', 'files.glyph.data'],
['README.md', '📝', 'files.glyph.markdown'],
['README.MD', '📝', 'files.glyph.markdown'],
['server.log', '📜', 'files.glyph.log'],
['exports.csv', '📊', 'files.glyph.sheet'],
['report.xlsx', '📊', 'files.glyph.sheet'],
['manual.pdf', '📕', 'files.glyph.document'],
['bundle.tar.gz', '🗜️', 'files.glyph.archive'],
['inter.woff2', '🔤', 'files.glyph.font'],
['clip.mp4', '🎬', 'files.glyph.media'],
['README', '📄', 'files.form.text'],
['data.parquet', '📄', 'files.form.text'],
['.foo', '📄', 'files.form.text'],
]
for (const [base, glyph, tip] of cases) assert.deepEqual(glyphOf('/x/' + base, 'text'), { glyph, tip }, base)
})
test('code files render letter badges over their language color, with readable text', () => {
const cases: [string, string, string, string][] = [
['main.ts', 'TS', '#3178c6', 'files.glyph.lang.ts'],
['App.tsx', 'TSX', '#3178c6', 'files.glyph.lang.ts'],
['index.js', 'JS', '#f7df1e', 'files.glyph.lang.js'],
['a.spec.js', 'JS', '#f7df1e', 'files.glyph.lang.js'],
['app.py', 'PY', '#3572a5', 'files.glyph.python'],
['notebook.ipynb', 'NB', '#da5b0b', 'files.glyph.notebook'],
['main.go', 'GO', '#00add8', 'files.glyph.lang.go'],
['lib.rs', 'RS', '#dea584', 'files.glyph.lang.rust'],
['App.java', 'JV', '#b07219', 'files.glyph.lang.java'],
['Main.kt', 'KT', '#a97bff', 'files.glyph.lang.kotlin'],
['app.rb', 'RB', '#701516', 'files.glyph.lang.ruby'],
['db.php', 'PHP', '#4f5d95', 'files.glyph.lang.php'],
['mem.c', 'C', '#555555', 'files.glyph.lang.c'],
['util.h', 'C', '#555555', 'files.glyph.lang.c'],
['graph.cpp', 'C++', '#f34b7d', 'files.glyph.lang.cpp'],
['Repo.cs', 'C#', '#178600', 'files.glyph.lang.csharp'],
['Router.scala', 'SC', '#c22d40', 'files.glyph.lang.scala'],
['conf.lua', 'LUA', '#000080', 'files.glyph.lang.lua'],
['cli.dart', 'DA', '#00b4ab', 'files.glyph.lang.dart'],
['App.swift', 'SW', '#f05138', 'files.glyph.lang.swift'],
['App.vue', 'VUE', '#41b883', 'files.glyph.lang.vue'],
['view.svelte', 'SV', '#ff3e00', 'files.glyph.lang.svelte'],
['deploy.sh', 'SH', '#89e051', 'files.glyph.shell'],
['index.html', 'HT', '#e34c26', 'files.glyph.lang.html'],
['main.css', 'CSS', '#563d7c', 'files.glyph.style'],
['schema.sql', 'SQL', '#e38c00', 'files.glyph.database'],
]
for (const [base, glyph, color, tip] of cases) {
const g = glyphOf('/x/' + base, 'text')
assert.equal(g.glyph, glyph, base)
assert.equal(g.tip, tip, base)
assert.equal(g.color, color, base)
assert.ok(g.text !== undefined, `badge text color missing for ${base}`)
}
// The text color contrasts with the fill: near-black on light shades (the
// JS yellow, shell green, rust peach), white on dark ones (including the kotlin lilac, just under the threshold).
assert.equal(glyphOf('/x/index.js', 'text').text, '#1f2328')
assert.equal(glyphOf('/x/deploy.sh', 'text').text, '#1f2328')
assert.equal(glyphOf('/x/lib.rs', 'text').text, '#1f2328')
assert.equal(glyphOf('/x/Main.kt', 'text').text, '#ffffff')
assert.equal(glyphOf('/x/main.ts', 'text').text, '#ffffff')
// An extension outside the badge table is not a known language: plain fallback.
assert.deepEqual(glyphOf('/x/build.zig', 'text'), { glyph: '📄', tip: 'files.form.text' })
})
test('near-miss names do not trip the test-file rule', () => {
for (const base of ['latest.ts', 'contest.py', 'attest.json']) {
const g = glyphOf('/x/' + base, 'text')
assert.notEqual(g.tip, 'files.glyph.tests', base)
}
})
})
describe('displayPathOf', () => {
const ws = '/Users/bw/dev/dsh-context'
test('workspace and relative paths display bare-relative; the workspace root itself keeps ./', () => {
assert.equal(displayPathOf(ws + '/src/client/a.ts', ws), 'src/client/a.ts')
// The workspace root has no relative remainder: the bare dot form stays.
assert.equal(displayPathOf(ws, ws), './')
assert.equal(displayPathOf(ws + '/', ws), './')
assert.equal(displayPathOf('tests/client/a.spec.ts', ws), 'tests/client/a.spec.ts')
assert.equal(displayPathOf('./tests/a.ts', ws), 'tests/a.ts')
// A recorded absolute path carrying an embedded `./` relativizes through it too.
assert.equal(displayPathOf('/repo/./a.ts', '/repo'), 'a.ts')
})
test('everything else stays verbatim', () => {
assert.equal(displayPathOf('/etc/hosts', ws), '/etc/hosts')
assert.equal(displayPathOf(ws + '-sibling/a.ts', ws), ws + '-sibling/a.ts')
assert.equal(displayPathOf('C:\\repo\\a.ts', ws), 'C:\\repo\\a.ts')
assert.equal(displayPathOf('needle', ws), 'needle')
assert.equal(displayPathOf(ws + '/a.ts', undefined), ws + '/a.ts')
// Without a known workspace a relative path is still workspace-relative by definition.
assert.equal(displayPathOf('needle', undefined), 'needle')
assert.equal(displayPathOf('./needle', undefined), 'needle')
// A raw all-`./` degenerate leaves nothing to display: the raw form stays.
assert.equal(displayPathOf('./', ws), './')
assert.equal(displayPathOf('././', ws), '././')
})
})
describe('activityOf — pattern rows', () => {
test('a pathless search marks its pattern-as-target entry; a narrowed one does not', () => {
const a = run([
op(3, 'grep', { pattern: 'needle' }),
op(5, 'grep', { pattern: 'needle', path: '/src' }),
])
// Newest first: /src (5) leads, the pattern row (3) follows.
assert.equal(a.entries[0].pattern, undefined)
assert.equal(a.entries[1].pattern, true)
})
test('a nested Code-Mode pathless search is a pattern row too; its narrowed sibling is not', () => {
const a = run([op(30, 'run_code', null, {}, { subCalls: [
sub(20, 'grep', { pattern: 'needle' }),
sub(22, 'grep', { pattern: 'needle', path: '/src' }),
] })])
// Newest first: /src (22) leads, the pattern row (20) follows.
assert.equal(a.entries[0].pattern, undefined)
assert.equal(a.entries[1].pattern, true)
})
})
describe('absPathOf', () => {
test('absolute paths stay; relative paths resolve against the workspace root', () => {
const ws = '/Users/bw/dev/dsh-context'
assert.equal(absPathOf('/etc/hosts', ws), '/etc/hosts')
assert.equal(absPathOf('src/a.ts', ws), ws + '/src/a.ts')
assert.equal(absPathOf('src/a.ts', ws + '/'), ws + '/src/a.ts')
assert.equal(absPathOf('src/a.ts', undefined), undefined)
assert.equal(absPathOf('./src/a.ts', ws), undefined)
assert.equal(absPathOf('C:\\repo\\a.ts', ws), 'C:\\repo\\a.ts')
})
})
describe('previewAddressOf', () => {
test('a real file becomes its session-scoped resource address, workspace-relative inside the root', () => {
const ws = '/repo'
assert.equal(
previewAddressOf('src/a.ts', 'text', undefined, 's1', ws),
'dsh-resource://file/session/s1/src/a.ts',
)
// An absolute path inside the workspace collapses to the same address as
// its workspace-relative spelling (one file, one tab).
assert.equal(
previewAddressOf('/repo/src/a.ts', 'text', undefined, 's1', ws),
'dsh-resource://file/session/s1/src/a.ts',
)
// Outside the root the absolute path rides through unchanged.
assert.equal(
previewAddressOf('/etc/hosts', 'text', undefined, 's1', ws),
'dsh-resource://file/session/s1//etc/hosts',
)
// A missing workspace still names a session-scoped address.
assert.equal(
previewAddressOf('rel/b.md', 'text', undefined, 's1', undefined),
'dsh-resource://file/session/s1/rel/b.md',
)
// An image is a real file like any other.
assert.equal(
previewAddressOf('/shots/ui.png', 'image', undefined, 's1', ws),
'dsh-resource://file/session/s1//shots/ui.png',
)
})
test('pattern rows, directory targets, and a missing session have no address', () => {
assert.equal(previewAddressOf('needle', 'text', true, 's1', '/repo'), undefined)
assert.equal(previewAddressOf('/src/', 'dir', undefined, 's1', '/repo'), undefined)
assert.equal(previewAddressOf('src/a.ts', 'text', undefined, undefined, '/repo'), undefined)
assert.equal(previewAddressOf('src/a.ts', 'text', undefined, '', '/repo'), undefined)
})
test('a path the encoder rejects degrades to no address, never a throw', () => {
// A lone surrogate is unrepresentable in URI encoding.
assert.equal(previewAddressOf('\uD800', 'text', undefined, 's1', '/repo'), undefined)
})
})
describe('activityOfOps — the fold-derived op log', () => {
function op(over: Partial<FileOp> & { seq: number; path: string }): FileOp {
return { kind: 'read', tool: 'read', err: false, added: 0, removed: 0, ...over }
}
test('aggregates the fold-derived records per file (newest first), scoped by the exclusive bound', () => {
const ops = [
op({ seq: 1, path: 'a.ts', read: { start: 1, count: 5 } }),
op({ seq: 2, path: 'b.ts', kind: 'write', tool: 'write', added: 3 }),
op({ seq: 3, path: 'a.ts', kind: 'write', tool: 'edit', added: 1, removed: 2 }),
op({ seq: 4, path: 'c.ts', kind: 'search', tool: 'grep', hits: 7 }),
]
const all = activityOfOps(ops, [], null)
assert.deepEqual(all.entries.map(e => [e.path, e.reads, e.writes, e.searches]), [
['c.ts', 0, 0, 1],
['a.ts', 1, 1, 0],
['b.ts', 0, 1, 0],
])
assert.deepEqual([all.totals.read.ops, all.totals.write.ops, all.totals.search.ops], [1, 2, 1])
const a = all.entries[1]
assert.deepEqual(a.ops.map(o => o.seq), [3, 1], 'ops newest first within the file')
assert.deepEqual([a.added, a.removed], [1, 2])
// Scope: before = 3 drops seq 3+ (a nested op keys on its parent result).
const scoped = activityOfOps(ops, [], 3)
assert.deepEqual(scoped.entries.map(e => e.path).sort(), ['a.ts', 'b.ts'])
assert.deepEqual([scoped.totals.read.ops, scoped.totals.write.ops, scoped.totals.search.ops], [1, 1, 0])
})
test('a nested op scopes by its parent result seq (the legacy node-level rule)', () => {
const ops = [
op({ seq: 2, path: 'a.ts', parent: 5, program: 'p' }),
op({ seq: 3, path: 'b.ts' }),
]
// before = 5: the parent-keyed op (key 5) is out, the plain seq-3 op is in.
const scoped = activityOfOps(ops, [], 5)
assert.deepEqual(scoped.entries.map(e => e.path), ['b.ts'])
})
test('gone joins from the detail archive by the op node (or its parent), never overriding an op stamp', () => {
const ops = [
op({ seq: 1, path: 'a.ts' }),
op({ seq: 2, path: 'b.ts', gone: 50 }),
op({ seq: 3, path: 'c.ts', parent: 10 }),
]
const archive = [
{ seq: 1, cat: 'tool' as const, tokens: 1, gone: 40 },
{ seq: 2, cat: 'tool' as const, tokens: 1, gone: 99 },
{ seq: 10, cat: 'tool' as const, tokens: 1, gone: 77 },
{ seq: 30, cat: 'tool' as const, tokens: 1 }, // a live-window echo row: no stamp, joins nothing
]
const out = activityOfOps(ops, archive, null)
const byPath = new Map(out.entries.map(e => [e.path, e]))
assert.equal(byPath.get('a.ts')?.ops[0].gone, 40, 'the archive stamp joins')
assert.equal(byPath.get('b.ts')?.ops[0].gone, 50, 'the op\'s own stamp wins')
assert.equal(byPath.get('c.ts')?.ops[0].gone, 77, 'a nested op reads its parent\'s stamp')
})
})