local describe = require("lua_test.test").describe
local test = require("lua_test.test").test
local expect = require("lua_test.test").expect
local mocks = require("lua_test.mocks")
mocks.reset()
mocks.install()
local RELOAD = {
"pasta.shiori.entry",
"pasta.shiori.event",
"pasta.shiori.event.register",
"pasta.shiori.event.callback",
"pasta.shiori.event.boot",
"pasta.shiori.event.choice_select",
"pasta.shiori.event.second_change",
"pasta.shiori.event.virtual_dispatcher",
"pasta.shiori.res",
"pasta.shiori.act",
"pasta.shiori.sakura_builder",
"pasta.act",
"pasta.actor",
"pasta.scene",
"pasta.word",
"pasta.global",
"pasta.store",
"pasta.config",
"pasta.save",
}
for _, name in ipairs(RELOAD) do
package.loaded[name] = nil
end
local ENTRY = require("pasta.shiori.entry")
local EVENT = require("pasta.shiori.event")
local REG = require("pasta.shiori.event.register")
local CALLBACK = require("pasta.shiori.event.callback")
local dispatcher = require("pasta.shiori.event.virtual_dispatcher")
local GLOBAL = require("pasta.global")
local STORE = require("pasta.store")
local SHIORI_ACT = require("pasta.shiori.act")
local function reset_state()
STORE.reset()
CALLBACK.reset()
end
describe("SHIORI.entry - load / unload / グローバルテーブル", function()
test("require はグローバル SHIORI テーブルと同一のモジュールを返す", function()
expect(_G.SHIORI):toBe(ENTRY)
expect(type(ENTRY.load)):toBe("function")
expect(type(ENTRY.request)):toBe("function")
expect(type(ENTRY.unload)):toBe("function")
end)
test("SHIORI.load は true を返す", function()
expect(ENTRY.load(0, "C:/ghost/master/")):toBe(true)
end)
test("SHIORI.unload はエラーなく完了し値を返さない", function()
local results = table.pack(pcall(ENTRY.unload))
expect(results[1]):toBe(true)
expect(results.n):toBe(1) end)
end)
describe("SHIORI.request - xpcall 境界", function()
test("登録ハンドラが文字列を返すと 200 OK + Value で応答する", function()
reset_state()
REG.OnEntryRequestTest = function(_act)
return "hello from entry"
end
local res = ENTRY.request({ id = "OnEntryRequestTest", method = "get", version = 30 })
expect(res:find("SHIORI/3.0 200 OK", 1, true)).not_:toBe(nil)
expect(res:find("Value: hello from entry", 1, true)).not_:toBe(nil)
end)
test("未登録イベントはシーン不在なら 204 No Content で応答する", function()
reset_state()
local res = ENTRY.request({ id = "OnEntryNoSuchEvent", method = "get", version = 30 })
expect(res:find("SHIORI/3.0 204 No Content", 1, true)).not_:toBe(nil)
end)
test("ハンドラのエラーは 500 となり X-Error-Reason は先頭行のみを含む", function()
reset_state()
REG.OnEntryRequestError = function(_act)
error("entry boom\nsecond line detail")
end
local res = ENTRY.request({ id = "OnEntryRequestError", method = "get", version = 30 })
expect(res:find("500 Internal Server Error", 1, true)).not_:toBe(nil)
expect(res:find("X%-Error%-Reason:")).not_:toBe(nil)
expect(res:find("entry boom", 1, true)).not_:toBe(nil)
expect(res:find("second line detail", 1, true)):toBe(nil)
end)
test("文字列以外のエラーオブジェクトは Unknown error として 500 になる", function()
reset_state()
REG.OnEntryRequestTableError = function(_act)
error({ code = 42 })
end
local res = ENTRY.request({ id = "OnEntryRequestTableError", method = "get", version = 30 })
expect(res:find("500 Internal Server Error", 1, true)).not_:toBe(nil)
expect(res:find("X-Error-Reason: Unknown error", 1, true)).not_:toBe(nil)
end)
end)
describe("GLOBAL.close_ghost - ゴースト終了スクリプト", function()
local function make_act_spy()
local calls = {}
local act = {
wait = function(_self, ms)
calls[#calls + 1] = "wait:" .. tostring(ms)
end,
raw_script = function(_self, script)
calls[#calls + 1] = "raw:" .. script
end,
}
return act, calls
end
test("ms >= 1 のとき wait(ms) の後に \\- を出力する", function()
local act, calls = make_act_spy()
GLOBAL.close_ghost(act, 250)
expect(#calls):toBe(2)
expect(calls[1]):toBe("wait:250")
expect(calls[2]):toBe("raw:\\-")
end)
test("ms 省略時は wait せず \\- のみ出力する", function()
local act, calls = make_act_spy()
GLOBAL.close_ghost(act)
expect(#calls):toBe(1)
expect(calls[1]):toBe("raw:\\-")
end)
test("ms = 0 は待機条件(>= 1)を満たさず wait しない", function()
local act, calls = make_act_spy()
GLOBAL.close_ghost(act, 0)
expect(#calls):toBe(1)
expect(calls[1]):toBe("raw:\\-")
end)
test("ms が数値以外(文字列)の場合も wait しない", function()
local act, calls = make_act_spy()
GLOBAL.close_ghost(act, "500")
expect(#calls):toBe(1)
expect(calls[1]):toBe("raw:\\-")
end)
test("GLOBAL.ゴースト終了 は close_ghost と同一関数", function()
expect(GLOBAL["ゴースト終了"]):toBe(GLOBAL.close_ghost)
end)
end)
describe("EVENT.fire - コールバックルーティング統合", function()
local function register_callback_handler(event_name)
REG[event_name] = function(_act)
return coroutine.create(function(_a)
local cb_id = CALLBACK.next_event_id()
CALLBACK.stage_pending(cb_id, os.time() + 60, nil)
local refs = coroutine.yield("get-tag-script")
coroutine.yield("received: " .. tostring(refs and refs[1]))
end)
end
end
test("コールバック待ちコルーチンは pending に登録され co_scene には保持されない", function()
reset_state()
register_callback_handler("OnEntryCallbackStage")
local res = EVENT.fire({ id = "OnEntryCallbackStage" })
expect(res:find("200 OK", 1, true)).not_:toBe(nil)
expect(res:find("get-tag-script", 1, true)).not_:toBe(nil)
expect(CALLBACK.pending["OnPastaCallBack1"]).not_:toBe(nil)
expect(STORE.co_scene):toBe(nil)
expect(STORE.co_callback):toBe(nil)
end)
test("到着したコールバックイベントは try_route 経由で待機コルーチンへ届く", function()
reset_state()
register_callback_handler("OnEntryCallbackRoute")
EVENT.fire({ id = "OnEntryCallbackRoute" })
local res = EVENT.fire({
id = "OnPastaCallBack1",
reference = { [0] = "value123" },
})
expect(res:find("200 OK", 1, true)).not_:toBe(nil)
expect(res:find("received: value123", 1, true)).not_:toBe(nil)
expect(CALLBACK.pending["OnPastaCallBack1"]):toBe(nil)
end)
end)
describe("OnSecondChange - sweep タイムアウト分岐", function()
test("タイムアウト済みペンディングがあれば dispatcher を呼ばず 500 を返す", function()
reset_state()
dispatcher._reset()
local co = coroutine.create(function()
coroutine.yield()
end)
coroutine.resume(co)
CALLBACK.pending["OnPastaCallBackEntryTimeout"] = {
co = co,
act = {},
timeout_at = 0, on_timeout = "entry sweep timeout",
}
local dispatch_called = false
local original_dispatch = dispatcher.dispatch
dispatcher.dispatch = function(...)
dispatch_called = true
return nil
end
local act = SHIORI_ACT.new(STORE.actors, {
id = "OnSecondChange",
status = "idle",
date = { unix = os.time(), year = 2026, month = 6, day = 11, hour = 12, min = 0, sec = 0, wday = 4 },
})
local result = REG.OnSecondChange(act)
dispatcher.dispatch = original_dispatch
expect(type(result)):toBe("string")
expect(result:find("500 Internal Server Error", 1, true)).not_:toBe(nil)
expect(result:find("entry sweep timeout", 1, true)).not_:toBe(nil)
expect(dispatch_called):toBe(false)
expect(CALLBACK.pending["OnPastaCallBackEntryTimeout"]):toBe(nil)
end)
end)