quad/quad-base/test/spec.brand.luau
qwreey d9898d6629
feat(m2): 첫 단위 공통 기반 — Void/Brand/LifetimeHandle/Ref 최소형 + mock 생명주기 + spec 5개
- quad-base/src: Void.luau(단일 no-op), Brand.luau(Brand() + 브랜드 인스턴스 15 +
  M2 is* 11), LifetimeHandle.luau(InitLifetimeHandle — 모듈 인스턴스에 영어 level 2
  에러 스텁 4종), Ref.luau(.Value/.Revision/:Set/:Callback/:WeakCallback/:Uncallback,
  EpochBrand+RefBrand), init.luau 재export. Relate.luau는 타입만 quad-types에서 재export.
- quad-types: Quad에 M2 첫 단위 탑레벨 값 + Ref<T>/RefCallback<T>/Relate/Epoch 타입.
- test/mock.luau: installLifetime(quad) — lifecycle-pattern.md (0)/(1) 스케치 그대로,
  Destroy가 모든 Connection을 끊도록 보강(gcconn 판정 근거).
- scripts/test.sh: spec.* 수집 + luau-analyze(src/spec/mock). 전부 ALL PASS, analyze 0건.
- 발견 ①: H-165 pesde shim은 생성 시점 export 타입만 안다(project-setup-plan.md),
  H-166 Ref.Revision 초기값 0(ref-plan.md). ROADMAP 공통 기반 체크박스 완료 표기.

Co-authored-by: qwreey <me@qwreey.moe>
2026-08-28 18:10:50 +09:00

109 lines
3.7 KiB
Text

--[[ Brand 계약 — `.claude/base/brand-plan.md` "구현 — 인스턴스 브랜드" / "`isX` wrapper" ]]
local Brand = require("../src/Brand")
local Quad = require("../src")
-- strict 모드에서 `collectgarbage`는 선언 안 된 전역 — 표준 luau CLI엔 있다(Roblox엔 없음)
local collectgarbage = (_G :: any).collectgarbage :: () -> ()
print("=== 1. register/is — 등록한 값만 통과 ===")
do
local b = Brand.Brand()
local x, y = {}, {}
b:register(x)
assert(b:is(x) == true, "registered value must pass")
assert(b:is(y) == false, "unregistered value must not pass")
assert(b:is(nil) == false and b:is(1) == false and b:is("s") == false, "non-table values are never members")
print("PASS")
end
print()
print("=== 2. 다중 태깅 — 한 값이 여러 브랜드에 동시에 ===")
do
local a, b = Brand.Brand(), Brand.Brand()
local x = {}
a:register(x)
b:register(x)
assert(a:is(x) and b:is(x), "a value may belong to several brands at once")
print("PASS")
end
print()
print("=== 3. 브랜드 간 독립 — 한 브랜드 등록이 다른 브랜드에 안 샘 ===")
do
local a, b = Brand.Brand(), Brand.Brand()
local x = {}
a:register(x)
assert(b:is(x) == false, "registration must not leak across brands")
print("PASS")
end
print()
print("=== 4. weak-key — 등록만으로는 값이 살아남지 않음 ===")
do
local b = Brand.Brand()
local weak = setmetatable({}, { __mode = "v" })
do
local x = {}
b:register(x)
weak[1] = x
end
collectgarbage()
collectgarbage()
assert(weak[1] == nil, "brand membership must not keep the value alive")
print("PASS")
end
print()
print("=== 5. isRef 계층 — PreRef/PostRef는 Ref의 한 종류, 서로는 배타 ===")
do
local pre, post, plain = {}, {}, {}
Brand.PreRefBrand:register(pre)
Brand.PostRefBrand:register(post)
Brand.RefBrand:register(plain)
assert(Brand.isRef(pre) and Brand.isRef(post) and Brand.isRef(plain), "isRef passes {Ref, PreRef, PostRef}")
assert(Brand.isPreRef(pre) and not Brand.isPreRef(post) and not Brand.isPreRef(plain), "isPreRef is the most specific identity")
assert(Brand.isPostRef(post) and not Brand.isPostRef(pre) and not Brand.isPostRef(plain), "isPostRef is the most specific identity")
-- Leaf 매치 핸들러가 쓰는 좁힘(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)
local function isPlainRef(v)
return Brand.isRef(v) and not Brand.isPreRef(v) and not Brand.isPostRef(v)
end
assert(isPlainRef(plain) and not isPlainRef(pre) and not isPlainRef(post), "plain-Ref narrowing")
print("PASS")
end
print()
print("=== 6. isState 계층 — Source는 State를 구조적으로 만족 ===")
do
local src, st = {}, {}
Brand.SourceBrand:register(src)
Brand.StateBrand:register(st)
assert(Brand.isState(src) and Brand.isState(st), "isState passes both")
assert(Brand.isSource(src) and not Brand.isSource(st), "isSource is the narrower one")
print("PASS")
end
print()
print("=== 7. 단순 항등 술어 + Quad 탑레벨 재export ===")
do
local pairsToCheck: { { brand: Brand.Brand, name: string } } = {
{ brand = Brand.EpochBrand, name = "isEpoch" },
{ brand = Brand.StoreBrand, name = "isStore" },
{ brand = Brand.ObserverBrand, name = "isObserver" },
{ brand = Brand.EffectBrand, name = "isEffect" },
{ brand = Brand.BlockerBrand, name = "isBlocker" },
{ brand = Brand.ModifierBrand, name = "isModifier" },
}
for _, pair in pairsToCheck do
local brand, name = pair.brand, pair.name
local x = {}
assert((Quad :: any)[name] == (Brand :: any)[name], name .. " must be re-exported on Quad as the same function")
assert((Brand :: any)[name](x) == false, name .. " false before registration")
brand:register(x)
assert((Brand :: any)[name](x) == true, name .. " true after registration")
end
print("PASS")
end
print()
print("=== ALL PASS ===")