fix(scope): make the dispatch carrier method-transparent for native-private subjects
ds-review-bot delta-round finding, verified: cordis hands the carrier to listeners as `this`, and the event declarations type it Scoped<Agent> — so driving the subject through it (this.send(...) in an agent/* listener) is a SUPPORTED shape. The withProps-based carrier delegated gets with the PROXY as receiver, so ReactLoopAgent's send/steer/cancel — which read the native-private #carrier through a getter — threw TypeError when called that way (private members do not exist on proxy receivers). scopeTarget now builds its own proxy: overlay props (the composed filter and the carrier mark) answer from a null-shadowed literal via hasOwn (`in` would let Object.prototype's toString/constructor shadow the subject's), every other get delegates with the BASE as receiver (getters see the real object) and returns functions bound to the base (method calls execute on the real receiver), sets land on the base. A proxy-invariant guard reports frozen own function props unchanged (binding them would violate the get invariant). This kills the class at the seam — any subject with native privates works, today's agents and whatever carries them next — instead of patching the one #carrier field. Pinned both ways: a scope.spec matrix (native-#private method/getter through the carrier mutates the real object; set delegation; frozen-own-prop invariant; overlay non-shadowing) and the bot's exact end-to-end scenario (an agent/session-start listener calling this.send drives a real turn) — both fail with TypeError against the withProps carrier.
This commit is contained in:
@@ -179,6 +179,49 @@ describe('scopeTarget dispatch filtering', () => {
|
||||
expect(result).toBe('seed+v')
|
||||
expect(seenLabel).toBe('the-base')
|
||||
})
|
||||
|
||||
it('is transparent for subjects with native #private fields: methods and getters through the carrier reach the real object', () => {
|
||||
// The ds-review-bot regression: cordis hands the carrier to listeners as
|
||||
// `this` (typed Scoped<Agent>), so subject method calls through it are a
|
||||
// supported shape. A proxy that delegates with the PROXY as receiver
|
||||
// (cordis withProps) throws TypeError on any native #private the method
|
||||
// or getter touches; the carrier must delegate with the BASE as receiver
|
||||
// and bind retrieved methods to it.
|
||||
class Subject {
|
||||
#count = 0
|
||||
bump(): number { return ++this.#count }
|
||||
get count(): number { return this.#count }
|
||||
}
|
||||
const subject = new Subject()
|
||||
const carrier = scopeTarget(subject, subject)
|
||||
expect(carrier.bump()).toBe(1) // method call: bound to the base
|
||||
expect(subject.count).toBe(1) // ...and it mutated the REAL object
|
||||
expect(carrier.count).toBe(1) // getter: runs with the base as receiver
|
||||
// The get trap returns the method already bound to the base;
|
||||
// detachability IS the assertion.
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const detached = carrier.bump
|
||||
expect(detached()).toBe(2)
|
||||
})
|
||||
|
||||
it('delegates sets to the base and leaves frozen own function props unbound (proxy invariant)', () => {
|
||||
const frozenFn = (): string => 'frozen'
|
||||
const base: { mutable: number; pinned: () => string; toString: () => string } = {
|
||||
mutable: 0,
|
||||
pinned: frozenFn,
|
||||
toString: () => 'base-str',
|
||||
}
|
||||
Object.defineProperty(base, 'pinned', { value: frozenFn, writable: false, configurable: false })
|
||||
const carrier = scopeTarget(base, undefined)
|
||||
carrier.mutable = 7
|
||||
expect(base.mutable).toBe(7) // sets land on the base, not a detached overlay
|
||||
// A non-configurable, non-writable own data prop must be reported
|
||||
// unchanged (binding it would violate the proxy get invariant).
|
||||
expect(carrier.pinned).toBe(frozenFn)
|
||||
// The overlay literal inherits Object.prototype; hasOwn (not `in`) keeps
|
||||
// it from shadowing the subject's own prototype-surface members.
|
||||
expect(String(carrier)).toBe('base-str')
|
||||
})
|
||||
})
|
||||
|
||||
describe('carrier marks', () => {
|
||||
|
||||
Reference in New Issue
Block a user