quad/quad-base/test/spec.blocker.luau
qwreey-agent-selene 0d66e03875
feat: round11 §4 배치 회신 1차 반영 — 확정 일곱(H-182~H-185/H-187/H-200/H-203), 재질문 둘(H-186/H-198)·보류(H-205)
- H-182 (a): Effect `_dying` — Destroying 콜백이 세우고 재바인드·Subscribe류가
  내림, rawRerun 홀드 조건 합류(Slot `_destroyed`와 다른 이름은 의도 — 재바인드 가능)
- H-183 (a): Observer `_running` — 모든 fn 실행 둘레 + 네 진입점 첫 줄 가드
  (H-147 대칭; error 시 잔류는 설계상 인정)
- H-184 (a): `_assertBindable` 훅 — bindLifetime이 부기 커밋 전에 문의(level 3),
  H-147 가드는 _bindDestroying에서 이 훅으로 이동. mock + lifecycle-pattern (1)
- H-185: 권고 기각 — cleanup은 하나만(목록 소진은 표면 확대), 문서·타입 주석 명시
- H-187 (a): 타입 별칭 이름 넷 승인(quad-types-plan 기록, 마커 해소)
- H-200 (b): Gate 생성이 setup 동안 상류 _subs에서 떼고 성공 후 재등록(pcall 없음)
- H-203 (a): Blocker 순회가 핸들마다 IsBlocked 재확인 — 재차단 시 잔여는 다음 Off로
- H-168~H-170 재확인 반영: H-170 한계 공개 문서화(ref-plan + content-map 22번)
- 스펙: spec.effect 10 / spec.observer 9 / spec.gate 1 확장 / spec.blocker 8
- 감사 3라운드(3+2 → 3 → 0 수렴): 필드 목록 _running/_dying, CLAUDE.md·
  project-context.md M2 배너(목록은 todos.md 00번 단일 소스), session-summary
- 남은 코드 마커 셋: H-186/H-198(재질문 — §4 회신 2 블록에 메인 답변), H-205(보류)

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG
2026-08-31 12:56:31 +09:00

190 lines
5.7 KiB
Text

--[[
Blocker 계약 — `.claude/base/blocker-plan.md` "메커니즘 (확정)"(On/Off/OffWithoutEmit/IsOn/Policy/`__apply`,
onunblock 핸들 보관 `H-63` 셋) / "재진입(네스팅) — 의도적으로 미지원", `.claude/base/gate-plan.md` 5번·8번.
]]
local Quad = require("../src")
local QuadTypes = require("../roblox_packages/quad_types")
type State<T> = QuadTypes.State<T>
type Probe = { count: number, last: any, _receive: (self: Probe, from: any) -> () }
-- strict 모드에서 `collectgarbage`는 선언 안 된 전역 — 표준 luau CLI엔 있다(Roblox엔 없음)
local collectgarbage = (_G :: any).collectgarbage :: () -> ()
local Source, Blocker = Quad.Source, Quad.Blocker
local function probe(target: any): Probe
local p = { count = 0, last = nil :: any } :: Probe
function p._receive(self: Probe, from: any)
self.count += 1
self.last = from
end
target._subs[p] = true
return p
end
print("=== 1. 생성·상태 — IsOn/On/Off/OffWithoutEmit은 self 반환, 브랜드, 단순 불리언(카운터 아님) ===")
do
local b = Blocker()
assert(Quad.isBlocker(b) and not Quad.isState(b), "brand")
assert(b:IsOn() == false, "starts open")
assert(b:On() == b and b:IsOn() == true, "On")
b:On() -- twice: still just true
assert(b:Off() == b and b:IsOn() == false, "Off — no counting: one Off closes two Ons")
assert(b:OffWithoutEmit() == b and b:IsOn() == false, "idempotent")
print("PASS")
end
print()
print("=== 2. state:Apply(blocker) — __apply 메소드형(H-158)으로 GateNode 하나, 열려 있으면 투명 통과 ===")
do
local s = Source(1)
local b = Blocker()
local g: State<number> = s:Apply(b)
assert(Quad.isState(g) and g ~= s and g:Get() == 1, "a new gated State (GateNode)")
local p = probe(g)
s:Set(2)
assert(p.count == 1 and p.last[s] == true, "open: passes through as a batch")
assert(g:Get() == 2, "value follows")
print("PASS")
end
print()
print("=== 3. On → 유보(HasBlockedEmit = withheld 비어있지 않음), Off → 정확히 1회 flush, 이미 비었으면 no-op ===")
do
local s = Source(1)
local b = Blocker()
local g: State<number> = s:Apply(b)
local p = probe(g)
b:On()
s:Set(2)
s:Set(3)
assert(p.count == 0 and g:Get() == 3, "blocked: no notification, value still visible")
b:Off()
assert(p.count == 1 and p.last[s] == true, "Off flushed exactly once")
b:On()
b:Off()
assert(p.count == 1, "nothing withheld → Off does nothing (idempotent)")
print("PASS")
end
print()
print("=== 4. OffWithoutEmit — 밀린 전파를 버리며 끈다, 다음 진짜 emit은 정상 ===")
do
local s = Source(1)
local b = Blocker()
local g: State<number> = s:Apply(b)
local p = probe(g)
b:On()
s:Set(2)
b:OffWithoutEmit()
assert(p.count == 0 and next((g :: any)._withheld) == nil, "discarded, set emptied")
s:Set(3)
assert(p.count == 1, "next real emit propagates")
print("PASS")
end
print()
print("=== 5. 하나의 Blocker가 여러 gated state를 — Off가 전부 풀고, 순회는 스냅샷(풀리는 중 새 등록 안전) ===")
do
local s, t = Source(1), Source(1)
local b = Blocker()
local gs: State<number> = s:Apply(b)
local gt: State<number> = t:Apply(b)
local ps, pt = probe(gs), probe(gt)
b:On()
s:Set(2)
t:Set(2)
local newGate: any = nil
local pn: any = nil
local sub = {}
function sub._receive(_self: any, _from: any)
if newGate == nil then
newGate = s:Apply(b) -- created mid-Off (mid-walk): must not break the walk
pn = probe(newGate)
end
end
;(gs :: any)._subs[sub] = true
b:Off()
assert(ps.count == 1 and pt.count == 1, "both gated states flushed")
assert(newGate ~= nil and pn.count == 0, "gate created mid-walk was not visited (snapshot) and had nothing withheld")
print("PASS")
end
print()
print("=== 6. H-63 — 핸들은 weak-key, 강한 주인은 게이트의 onUpstreamEmit 클로저: 게이트가 죽으면 핸들도 사라진다 ===")
do
local b = Blocker()
local s = Source(1)
local function make()
local g: State<number> = s:Apply(b)
g:Get()
end
make()
collectgarbage()
collectgarbage()
assert(next((b :: any)._handles) == nil, "dead gate → its handle left the weak set (Blocker does not pin gates)")
local g2: State<number> = s:Apply(b)
local n = 0
for _ in pairs((b :: any)._handles) do
n += 1
end
assert(n == 1 and g2:Get() == 1, "a live gate keeps its handle")
print("PASS")
end
print()
print("=== 7. Policy(emit)를 직접 Gate에 배선 — Apply와 같은 모양, 여러 노드가 정책을 공유 ===")
do
local s = Source(1)
local b = Blocker()
local g: State<number> = s:Gate(function(emit)
return b:Policy(emit)
end)
local p = probe(g)
b:On()
s:Set(2)
assert(p.count == 0, "withheld via the shared blocker")
b:Off()
assert(p.count == 1, "released")
print("PASS")
end
print()
print("=== 8. H-203 — Off 순회 중 재차단: 남은 핸들은 유보 유지, IsOn true인 채 통지가 새지 않는다 ===")
do
local b = Blocker()
local s, t = Source(0), Source(0)
local gs: State<number> = s:Apply(b)
local gt: State<number> = t:Apply(b)
-- 어느 게이트가 먼저 flush되든(스냅샷 순서는 비결정), 먼저 깨어난 쪽이 재차단한다
local delivered = 0
local function reblock()
delivered += 1
b:On()
end
local ps, pt = probe(gs), probe(gt)
local origPs, origPt = ps._receive, pt._receive
ps._receive = function(self: any, from: any)
origPs(self, from)
reblock()
end
pt._receive = function(self: any, from: any)
origPt(self, from)
reblock()
end
b:On()
s:Set(1)
t:Set(1)
assert(delivered == 0, "both withheld")
b:Off()
assert(delivered == 1, "exactly one gate flushed — the re-block stopped the walk")
assert(b:IsOn() == true, "the mid-walk On() sticks")
b:Off()
assert(delivered == 2, "the survivor's withheld batch flushes on the next Off")
print("PASS")
end
print()
print("=== ALL PASS ===")