quad/quad-base/test/spec.store.luau
qwreey-agent-selene dc81dd26db
feat: 단위 2 /code-review high 반영 — ① 여섯(H-199/H-201/H-202/H-204/H-206/H-207) + ② 넷(H-198/H-200/H-203/H-205) §4 합류
체크포인트 재개 첫 항목. 8각도 후보 22 → 검증 생존 10(정확성 8건 실측 재현).

- H-199: With/Compute nil dep 조용한 탈락 → collectDeps + "dep #N is nil"(level 3)
- H-201: store:Of 이름 문자열 검증(할당 전, level 2)
- H-202: Compute fn 함수 검증(형제 표면과 같은 급)
- H-204: Store defaults 평범한 테이블 검증(clone 전)
- H-206: implsOf 세 벌 → ImplRegistry.luau 신설(잎, 내부 전용)
- H-207: Source.Set이 Impl.Emit 직접 호출로 위임(꼬리 한 벌)
- ② 넷은 코드에 TODO 마커만, 문항은 round11.md §4(마커 10 = 문항 10, 1:1)
- H-198(🔴 닫힌 게이트 너머 fn 도중 Set → 영구 stale)은 state-epoch-plan §4
  확정 의사코드 자체의 구멍 — 그 절에 ⚠️ 결정 대기 배너, README 색인 갱신
- architecture.md: ImplRegistry 소스 트리 등재, error 계약 "도착지가 계약" 명료화
- spec.state 13절·spec.store 2·3절 신설/확장, 감사 2라운드(6건 → 니트 2) 반영

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

152 lines
6.1 KiB
Text

--[[
Store 계약 — `.claude/base/store-plan.md` "Store = Source들의 이름 붙은 모음 (명시적 초기화)" /
"타입 추론 문제 — `store.key`(dot-access)를 1급 경로로 확정" / "`store.key` 레코드 필드 타이핑".
`H-122`(isSource 화이트리스트) / `H-153`(예약 이름 런타임 가드, 그림자 = store 자신) / `H-83`(무인자).
]]
local Quad = require("../src")
local QuadTypes = require("../roblox_packages/quad_types")
type Source<T> = QuadTypes.Source<T>
type State<T> = QuadTypes.State<T>
local Source, Store = Quad.Source, Quad.Store
print("=== 1. 명시적 초기화 — store.key는 넣은 Source 그 자체, 평범한 레코드 필드 ===")
do
local hp, name = Source(100), Source("x")
local s = Store({ hp = hp, name = name })
assert(s.hp == hp and s.name == name, "fields are the very Sources passed in")
assert(rawget(s, "hp") == hp, "shadow = the store itself (H-153)")
assert(Quad.isStore(s) and not Quad.isSource(s), "brand")
local v: number = s.hp:Get()
assert(v == 100, "typed dot-access")
s.hp:Set(5)
assert(hp:Get() == 5, "Set goes to the same Source")
-- 무주석 콜백 파라미터 추론이 사는 선언 스타일(§1②) — analyze가 지킨다
local doubled: State<number> = s.hp:Compute(function(x)
return x:Get() * 2
end)
assert(doubled:Get() == 10, "Compute through a store field")
print("PASS")
end
print()
print("=== 2. defaults 검증 (H-122) — isSource 화이트리스트, level 2, 영어 ===")
do
local ok, err = pcall(function()
Store({ hp = 100 } :: any)
end)
assert(not ok and string.find(tostring(err), "not a Source", 1, true) ~= nil, "raw value rejected: " .. tostring(err))
assert(string.find(tostring(err), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller")
local ok2 = pcall(function()
Store({ ok = Source(1), bad = Quad.Ref(1) } :: any)
end)
assert(not ok2, "a Ref is not a Source")
-- H-204: the bag itself is validated before clone — a non-table used to die
-- inside table.clone, a bare Source got an error naming a Source-internal field.
local ok3, err3 = pcall(function()
Store(5 :: any)
end)
assert(not ok3 and string.find(tostring(err3), "must be a table", 1, true) ~= nil, "non-table defaults: " .. tostring(err3))
assert(string.find(tostring(err3), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller")
local ok4, err4 = pcall(function()
Store(Source(100) :: any)
end)
assert(not ok4 and string.find(tostring(err4), "plain table", 1, true) ~= nil, "bare Source as defaults: " .. tostring(err4))
local ok5 = pcall(function()
Store(setmetatable({}, { __metatable = "locked" }) :: any)
end)
assert(not ok5, "protected metatable rejected before clone")
print("PASS")
end
print()
print("=== 3. 예약 이름 (H-153) — 생성자 순회와 Of 둘 다 error(level 2) ===")
do
for _, name in { "Of", "Names", "__reservedCheck" } do
local ok, err = pcall(function()
Store({ [name] = Source(1) } :: any)
end)
assert(not ok and string.find(tostring(err), "reserved", 1, true) ~= nil, name .. " in defaults: " .. tostring(err))
local s = Store({} :: {})
local ok2, err2 = pcall(function()
s:Of(name)
end)
assert(not ok2 and string.find(tostring(err2), "reserved", 1, true) ~= nil, name .. " via Of: " .. tostring(err2))
assert(string.find(tostring(err2), "spec.store.luau", 1, true) ~= nil, "level 2")
end
-- H-201: Of exists for computed names, so a computed nil/number must error
-- here (nil used to allocate a Source then die at an internal frame; a number
-- silently broke `Names(): { string }`).
local s = Store({} :: {})
local okNil, errNil = pcall(function()
s:Of(nil :: any)
end)
assert(not okNil and string.find(tostring(errNil), "Of name must be a string", 1, true) ~= nil, "nil Of name: " .. tostring(errNil))
assert(string.find(tostring(errNil), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller")
local okNum, errNum = pcall(function()
s:Of(123 :: any)
end)
assert(not okNum and string.find(tostring(errNum), "Of name must be a string", 1, true) ~= nil, "number Of name: " .. tostring(errNum))
assert(#s:Names() == 0, "nothing was allocated by the rejected names")
print("PASS")
end
print()
print("=== 4. :Of — 없는 이름은 그 자리에서 만들어 저장(lazy가 남는 유일한 자리), 있으면 그것 ===")
do
local s = Store({ hp = Source(1) })
local dyn: Source<boolean> = s:Of<<boolean>>("dyn")
assert(Quad.isSource(dyn) and dyn:Get() == nil, "created as Source(nil)")
assert(s:Of("dyn") == dyn, "same name → same Source")
assert(rawget(s, "dyn") == dyn, "stored on the store itself")
assert(s:Of("hp") == s.hp, "declared keys are reachable through Of too")
print("PASS")
end
print()
print("=== 5. :Names — defaults 키 + Of가 만든 키, 메소드는 안 셈, 팬텀 필드도 없음 ===")
do
local s = Store({ hp = Source(1), name = Source("n") })
local function set(list: { string }): { [string]: true }
local t = {}
for _, k in list do
t[k] = true
end
return t
end
local names = set(s:Names())
assert(names.hp and names.name and not names.Of and not names.Names and not names.__reservedCheck, "declared keys only")
assert(#s:Names() == 2, "count")
s:Of("dyn")
assert(set(s:Names()).dyn == true and #s:Names() == 3, "Of adds to Names")
assert(rawget(s, "__reservedCheck") == nil, "phantom field has no runtime counterpart")
print("PASS")
end
print()
print("=== 6. 빈 Store (H-83/H-157) — 무인자도 유효 ===")
do
local e = Store({} :: {})
assert(#e:Names() == 0, "no names")
local d: Source<number> = e:Of("x")
assert(Quad.isSource(d), "Of works on an empty store")
local e2 = (Quad.Store :: any)()
assert(Quad.isStore(e2) and #e2:Names() == 0, "no-arg form (or {} guard)")
print("PASS")
end
print()
print("=== 7. 인스턴스별 팩토리 (H-174) — Of가 자기 quad의 Source를 만든다 ===")
do
local other = Quad.New()
local s = other.Store({} :: {})
local d = s:Of("x")
assert(getmetatable(d :: any) == getmetatable(other.Source(0) :: any), "Of uses the module's own Source, read at call time")
assert(getmetatable(d :: any) ~= getmetatable(Quad.Source(0) :: any), "not the top-level instance's")
print("PASS")
end
print()
print("=== ALL PASS ===")