mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
167 lines
7.5 KiB
Lua
167 lines
7.5 KiB
Lua
-- Sandboxed background asset-I/O: declared imports + owning private cache.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.modkit")
|
|
local Job = require("src.mods.Job")
|
|
local JobAssetIO = require("src.mods.JobAssetIO")
|
|
local Json = require("src.link.Json")
|
|
|
|
local digest = "00000000000000000000000000000000"
|
|
local files = {
|
|
["mods/asset_probe/baseroms/source.iso"] = "0123456789abcdef",
|
|
["mods/asset_probe/baseroms/.required-import-disc.validated"] =
|
|
"v1\n" .. digest .. "\n16\n1\n",
|
|
}
|
|
|
|
local fs = {
|
|
read = function(path) return files[path] end,
|
|
write = function(path, body) files[path] = body return true end,
|
|
getInfo = function(path)
|
|
local body = files[path]
|
|
if body ~= nil then return { type = "file", size = #body, modtime = 1 } end
|
|
local prefix = path .. "/"
|
|
for key in pairs(files) do
|
|
if key:sub(1, #prefix) == prefix then return { type = "directory" } end
|
|
end
|
|
return nil
|
|
end,
|
|
readRange = function(path, offset, length)
|
|
local body = files[path]
|
|
if not body then return nil end
|
|
return body:sub(offset + 1, offset + length)
|
|
end,
|
|
createDirectory = function() return true end,
|
|
remove = function(path) files[path] = nil return true end,
|
|
}
|
|
|
|
local manifest = {
|
|
id = "asset_probe",
|
|
path = "mods/asset_probe",
|
|
permissionSet = { background = true },
|
|
required_imports = {{
|
|
id = "disc", name = "Disc", file = "source.iso", md5 = { digest }, size = 16,
|
|
}},
|
|
optional_imports = {},
|
|
}
|
|
|
|
-- The descriptor carries declarations, not host paths or a filesystem handle.
|
|
local descriptor, descriptorErr = JobAssetIO.descriptor(manifest, manifest.path)
|
|
T.check(descriptor ~= nil, "asset descriptor builds (" .. tostring(descriptorErr) .. ")")
|
|
T.eq(descriptor.id, "asset_probe", "descriptor binds the owning mod id")
|
|
T.eq(descriptor.path, "mods/asset_probe", "descriptor keeps the engine-owned mod root")
|
|
T.eq(descriptor.fs, nil, "descriptor does not carry a filesystem handle")
|
|
T.eq(descriptor.cache, nil, "descriptor does not carry a cache path/handle")
|
|
|
|
-- Reconstructed worker facade is exactly the import/cache subset.
|
|
local asset, openErr = JobAssetIO.open(descriptor, fs, "asset_probe")
|
|
T.check(asset ~= nil, "worker asset facade opens (" .. tostring(openErr) .. ")")
|
|
T.eq(type(asset.imports), "table", "worker gets imports capability")
|
|
T.eq(type(asset.cache), "table", "worker gets cache capability")
|
|
T.eq(asset.cache.delete, nil, "worker cache deliberately has no destructive delete")
|
|
T.eq(asset.filesystem, nil, "worker facade exposes no raw filesystem")
|
|
|
|
local info, infoErr = asset.imports:info("disc")
|
|
T.check(type(info) == "table", "declared validated import has info (" .. tostring(infoErr) .. ")")
|
|
T.eq(info.size, 16, "worker import info reports stored size")
|
|
local slice, sliceErr = asset.imports:read("disc", 4, 6)
|
|
T.eq(slice, "456789", "worker bounded import read seeks exact bytes")
|
|
T.eq(sliceErr, nil, "worker bounded import read has no error")
|
|
local oob, oobErr = asset.imports:read("disc", 15, 2)
|
|
T.eq(oob, nil, "worker out-of-bounds import read is refused")
|
|
T.check(oobErr and oobErr:find("out of bounds", 1, true),
|
|
"worker out-of-bounds refusal is explicit")
|
|
local undeclared, undeclaredErr = asset.imports:read("other", 0, 1)
|
|
T.eq(undeclared, nil, "worker cannot read an undeclared import")
|
|
T.check(undeclaredErr and undeclaredErr:find("undeclared", 1, true),
|
|
"undeclared worker import refusal is explicit")
|
|
|
|
local wrote, writeErr = asset.cache:write("models/059/model.bin", "runtime-bytes")
|
|
T.check(wrote == true, "worker private-cache write succeeds (" .. tostring(writeErr) .. ")")
|
|
T.eq(asset.cache:read("models/059/model.bin"), "runtime-bytes",
|
|
"worker private-cache read returns exact bytes")
|
|
T.eq(asset.cache:exists("models/059/model.bin"), true,
|
|
"worker private-cache exists sees completed file")
|
|
T.eq(asset.cache:info("models/059/model.bin").size, 13,
|
|
"worker private-cache info stays scoped")
|
|
T.eq(files["mod_cache/asset_probe/models/059/model.bin"], "runtime-bytes",
|
|
"worker output is rooted under the owning mod id")
|
|
local escaped = pcall(function() asset.cache:write("../escape.bin", "x") end)
|
|
T.eq(escaped, false, "worker cache traversal is rejected")
|
|
T.eq(files["escape.bin"], nil, "worker traversal creates nothing outside cache root")
|
|
|
|
local wrong, wrongErr = JobAssetIO.open(descriptor, fs, "different_mod")
|
|
T.eq(wrong, nil, "descriptor cannot be rebound to another mod")
|
|
T.check(wrongErr and wrongErr:find("owner mismatch", 1, true),
|
|
"cross-mod descriptor refusal names ownership")
|
|
local badPath = {
|
|
id = "asset_probe", path = "../other_mod",
|
|
required_imports = manifest.required_imports, optional_imports = {},
|
|
}
|
|
local climbed, climbedErr = JobAssetIO.open(badPath, fs, "asset_probe")
|
|
T.eq(climbed, nil, "worker descriptor cannot climb to another root")
|
|
T.check(climbedErr and climbedErr:find("inside its root", 1, true),
|
|
"worker descriptor path refusal uses the safe-path rule")
|
|
|
|
-- Parent-side transport: assetIO gets a longer bounded foreground budget and
|
|
-- the worker receives only JSON descriptor metadata. Pure compute remains 30s.
|
|
local previousLove = rawget(_G, "love")
|
|
local startedArgs
|
|
local channels = {}
|
|
local function channel(name)
|
|
local ch = channels[name]
|
|
if ch then return ch end
|
|
ch = { values = {} }
|
|
function ch:clear() self.values = {} end
|
|
function ch:push(v) self.values[#self.values + 1] = v return true end
|
|
function ch:pop()
|
|
if #self.values == 0 then return nil end
|
|
return table.remove(self.values, 1)
|
|
end
|
|
channels[name] = ch
|
|
return ch
|
|
end
|
|
_G.love = {
|
|
thread = {
|
|
getChannel = channel,
|
|
newThread = function(path)
|
|
T.eq(path, "src/mods/job_worker.lua", "Job.run uses the engine worker")
|
|
return {
|
|
start = function(_, ...)
|
|
startedArgs = { ... }
|
|
return true
|
|
end,
|
|
getError = function() return nil end,
|
|
}
|
|
end,
|
|
},
|
|
timer = { getTime = function() return 100 end },
|
|
}
|
|
|
|
local loader = { mods = { asset_probe = { manifest = manifest } }, jobs = {} }
|
|
local handle, runErr = Job.run(loader, "asset_probe", "mods/asset_probe",
|
|
"jobs/build.lua", { asset = 59 }, { assetIO = true, maxSeconds = 999 })
|
|
T.check(handle ~= nil, "asset-I/O Job.run starts (" .. tostring(runErr) .. ")")
|
|
local state = handle and loader.jobs.asset_probe[handle]
|
|
T.eq(state and state.seconds, 300, "asset-I/O jobs have a hard 300-second poll budget")
|
|
T.eq(startedArgs and startedArgs[1], "asset_probe", "worker transport binds owner id")
|
|
local transported = startedArgs and Json.decode(startedArgs[6] or "")
|
|
T.eq(transported and transported.id, "asset_probe", "worker transport includes owner descriptor")
|
|
T.eq(transported and transported.path, "mods/asset_probe", "worker transport includes only safe mod root")
|
|
T.eq(transported and transported.required_imports[1].id, "disc",
|
|
"worker transport includes declared import metadata")
|
|
T.eq(transported and transported.permissionSet, nil,
|
|
"asset descriptor does not duplicate unrelated manifest state")
|
|
if handle then Job.release(loader, "asset_probe", handle) end
|
|
|
|
startedArgs = nil
|
|
local pureHandle, pureErr = Job.run(loader, "asset_probe", "mods/asset_probe",
|
|
"jobs/crunch.lua", { n = 4 }, { maxSeconds = 999 })
|
|
T.check(pureHandle ~= nil, "pure Job.run still starts (" .. tostring(pureErr) .. ")")
|
|
local pureState = pureHandle and loader.jobs.asset_probe[pureHandle]
|
|
T.eq(pureState and pureState.seconds, 30, "pure-compute job timeout remains capped at 30 seconds")
|
|
T.eq(startedArgs and startedArgs[6], "", "pure-compute worker receives no asset-I/O descriptor")
|
|
if pureHandle then Job.release(loader, "asset_probe", pureHandle) end
|
|
_G.love = previousLove
|
|
|
|
T.finish("mod_job_asset_io")
|