Files
sky_phone/tests/server_calls.lua
T
Leon.Schmidt 30559048fd FIX - complete LB custom app compatibility (#11)
* FIX - preserve LB custom app lifecycle

* FIX - reset LB export aliases before startup

* FIX - report competing custom app providers

* FIX - identify LB app frames as live NUI

* FIX - complete LB custom app compatibility

* FIX - provide LB PicChat runtime exports

* FIX - route LB PicChat through Sky Phone

* FIX - return cached LB equipped phone number

* FIX - harden PicChat compatibility migration

* ENH - complete modular phone provider and Creator APIs

* DOC - document the Sky Phone Creator API

---------

Co-authored-by: DerEchteAlec <bycraky@gmail.com>
Co-authored-by: DerEchteAlec <alec.schitzkat@luwan.io>
2026-08-20 18:19:16 +02:00

289 lines
10 KiB
Lua

local registered_callbacks = {}
local migration_callback = nil
local client_events = {}
local transactions = {}
local stopped_calls = {}
local speaker_enabled = true
Bridge = {
Callbacks = {
Register = function(name, callback)
assert(type(name) == "string" and type(callback) == "function")
registered_callbacks[name] = callback
end,
},
Calls = {
IsAvailable = function()
return true
end,
SetMuted = function()
return true
end,
SetSpeaker = function()
return true
end,
Start = function()
return true, "yaca"
end,
Stop = function(call_id, sources, provider)
stopped_calls[#stopped_calls + 1] = {
callId = call_id,
provider = provider,
sources = sources,
}
end,
SupportsMute = function()
return false
end,
SupportsSpeaker = function()
return false
end,
},
Database = {
AfterMigration = function(name, callback)
assert(name == "sky_phone")
migration_callback = callback
end,
Query = function()
return {}
end,
Transaction = function(operations)
transactions[#transactions + 1] = operations
return true
end,
},
Debug = function()
end,
Framework = {
GetPlayers = function()
return {}
end,
},
Speaker = {
IsEnabled = function()
return speaker_enabled
end,
},
}
Config = {
Calls = {
RecentPageSize = 50,
RingSeconds = 30,
},
Companies = {
CallRouting = {
MaxAttempts = 1,
RingSeconds = 30,
},
},
Mail = {
Domain = "test.local",
LocalPartMaxLength = 32,
LocalPartMinLength = 3,
},
Payphones = {
Animation = {
HangupDurationMs = 2000,
},
},
Sim = {
NumberLength = 7,
NumberPrefix = "",
},
}
SkyPhone = {}
SkyPhoneCompanies = {}
SkyPhoneSimNumber = {}
json = {
decode = function()
return {}
end,
}
function AddEventHandler()
end
function CreateThread()
end
function GetCurrentResourceName()
return "sky_phone"
end
function SetTimeout()
end
function TriggerClientEvent(name, target, payload)
client_events[#client_events + 1] = {
name = name,
payload = payload,
target = target,
}
end
assert(loadfile("sky_phone/source/server/calls.lua"))()
assert(type(migration_callback) == "function", "calls module must wait for its migration")
migration_callback()
local function find_upvalue(callback, expected_name)
for index = 1, 100 do
local name, value = debug.getupvalue(callback, index)
if not name then
break
end
if name == expected_name then
return value
end
end
error("missing upvalue: " .. expected_name)
end
local active_lookup = find_upvalue(SkyPhoneCalls.GetForSource, "active_call_for_source")
local active_by_source = find_upvalue(active_lookup, "active_by_source")
local calls = find_upvalue(active_lookup, "calls")
local function seed_call(call)
calls[call.id] = call
active_by_source[call.caller_source] = call.id
active_by_source[call.callee_source] = call.id
end
local call = {
id = "550e8400-e29b-41d4-a716-446655440000",
caller_number = "5550101",
caller_source = 10,
callee_number = "5550102",
callee_source = 20,
muted = {},
speakers = {},
started_at = 1000,
}
seed_call(call)
assert(SkyPhoneCalls.IsActiveForSource(10), "caller must be active while ringing")
assert(SkyPhoneCalls.IsActiveForSource(20), "callee must be active while ringing")
assert(not SkyPhoneCalls.IsActiveForSource("10"), "numeric strings must not be coerced into player sources")
assert(not SkyPhoneCalls.IsActiveForSource(10.5), "fractional player sources must be rejected")
local invalid_call, invalid_error = SkyPhoneCalls.GetForSource("10")
assert(invalid_call == nil and invalid_error == "invalid_source", "invalid source lookups must be explicit")
local caller_state = assert(SkyPhoneCalls.GetForSource(10))
assert(type(caller_state.id) == "string", "Sky call UUIDs must stay strings")
assert(caller_state.id == call.id and caller_state.state == "ringing", "ringing call state changed")
assert(caller_state.direction == "outgoing" and caller_state.otherNumber == "5550102", "caller view changed")
assert(caller_state.channel == nil, "ringing calls must not expose a voice channel")
assert(caller_state.caller.source == 10 and caller_state.caller.number == "5550101", "caller identity changed")
assert(caller_state.callee.source == 20 and caller_state.callee.number == "5550102", "callee identity changed")
assert(not caller_state.anonymous and not caller_state.video, "unsupported call modes must remain explicit")
assert(not caller_state.payphone and caller_state.companyId == nil, "ordinary call classification changed")
caller_state.state = "ended"
assert(SkyPhoneCalls.GetForSource(10).state == "ringing", "call lookups must return isolated snapshots")
local callee_state = assert(SkyPhoneCalls.GetForSource(20))
assert(callee_state.direction == "incoming" and callee_state.otherNumber == "5550101", "callee view changed")
local by_id_state = assert(SkyPhoneCalls.GetById(call.id))
assert(by_id_state.id == call.id and by_id_state.direction == "outgoing", "ID lookup must use a caller view")
by_id_state.caller.number = "changed"
assert(SkyPhoneCalls.GetById(call.id).caller.number == "5550101", "ID lookups must be isolated")
local invalid_id_call, invalid_id_error = SkyPhoneCalls.GetById("not-a-uuid")
assert(invalid_id_call == nil and invalid_id_error == "invalid_call_id", "invalid call IDs must be rejected")
call.answered_at = 1010
call.channel = 42
call.muted[10] = true
call.speakers[10] = true
call.voice_provider = "yaca"
call.voice_started = true
caller_state = assert(SkyPhoneCalls.GetForSource(10))
assert(caller_state.state == "connected" and caller_state.channel == 42, "connected call state changed")
assert(caller_state.muted and caller_state.muteSupported, "muted state must be projected for the caller")
assert(caller_state.speakerEnabled and caller_state.speakerSupported, "speaker state must be projected for the caller")
local ended, end_error = SkyPhoneCalls.EndForSource(0)
assert(not ended and end_error == "invalid_source", "invalid termination sources must be rejected")
assert(SkyPhoneCalls.EndForSource(10), "authoritative source termination must end the call")
assert(not SkyPhoneCalls.IsActiveForSource(10) and not SkyPhoneCalls.IsActiveForSource(20), "termination must clear participants")
assert(calls[call.id] == nil, "termination must remove the authoritative call")
local ended_id_call, ended_id_error = SkyPhoneCalls.GetById(call.id)
assert(ended_id_call == nil and ended_id_error == "call_not_found", "ended calls must not remain addressable")
assert(#stopped_calls == 1 and stopped_calls[1].callId == call.id, "termination must stop the voice backend")
assert(#transactions == 1, "termination must persist the completed call lifecycle")
assert(transactions[1][1].params[1] == "completed", "answered calls must remain completed")
assert(#client_events == 2, "both participants must receive the terminal state")
assert(client_events[1].payload.channel == nil and client_events[2].payload.channel == nil, "terminal states must drop the voice channel")
ended, end_error = SkyPhoneCalls.EndForSource(10)
assert(not ended and end_error == "call_not_found", "finished calls must not be ended twice")
local callback_call = {
id = "550e8400-e29b-41d4-a716-446655440001",
caller_number = "5550103",
caller_source = 30,
callee_number = "5550104",
callee_source = 40,
muted = {},
speakers = {},
started_at = 1100,
}
seed_call(callback_call)
local hangup_response = registered_callbacks["sky_phone:calls:hangup"](30, { id = callback_call.id })
assert(hangup_response.success, "the existing NUI hangup callback must retain its behavior")
assert(transactions[2][1].params[1] == "cancelled", "unanswered caller hangups must remain cancelled")
assert(transactions[2][3].params[1] == "missed", "unanswered callees must retain their missed status")
local company_call = {
id = "550e8400-e29b-41d4-a716-446655440002",
caller_number = "5550105",
caller_source = 50,
callee_number = "911",
callee_source = 60,
company_service_call = true,
muted = {},
speakers = {},
started_at = 1200,
}
seed_call(company_call)
assert(SkyPhoneCalls.TerminateForSource(60), "provider termination must end a routed company call")
assert(calls[company_call.id] == nil, "forced company-call termination must remove the call")
assert(not SkyPhoneCalls.IsActiveForSource(50) and not SkyPhoneCalls.IsActiveForSource(60),
"forced company-call termination must clear both participants")
assert(transactions[3][1].params[1] == "cancelled",
"forced unanswered company calls must remain cancelled")
local provider_call = {
id = "550e8400-e29b-41d4-a716-446655440003",
caller_number = "5550106",
caller_source = 70,
callee_number = "911",
callee_source = 80,
company_service_call = true,
muted = {},
speakers = {},
started_at = 1300,
}
seed_call(provider_call)
local terminate_response = registered_callbacks["sky_phone:calls:terminate"](
80,
{ id = provider_call.id }
)
assert(terminate_response.success, "provider termination callback must end the participant's call")
assert(calls[provider_call.id] == nil, "provider termination callback must bypass company rerouting")
assert(not SkyPhoneCalls.IsActiveForSource(70) and not SkyPhoneCalls.IsActiveForSource(80),
"provider termination callback must clear both participants")
local spoofed_terminate = registered_callbacks["sky_phone:calls:terminate"](
70,
{ id = "550e8400-e29b-41d4-a716-446655440099" }
)
assert(not spoofed_terminate.success and spoofed_terminate.error == "call_not_found",
"provider termination must revalidate the participant and call ID")
print("Server call seam tests passed")