Merge remote-tracking branch 'origin/master' into pr-265
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/rfc/INDEX.md # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/core/tools/README.md # packages/core/tools/src/index.ts # packages/core/tools/src/schema.ts # packages/ui/acp/src/index.ts # packages/ui/stdio-agent/README.md
This commit is contained in:
@@ -350,10 +350,8 @@ describe('the run_code dispatch bridge', () => {
|
||||
return { logs: [], value: 'done' }
|
||||
}
|
||||
|
||||
// Model a timeout-style outer wrapper: it temporarily installs a signal,
|
||||
// delegates, then restores the exact prior shape. A nested result observer
|
||||
// is observe-only and must not receive the live outer execution object;
|
||||
// freezing the correlation value it sees therefore cannot break restore.
|
||||
// Freeze the nested observer's parent correlation. If that were the live
|
||||
// outer execution object, the timeout-style wrapper could not restore it.
|
||||
ctx.on('tools/execute', async (exec, next) => {
|
||||
if (exec.name !== RUN_CODE_NAME) return next()
|
||||
const previous = exec.signal
|
||||
@@ -577,11 +575,8 @@ describe('the run_code dispatch bridge', () => {
|
||||
},
|
||||
}))
|
||||
runtime.behavior = async (request) => {
|
||||
// Start a sub-dispatch, keep its rejection held, and fail the run once
|
||||
// the tool is genuinely in flight — a seam error AFTER work has begun.
|
||||
// The bridge's settlement still owes quiescence: without the finally,
|
||||
// run_code would return now and the slow tool would finish (and log)
|
||||
// afterwards.
|
||||
// Start a sub-dispatch, keep its rejection held, and fail the run once the tool is
|
||||
// genuinely in flight — a seam error after work has begun.
|
||||
request.bindings[0]!.functions.slow!({ id: 'orphan' }).catch(() => 'held')
|
||||
await inFlight
|
||||
throw new Error('backend exploded')
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
/**
|
||||
* Guarantee tests for the tool-schema catalog generator
|
||||
* (`scripts/gen-tool-catalog.ts`).
|
||||
*
|
||||
* The generated catalog is frozen by a regenerate-and-diff freshness gate, so
|
||||
* the freshness half is exercised by `pnpm run verify-tool-catalog` in CI. What
|
||||
* a freshness diff CANNOT prove is (a) that BOOTING the tool plugins yields the
|
||||
* shipped schema — the whole reason this generator boots instead of parsing
|
||||
* source (a runtime-spread enum resolves to its literal members) — and (b) that
|
||||
* the completeness guard REJECTS a tool package missing from the boot manifest,
|
||||
* the property that replaces the AST pass's "nothing silently omitted". These
|
||||
* tests drive the exported `collectToolCatalog` / `assertManifestComplete` /
|
||||
* `render` directly, mirroring the negative-path style of the cordis-catalog
|
||||
* generator tests.
|
||||
* Guarantee tests for the tool-schema catalog generator (`scripts/gen-tool-catalog.ts`).
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -62,11 +50,8 @@ describe('gen-tool-catalog collectToolCatalog', () => {
|
||||
})
|
||||
|
||||
it('records the shipped `subagent_fork` alias in a note (config-driven tool name)', async () => {
|
||||
// `tool-subagent`'s registered name is the load-time `toolName` config, so
|
||||
// the shipped agents surface this one package as both `subagent` and
|
||||
// `subagent_fork`. Booting yields only the default name; the note is how a
|
||||
// reader learns the fork alias the model also sees. Without it the catalog
|
||||
// would silently under-report the shipped tool surface.
|
||||
// `tool-subagent`'s registered name is the load-time `toolName` config, so the shipped
|
||||
// agents surface this one package as both `subagent` and `subagent_fork`.
|
||||
const catalog = await collectToolCatalog()
|
||||
const subagent = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-subagent')
|
||||
expect(subagent?.schemas.map(s => s.name)).toEqual(['subagent'])
|
||||
|
||||
@@ -703,16 +703,9 @@ describe('ToolRegistry', () => {
|
||||
})
|
||||
|
||||
it('register() returns the EXACT effect disposer: a composite yield nests the teardown in order', async () => {
|
||||
// The registry-disposer convention (set by agents.register): the returned
|
||||
// function IS the cordis effect disposer, so a composite (generator)
|
||||
// effect that yields it has the unregistration run at that yield's LIFO
|
||||
// position on owner unload. A wrapper would leave the inner effect
|
||||
// disposing as a CONCURRENT SIBLING of the composite; the async probe
|
||||
// below (disposed first, LIFO) yields the event loop exactly like the
|
||||
// agent factory's stop-and-drain link, and a sibling unregistration fires
|
||||
// in that window — the probe would observe the tool already gone. Pins
|
||||
// the convention for the whole register-method family (system-prompt
|
||||
// registrars, registerProvider, setFactory share the same return).
|
||||
// Registry methods return the exact Cordis effect disposer so a composite yield places
|
||||
// unregistration at its LIFO position. A wrapper would create a concurrent sibling; this async
|
||||
// probe yields during earlier teardown and would then observe the tool already removed.
|
||||
const ctx = await setup()
|
||||
const order: string[] = []
|
||||
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
@@ -999,20 +992,18 @@ describe('schema DSL edge cases', () => {
|
||||
port: { type: 'number' },
|
||||
},
|
||||
})
|
||||
// no 'required' key in the nested object because nothing is required
|
||||
const config = jsonSchema.properties['config'] as Record<string, unknown>
|
||||
expect('required' in config).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('schema DSL regressions (Codex review round 2)', () => {
|
||||
describe('schema DSL optional and nested contracts', () => {
|
||||
it('InferArgs makes non-required keys genuinely optional (omittable)', () => {
|
||||
type Args = InferArgs<{
|
||||
path: { type: 'string'; required: true }
|
||||
limit: { type: 'number' }
|
||||
}>
|
||||
expectTypeOf<Args>().toEqualTypeOf<{ path: string; limit?: number }>()
|
||||
// omitting the optional key is assignable — the actual regression
|
||||
const omitted: Args = { path: '/tmp' }
|
||||
expect(omitted.limit).toBeUndefined()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user