Files
sky_phone/frontend/src/phoneInventory.contract.test.ts
T
2026-08-22 19:48:30 +02:00

368 lines
15 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const readResourceFile = (path: string) =>
readFileSync(
new URL(`../../sky_phone/${path}`, import.meta.url),
'utf8',
).replace(/\r\n/g, '\n')
const readFrontendFile = (path: string) =>
readFileSync(new URL(path, import.meta.url), 'utf8')
const inventoryAdapters = [
['jaksam', 'source/bridge/server/inventory/jaksam.lua'],
['ox', 'source/bridge/server/inventory/ox.lua'],
['qb', 'source/bridge/server/inventory/qb.lua'],
['lj', 'source/bridge/server/inventory/qb.lua'],
['qs', 'source/bridge/server/inventory/qs.lua'],
['ps', 'source/bridge/server/inventory/ps.lua'],
['codem', 'source/bridge/server/inventory/codem.lua'],
['tgiann', 'source/bridge/server/inventory/tgiann.lua'],
['core', 'source/bridge/server/inventory/core.lua'],
['jpr', 'source/bridge/server/inventory/jpr.lua'],
['origen', 'source/bridge/server/inventory/origen.lua'],
['ak47', 'source/bridge/server/inventory/ak47.lua'],
['one', 'source/bridge/server/inventory/one.lua'],
['mf', 'source/bridge/server/inventory/mf.lua'],
['smx', 'source/bridge/server/inventory/smx.lua'],
['hex', 'source/bridge/server/inventory/esx.lua'],
['esx', 'source/bridge/server/inventory/esx.lua'],
] as const
describe('phone inventory contracts', () => {
it.each(inventoryAdapters)(
'registers the phone as a usable item through the %s adapter',
(_inventory, path) => {
expect(readResourceFile(path)).toContain(
'function Bridge.Inventory.RegisterUsableItem',
)
},
)
it('fails startup when the selected inventory cannot register the phone item', () => {
const phoneServer = readResourceFile('source/server/phone.lua')
expect(phoneServer).toContain(
'Bridge.Inventory.RegisterUsableItem(item_name, function(...)',
)
expect(phoneServer).toContain('if Config.Phone.Item == item_name then')
expect(phoneServer).toContain(
'if not Bridge.Inventory.RegisterUsableItem(item_name, function(...)',
)
})
it('uses One Inventory slot ids and authoritative slot reads', () => {
const adapter = readResourceFile(
'source/bridge/server/inventory/one.lua',
)
expect(adapter).toContain('inventory:GetSlotIdsWithItem(')
expect(adapter).toContain(
'local normalized = Bridge.Inventory.GetSlot(source, slot_id)',
)
expect(adapter).not.toContain('inventory:SearchInventory(')
expect(adapter).toContain(
'inventory:SetItemMetadata(source, slot.slot, requested_metadata) == false',
)
})
it('serializes and rate-limits phone bootstrap requests on both sides', () => {
const phoneClient = readResourceFile('source/client/main.lua')
const phoneServer = readResourceFile('source/server/phone.lua')
expect(phoneClient).toContain('local function request_phone_open(')
expect(phoneClient).toMatch(
/request_phone_open\(callback_name\)[\s\S]*?open_requested = true[\s\S]*?Bridge\.Callbacks\.Trigger\(callback_name, \{\}\)/,
)
expect(phoneClient).toMatch(
/RegisterNetEvent\("sky_phone:device:error"[\s\S]*?if not is_open then[\s\S]*?open_requested = false/,
)
expect(phoneServer).toContain('local phone_open_in_progress = {}')
expect(phoneServer).toContain(
'SkyPhone.AllowOperation(source, "phone_open", request_limit, 60)',
)
expect(phoneServer).toContain(
'pcall(perform_phone_open, source, used_item)',
)
})
it('auto-detects registered inventories and forces metadata-free adapters into compatible modes', () => {
const inventoryBridge = readResourceFile(
'source/bridge/server/inventory.lua',
)
expect(inventoryBridge).toContain(
'{ name = "hex", resource = "hex_4_inventory", framework = "esx", metadata = false },',
)
expect(inventoryBridge).toContain(
'GetResourceState(adapter.resource) == "started"',
)
expect(inventoryBridge).toContain('configured_inventory = adapter.name')
expect(inventoryBridge).toContain('selected_adapter.metadata == false')
expect(inventoryBridge).toContain('Config.Phone.Unique = false')
expect(inventoryBridge).toContain('Config.Sim.Enabled = false')
expect(inventoryBridge).toContain(
'does not support item metadata; unique phones and physical SIM cards were disabled automatically',
)
expect(inventoryBridge).toContain(
'AddEventHandler("sky_phone:configurator:serverUpdated"',
)
expect(inventoryBridge).not.toContain(
'cannot store unique phone or physical SIM metadata',
)
})
it('provides the LB IsOpen export alias from the authoritative client state', () => {
const phoneClient = readResourceFile('source/client/main.lua')
const phoneBridge = readResourceFile('source/bridge/phones/client/lb.lua')
expect(phoneBridge).toContain(
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsOpen"',
)
expect(phoneClient).toContain('open = is_open')
expect(phoneBridge).toContain('return get_phone_state_value("open")')
})
it('provides the LB equipped phone number exports from authoritative device state', () => {
const phoneClient = readResourceFile('source/client/main.lua')
const phoneServer = readResourceFile('source/server/phone.lua')
const clientBridge = readResourceFile('source/bridge/phones/client/lb.lua')
const serverBridge = readResourceFile('source/bridge/phones/server/lb.lua')
const serverLifecycle = readResourceFile(
'source/bridge/phones/server/lifecycle.lua',
)
expect(clientBridge).toMatch(
/"GetEquippedPhoneNumber",\s+phone\.GetEquippedPhoneNumber/,
)
expect(phoneClient).toContain('return device_payload.device.sim.number')
expect(phoneClient).toContain(
'Bridge.Callbacks.Trigger("sky_phone:device:equipped-number", {})',
)
expect(phoneServer).toContain(
'Bridge.Callbacks.Register("sky_phone:device:equipped-number", function(source)',
)
expect(phoneServer).toContain(
'function SkyPhone.GetEquippedPhoneNumber(player)',
)
expect(phoneServer).toContain(
'cache_equipped_phone_number(source, identifier, device.phone_number)',
)
expect(phoneServer).toContain(
'equipped_phone_sources[phone_number] = source',
)
const equippedNumberExport = phoneServer.match(
/function SkyPhone\.GetEquippedPhoneNumber\(player\)([\s\S]*?)\nfunction SkyPhone\.GetSourceFromNumber/,
)?.[1]
const equippedNumberResolver = phoneServer.match(
/local function resolve_equipped_phone_number\(source\)([\s\S]*?)\nfunction SkyPhone\.GetEquippedPhoneNumber/,
)?.[1]
expect(equippedNumberExport).toBeDefined()
expect(equippedNumberResolver).toBeDefined()
expect(equippedNumberExport).toContain('type(player) == "number"')
expect(equippedNumberExport).toContain(
'online_source_for_identifier(player)',
)
expect(phoneServer).toContain(
'Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)',
)
expect(phoneServer).toContain(
'return resolve_equipped_phone_number(player)',
)
expect(equippedNumberExport).not.toContain('tonumber(player)')
expect(equippedNumberExport).not.toContain('equipped_phone_numbers[player]')
expect(equippedNumberResolver).not.toContain('return cached_number')
expect(phoneServer).toContain(
'player_source and resolve_equipped_phone_number(player_source) == normalized',
)
expect(serverBridge).toMatch(
/"GetEquippedPhoneNumber",\s+phone\.GetEquippedPhoneNumber/,
)
expect(serverBridge).toMatch(
/"GetSourceFromNumber",\s+phone\.GetSourceFromNumber/,
)
expect(phoneServer).toContain(
'TriggerEvent("sky_phone:server:phoneNumberChanged", source, phone_number)',
)
expect(serverLifecycle).toContain(
'SkyPhoneCompatibility.EmitServerProviderStop(LB_PROVIDER_NAME)',
)
expect(serverLifecycle).toContain(
'SkyPhoneCompatibility.EmitServerProviderStart(LB_PROVIDER_NAME)',
)
})
it('maps LB client lifecycle and state contracts', () => {
const phoneClient = readResourceFile('source/client/main.lua')
const phoneBridge = readResourceFile('source/bridge/phones/client/lb.lua')
expect(phoneBridge).toContain(
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "ToggleOpen"',
)
expect(phoneClient).toContain(
'local result = Bridge.Callbacks.Trigger(callback_name, {})',
)
expect(phoneClient).toMatch(
/result\.success == true[\s\S]*open_requested = false[\s\S]*open_without_focus = false[\s\S]*return false/,
)
expect(phoneBridge).toContain(
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsPhoneOnScreen"',
)
expect(phoneBridge).toContain(
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsInCall"',
)
expect(phoneBridge).toContain(
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "FormatNumber", client_bridge.FormatNumber)',
)
expect(phoneClient).toContain(
'TriggerEvent("sky_phone:client:phoneNumberChanged", next_number)',
)
expect(phoneClient).toContain(
'TriggerEvent("sky_phone:client:phoneToggled", true)',
)
expect(phoneClient).toContain(
'TriggerEvent("sky_phone:client:phoneToggled", false)',
)
expect(phoneBridge).toContain(
'TriggerEvent("lb-phone:numberChanged", phone_number)',
)
expect(phoneBridge).toContain('TriggerEvent("lb-phone:phoneToggled", open)')
})
it('keeps vendor contracts outside the phone business core', () => {
const corePaths = [
'source/client/main.lua',
'source/client/camera.lua',
'source/client/custom_apps.lua',
'source/server/phone.lua',
'source/server/sim.lua',
'source/server/media.lua',
]
for (const path of corePaths) {
expect(readResourceFile(path)).not.toMatch(
/lb-phone|17mov|high-phone|qs-smartphone|yseries|SkyPhoneCompatibility/,
)
}
})
it('maps the LB custom-app delete lifecycle from the App Store to Lua', () => {
const appStore = readFrontendFile('stores/app-store.ts')
const customApps = readResourceFile('source/client/custom_apps.lua')
const compatibility = readResourceFile('source/bridge/phones/shared/lb.lua')
expect(appStore).toContain("event: 'delete'")
expect(customApps).toContain('and lifecycle_event ~= "delete"')
expect(customApps).toContain(
'invoke_or_defer_hook(app, "onDelete", lifecycle_payload, deferred_hooks)',
)
expect(compatibility).toContain('onDelete = app_data.onDelete')
})
it('emits exact LB observer events after authoritative state changes', () => {
const phonePersistence = readResourceFile(
'source/server/phone_persistence.lua',
)
const simServer = readResourceFile('source/server/sim.lua')
const mediaServer = readResourceFile('source/server/media.lua')
const phoneBridge = readResourceFile(
'source/bridge/phones/server/lifecycle.lua',
)
expect(simServer).toContain(
'TriggerEvent("sky_phone:server:phoneNumberGenerated", source, sim.phone_number)',
)
expect(phonePersistence).toContain(
'TriggerEvent("sky_phone:server:factoryReset", source, phone_number)',
)
expect(mediaServer).toContain(
'TriggerEvent("sky_phone:server:galleryMediaDeleted", src, phone_number, deleted_link)',
)
expect(phoneBridge).toContain(
'TriggerEvent("lb-phone:phoneNumberGenerated"',
)
expect(phoneBridge).toContain('TriggerEvent("lb-phone:factoryReset"')
expect(phoneBridge).toContain('TriggerEvent("lb-phone:deletedFromGallery"')
})
it('opens from a configurable F1 mapping without client-provided device identity', () => {
const config = readResourceFile('config/config.lua')
const phoneClient = readResourceFile('source/client/main.lua')
const phoneServer = readResourceFile('source/server/phone.lua')
expect(config).toContain('Keybind = "F1"')
expect(phoneClient).toContain('refresh_phone_key_mapping = function()')
expect(phoneClient).toContain(
'RegisterKeyMapping(command_name, locale.Controls.OpenPhone, "keyboard", key_name)',
)
expect(phoneClient).toContain(
'if active_key_mapping_command == command_name then',
)
expect(phoneClient).toContain(
'request_phone_open("sky_phone:device:open-request")',
)
expect(phoneServer).toContain(
'Bridge.Callbacks.Register("sky_phone:device:open-request", function(source)',
)
})
it('applies development command changes immediately without a resource restart', () => {
const phoneClient = readResourceFile('source/client/main.lua')
expect(phoneClient).toContain('local active_development_command = nil')
expect(phoneClient).toContain('refresh_development_command = function()')
expect(phoneClient).toContain(
'local command_name = Config.Phone.DevelopmentCommand and Config.Command or nil',
)
expect(phoneClient).toContain('RegisterCommand(command_name, function()')
expect(phoneClient).toContain(
'if active_development_command == command_name and Config.Phone.DevelopmentCommand then',
)
expect(phoneClient).toContain(
'TriggerEvent("chat:removeSuggestion", "/" .. active_development_command)',
)
expect(phoneClient).toMatch(
/AddEventHandler\("sky_phone:configurator:updated", function\(\)[\s\S]*?refresh_development_command\(\)/,
)
})
it('opens a running live activity with Space without affecting normal gameplay', () => {
const phoneClient = readResourceFile('source/client/main.lua')
expect(phoneClient).toContain(
'RegisterCommand("sky_phone_live_activity_open"',
)
expect(phoneClient).toContain(
'if not live_activity_active or is_open or open_requested then',
)
expect(phoneClient).toContain(
'RegisterKeyMapping(\n "sky_phone_live_activity_open"',
)
expect(phoneClient).toContain('"SPACE"')
expect(phoneClient).toContain('RegisterNUICallback("ui:live-activity"')
})
it('keeps a server-selected unique handset as the preferred hotkey device', () => {
const phoneServer = readResourceFile('source/server/phone.lua')
expect(phoneServer).toContain('local preferred_device_imeis = {}')
expect(phoneServer).toContain(
'local preferred_imei = preferred_device_imeis[source]',
)
expect(phoneServer).toContain('preferred_device_imeis[source] = imei')
})
it('keeps non-unique phones bound to one persistent device per character', () => {
const phoneServer = readResourceFile('source/server/phone.lua')
const migration = readResourceFile('source/server/db_migrate.lua')
expect(phoneServer).toContain('if Config.Phone.Unique == false then')
expect(phoneServer).toContain('return map_character_device(source, slot)')
expect(phoneServer).toContain('FROM `sky_phone_character_devices`')
expect(phoneServer).toContain('WHERE `owner_identifier` = ?')
expect(migration).toContain('name = "sky_phone_character_devices"')
expect(migration).toContain('primaryKey = "owner_identifier"')
})
})