init
This commit is contained in:
commit
0858862cda
17 files changed
+2189
No files matched your search
@@ -0,0 +1,106 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import {
|
||||
displayPath,
|
||||
facetContextText,
|
||||
facetRuleText,
|
||||
isWideningCall,
|
||||
outsideReason,
|
||||
pathArgumentsFor,
|
||||
widenReason,
|
||||
} from '../lib/scope.js'
|
||||
|
||||
test('pathArgumentsFor reads the path argument of every guarded tool', () => {
|
||||
assert.deepEqual(pathArgumentsFor('read', { file_path: 'src/a.ts' }), ['src/a.ts'])
|
||||
assert.deepEqual(pathArgumentsFor('write', { file_path: 'src/a.ts', content: 'x' }), ['src/a.ts'])
|
||||
assert.deepEqual(pathArgumentsFor('edit', { file_path: 'src/a.ts' }), ['src/a.ts'])
|
||||
assert.deepEqual(pathArgumentsFor('read_image', { file_path: 'a.png' }), ['a.png'])
|
||||
assert.deepEqual(pathArgumentsFor('grep', { pattern: 'x', path: 'src' }), ['src'])
|
||||
assert.deepEqual(pathArgumentsFor('glob', { pattern: '*.ts', path: 'src' }), ['src'])
|
||||
assert.deepEqual(pathArgumentsFor('str_replace_editor', { path: 'src/a.ts' }), ['src/a.ts'])
|
||||
assert.deepEqual(pathArgumentsFor('recall', { path: 'src', focus: 'src/controllers' }), [
|
||||
'src',
|
||||
'src/controllers',
|
||||
])
|
||||
})
|
||||
|
||||
test('pathArgumentsFor has no opinion where it cannot have one', () => {
|
||||
// The deliberate hole: a shell command cannot be judged from argv.
|
||||
assert.deepEqual(pathArgumentsFor('bash', { command: 'cat /etc/passwd' }), [])
|
||||
// Unknown tools are not guessed at.
|
||||
assert.deepEqual(pathArgumentsFor('mcp_something', { path: '/etc' }), [])
|
||||
// An omitted path means the session directory, which is already in scope.
|
||||
assert.deepEqual(pathArgumentsFor('grep', { pattern: 'x' }), [])
|
||||
// Unvalidated JSON must never make the guard throw.
|
||||
assert.deepEqual(pathArgumentsFor('read', null), [])
|
||||
assert.deepEqual(pathArgumentsFor('read', 'nonsense'), [])
|
||||
assert.deepEqual(pathArgumentsFor('read', { file_path: 42 }), [])
|
||||
assert.deepEqual(pathArgumentsFor('read', { file_path: ' ' }), [])
|
||||
assert.deepEqual(pathArgumentsFor('grep', { path: { nested: '/etc' } }), [])
|
||||
})
|
||||
|
||||
test('isWideningCall is true only for the one act that needs the user', () => {
|
||||
assert.equal(isWideningCall('facet', { action: 'add', paths: ['x'] }), true)
|
||||
// Narrowing and reading cannot show the agent anything new.
|
||||
assert.equal(isWideningCall('facet', { action: 'remove', paths: ['x'] }), false)
|
||||
assert.equal(isWideningCall('facet', { action: 'reset' }), false)
|
||||
assert.equal(isWideningCall('facet', { action: 'status' }), false)
|
||||
assert.equal(isWideningCall('read', { action: 'add' }), false)
|
||||
assert.equal(isWideningCall('facet', null), false)
|
||||
})
|
||||
|
||||
test('facetRuleText states the rule and never the binding', () => {
|
||||
const ask = facetRuleText({ mode: 'ask' })
|
||||
assert.match(ask, /put to the user as a question/)
|
||||
// The two modes must not read alike: ask must not claim the call is refused.
|
||||
assert.doesNotMatch(ask, /A call outside it is refused/)
|
||||
|
||||
const deny = facetRuleText({ mode: 'deny' })
|
||||
assert.match(deny, /A call outside it is refused/)
|
||||
|
||||
// The rule must not name a path: a changing system prompt invalidates the
|
||||
// prompt cache for the whole conversation.
|
||||
assert.doesNotMatch(ask, /\/|src\//)
|
||||
|
||||
assert.equal(facetRuleText({ mode: 'off' }), '')
|
||||
assert.equal(facetContextText({ cwd: '/a', root: '/a/b', extras: [], mode: 'off' }), '')
|
||||
})
|
||||
|
||||
test('facetContextText carries the binding that does change', () => {
|
||||
assert.equal(
|
||||
facetContextText({ cwd: '/w', root: '/w/src/room', extras: [], mode: 'ask' }),
|
||||
'facet: /w/src/room\noutside the facet: ask the user',
|
||||
)
|
||||
assert.equal(
|
||||
facetContextText({ cwd: '/w', root: '/w/src/room', extras: ['/w/src/shared'], mode: 'deny' }),
|
||||
'facet: /w/src/room\ngranted outside it: src/shared\noutside the facet: refused',
|
||||
)
|
||||
// Unscoped sessions contribute nothing rather than an empty heading.
|
||||
assert.equal(facetContextText({ cwd: '/w', root: undefined, extras: [], mode: 'ask' }), '')
|
||||
})
|
||||
|
||||
test('displayPath prefers the shortest unambiguous form', () => {
|
||||
assert.equal(displayPath('/w', '/w/src/a.ts'), 'src/a.ts')
|
||||
assert.equal(displayPath('/w', '/w'), '.')
|
||||
assert.equal(displayPath('/w', '/other/a.ts'), '/other/a.ts')
|
||||
assert.equal(displayPath('', '/w/a.ts'), '/w/a.ts')
|
||||
})
|
||||
|
||||
test('outsideReason names the path, the boundary and the way forward', () => {
|
||||
const reason = outsideReason({
|
||||
cwd: '/w/src/controllers/room',
|
||||
path: '/w/src/shared/types.ts',
|
||||
root: '/w/src/controllers/room',
|
||||
mode: 'ask',
|
||||
})
|
||||
assert.match(reason, /src\/shared\/types\.ts/)
|
||||
assert.match(reason, /src\/controllers\/room/)
|
||||
// A denial that does not say how to proceed is one the agent routes around.
|
||||
assert.match(reason, /facet action=add/)
|
||||
})
|
||||
|
||||
test('widenReason survives an add with no usable paths', () => {
|
||||
assert.match(widenReason(['src/shared']), /src\/shared/)
|
||||
assert.match(widenReason([]), /nothing/)
|
||||
})
|
||||
Reference in new issue
Block a user