quad/quad-base/test/spec.init.luau
qwreey-agent-selene 3670e88d78
fix(m2): /code-review high(단위 3·4) 반영 — 임플을 module._impl로(H-181, 인스턴스 영구 핀 해소), Gate 실패 시 detach(H-188), Observer.Subscribed=false(H-189), Apply 검증(H-190); ② H-182~H-187은 §4 + TODO 마커
- State/Observer/Effect: implByModule(weak-key, 값이 키 캡처 → ephemeron 없어 불멸) 제거,
  임플은 rawset(module, "_impl") 비공개 필드(H-174 (a) 원문 모양). spec.init 2가 인스턴스 GC 고정.
- State.Gate: 검증 실패 시 상류 _subs에서 detach, setup 전 _onUpstreamEmit = Void. Apply 객체 분기 검증.
- Effect 시딩은 dep:_track(map), EpochSet 타입은 quad-types 하나로, State.Impl._module 제거.
- 코드 마커 TODO(H-182)(H-183)(H-184)(H-185)(H-186)(H-187); round11 §4 표 여섯 행(권고 전부 (a)).
- WARN 둘(round11.md 축약 참조) 전체 이름으로. 세션 원문·요약.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-29 02:58:27 +09:00

54 lines
2.4 KiB
Text

--[[
`Quad` 탑레벨 값 존재 확인 — `quad-types`의 `Quad` 타입과 `New()`가 실제로 싣는 값의
드리프트(`ROADMAP.md` M2 `H-80`/`H-25` 부류)를 타입 검사 아래에서 잡는다.
`init.luau`의 `{...} :: Quad` 캐스트는 빠진 필드를 안 잡으므로(실측) 이 런타임 확인이
유일한 가드다 — 그래서 `smoke.*`(analyze 제외)가 아니라 `spec.*`에 둔다.
]]
local Quad = require("../src")
local mock = require("./mock")
local QuadTypes = require("../roblox_packages/quad_types")
type Quad = QuadTypes.Quad
print("=== 1. New()마다 M2 첫 단위 탑레벨 값이 실려 있음 ===")
do
local m: Quad = Quad.New()
assert(type(m.Relate) == "function" and type(m.Void) == "function" and type(m.Ref) == "function", "Relate/Void/Ref constructors")
assert(type(m.Source) == "function" and type(m.Store) == "function", "Source/Store constructors (unit 2, per instance)")
assert(m.Source ~= Quad.Source, "Source is an instance-level factory (H-174), not a shared leaf")
local predicates: { (any) -> boolean } = {
m.isEpoch, m.isSource, m.isState, m.isStore, m.isObserver, m.isEffect,
m.isBlocker, m.isModifier, m.isRef, m.isPreRef, m.isPostRef,
}
for i, p in predicates do
assert(type(p) == "function", "predicate #" .. i .. " must be a function")
assert(p({}) == false, "predicate #" .. i .. " is false for an unregistered table")
end
assert(type(m.bindLifetime) == "function" and type(m.unbindLifetime) == "function", "lifetime stubs")
assert(type(m.canBound) == "function" and type(m.canExecute) == "function", "lifetime stubs")
assert(m.Void == Quad.Void and m.Ref == Quad.Ref and m.Relate == Quad.Relate, "leaf modules are shared across instances")
print("PASS")
end
print()
print("=== 2. [H-181] New() 인스턴스는 참조를 놓으면 수거된다 — 임플을 모듈 비공개 필드에 두므로 순환이 자기완결 ===")
do
local collectgarbage = (_G :: any).collectgarbage :: () -> ()
local weak = setmetatable({}, { __mode = "v" }) :: { any }
local function make()
local q = Quad.New()
mock.installLifetime(q)
local s = q.Source(1)
q.Effect(function() end, s):Subscribe() -- 강한 레지스트리까지 채워도 인스턴스째 죽어야 한다
weak[1] = q
end
make()
collectgarbage()
collectgarbage()
assert(weak[1] == nil, "a dropped quad instance is collected (no module-keyed weak map pinning it)")
print("PASS")
end
print()
print("=== ALL PASS ===")