fix(review): close interpolation strictness holes; make tool-subagent mirror provider lifecycle
Codex round-1 findings, both confirmed:
- renderPrompt: variable lookup now uses Object.hasOwn (an unregistered
{{constructor}} previously resolved through Object.prototype and spliced
function source into the prompt), and a {{ that opens no complete group
while a }} still follows ({{{model}}}, {{a{b}}) now throws instead of
passing or partially interpolating. A lone {{ with no }} after it stays
verbatim; substituted values are never re-scanned.
- tool-subagent: the apply-time provider lookup assumed a load order the
cordis Loader does not guarantee (siblings start concurrently). The seam
now announces subagent/provider-added/-removed and the tool mirrors the
provider's lifecycle: registers when the provider is (or becomes)
available, unregisters when it goes away, re-derives wording on reload.
No load-order requirement remains.
- loop.spec containment test now proves live continuation: after the
contained render failure, a waterfall listener rescues {{cwd}} and the
same agent completes a real model turn.
RFC/READMEs updated to the shipped contract; cordis catalog regenerated.
This commit is contained in:
@@ -325,7 +325,7 @@ describe('SystemPrompt', () => {
|
||||
})).toThrow('malformed prompt variable reference "{{ model }}" in section "s"')
|
||||
})
|
||||
|
||||
it('leaves a lone {{ without a closing }} verbatim (only complete groups are interpreted)', () => {
|
||||
it('leaves a lone {{ verbatim only when NO }} follows anywhere after it', () => {
|
||||
const text = renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text: 'shell ${X:-{{fallback} stays' }],
|
||||
tools: [],
|
||||
@@ -333,5 +333,43 @@ describe('SystemPrompt', () => {
|
||||
})
|
||||
expect(text).toBe('shell ${X:-{{fallback} stays')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ text: '{{{model}}}', label: 'extra outer braces' },
|
||||
{ text: 'x {{a{b}} y {{model}}', label: 'nested brace inside a would-be group' },
|
||||
])('throws on a mangled reference with a }} still following ($label)', ({ text }) => {
|
||||
expect(() => renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text }],
|
||||
tools: [],
|
||||
variables: { model: 'm' },
|
||||
})).toThrow('malformed prompt variable reference at')
|
||||
})
|
||||
|
||||
it('rejects {{constructor}} as UNKNOWN — prototype properties are not variables', () => {
|
||||
// `in` would find Object.prototype.constructor and splice function
|
||||
// source into the prompt; Object.hasOwn must reject it instead.
|
||||
expect(() => renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text: 'on {{constructor}}' }],
|
||||
tools: [],
|
||||
variables: { model: 'm' },
|
||||
})).toThrow('unknown prompt variable "{{constructor}}"')
|
||||
})
|
||||
|
||||
it('a variable NAMED like a prototype property works once actually registered', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
ctx.systemPrompt.section({ name: 's', order: 0, text: '{{constructor}}' })
|
||||
ctx.systemPrompt.variable('constructor', () => 'own-value')
|
||||
expect(renderPrompt(await ctx.systemPrompt.assemble())).toBe('own-value')
|
||||
})
|
||||
|
||||
it('never re-scans substituted values (a value containing {{sneaky}} stays literal)', () => {
|
||||
const text = renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text: 'v = {{model}}!' }],
|
||||
tools: [],
|
||||
variables: { model: 'literal {{sneaky}} inside' },
|
||||
})
|
||||
expect(text).toBe('v = literal {{sneaky}} inside!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user