test(scum): add migration parity fixtures
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { SCUMConfigRead, SCUMFeatureKey, SCUMFeatureMigrationAuthority, SCUMFeatureMigrationStatus, SCUMGiftGrant, SCUMMigrationRecord, SCUMPlayer, SCUMPlayerProfile, SCUMPlayerRisk, SCUMPlayerSession, SCUMStatePatch, SCUMTrajectory, SCUMTrajectoryPoint } from "./contracts.js";
|
||||
import { configurationCatalog } from "./schemas.js";
|
||||
|
||||
export function transitionalReadOnly<T extends Record<string, unknown>>(payload: T, recordedAt: string, sourceRecordId?: string): SCUMMigrationRecord<T> { return { provenance: "transitional-read-only", readOnly: true, payload, recordedAt, sourceRecordId }; }
|
||||
export function pluginOwned<T extends Record<string, unknown>>(payload: T, recordedAt: string): SCUMMigrationRecord<T> { return { provenance: "plugin", readOnly: false, payload, recordedAt }; }
|
||||
@@ -17,17 +18,18 @@ export function migrationStatus(flags: readonly SCUMFeatureMigrationAuthority[],
|
||||
|
||||
export function migratePlayerRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMPlayer> | null {
|
||||
const id = text(record.id); const gamePlayerId = text(record.gamePlayerId); const displayName = text(record.displayName); if (!id || !gamePlayerId || !displayName) return null;
|
||||
return transitionalReadOnly({ id, gamePlayerId, displayName, lastSeenAt: optionalText(record.lastSeenAt), status: record.online === true ? "online" : "unknown" }, optionalText(record.updatedAt) ?? new Date(0).toISOString(), id);
|
||||
const lastSeenAt = timestamp(record.lastSeenAt); const recordedAt = timestamp(record.updatedAt) ?? lastSeenAt ?? new Date(0).toISOString();
|
||||
return transitionalReadOnly({ id, gamePlayerId, displayName, ...(lastSeenAt ? { lastSeenAt } : {}), status: record.online === true ? "online" : "unknown" }, recordedAt, id);
|
||||
}
|
||||
|
||||
export function migrateConfigurationRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMConfigRead> | null {
|
||||
const version = text(record.version) ?? text(record.gameVersion); const fields = stringFields(record.fields); const observedAt = timestamp(record.observedAt) ?? timestamp(record.updatedAt); if (!version || !fields || !observedAt) return null;
|
||||
const version = text(record.version) ?? text(record.gameVersion); const fields = version ? allowlistedConfigFields(version, record.fields) : null; const observedAt = timestamp(record.observedAt) ?? timestamp(record.updatedAt); if (!version || !fields || !observedAt) return null;
|
||||
return transitionalReadOnly({ version, fields, observedAt }, observedAt, text(record.id));
|
||||
}
|
||||
|
||||
export function migratePlayerProfileRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMPlayerProfile> | null {
|
||||
const player = migratePlayerRecord(object(record.player) ?? record); if (!player) return null;
|
||||
const sessions = array(record.sessions).map(migrateSession).filter((item): item is SCUMPlayerSession => item !== null);
|
||||
const sessions = array(record.sessions).map((item) => migrateSession(item, player.payload.id)).filter((item): item is SCUMPlayerSession => item !== null);
|
||||
const risks = [...array(record.accessAttempts), ...array(record.securitySignals)].map(migrateRisk).filter((item): item is SCUMPlayerRisk => item !== null);
|
||||
const recordedAt = timestamp(record.updatedAt) ?? player.recordedAt;
|
||||
return transitionalReadOnly({ player: player.payload, sessions, risks }, recordedAt, player.sourceRecordId);
|
||||
@@ -46,8 +48,9 @@ export function migrateStatePatchRecord(record: Record<string, unknown>): SCUMMi
|
||||
}
|
||||
|
||||
export function migrateTrajectoryRecord(record: Record<string, unknown>): SCUMTrajectory | null {
|
||||
const subjectId = text(record.playerRecordId) ?? text(record.vehicleId); const subjectType = text(record.playerRecordId) ? "player" : "vehicle"; const points = Array.isArray(record.points) ? record.points.map(migratePoint).filter((point): point is SCUMTrajectoryPoint => point !== null) : [];
|
||||
return subjectId && points.length ? { subjectId, subjectType, points, provenance: "transitional-read-only" } : null;
|
||||
const subjectId = text(record.playerRecordId) ?? text(record.gamePlayerRecordId) ?? text(record.vehicleId) ?? text(record.entityId); const subjectType = trajectorySubjectType(record); const points = Array.isArray(record.points) && subjectId && subjectType ? record.points.map((point) => migratePoint(point, subjectId, subjectType)).filter((point): point is SCUMTrajectoryPoint => point !== null) : [];
|
||||
if (!subjectId || !subjectType || !points.length) return null;
|
||||
return { subjectId, subjectType, points, provenance: "transitional-read-only" };
|
||||
}
|
||||
|
||||
export function migrateTrajectoryHistoryRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMTrajectory> | null {
|
||||
@@ -56,13 +59,14 @@ export function migrateTrajectoryHistoryRecord(record: Record<string, unknown>):
|
||||
return transitionalReadOnly(trajectory, recordedAt, text(record.id));
|
||||
}
|
||||
|
||||
function migratePoint(value: unknown): SCUMTrajectoryPoint | null { if (!value || typeof value !== "object") return null; const record = value as Record<string, unknown>; const x = number(record.mapX) ?? number(record.worldX); const y = number(record.mapY) ?? number(record.worldY); const occurredAt = text(record.recordedAt) ?? text(record.occurredAt); if (x === undefined || y === undefined || !occurredAt) return null; return { occurredAt, subjectId: text(record.playerRecordId) ?? text(record.vehicleId) ?? "unknown", subjectType: text(record.playerRecordId) ? "player" : "vehicle", x, y, z: number(record.worldZ), source: "transitional-read-only" }; }
|
||||
function migrateSession(value: unknown): SCUMPlayerSession | null { const record = object(value); const id = record && text(record.id); const playerId = record && (text(record.gamePlayerRecordId) ?? text(record.playerId)); const startedAt = record && timestamp(record.startedAt); if (!id || !playerId || !startedAt) return null; const endedAt = timestamp(record.endedAt); return { id, playerId, kind: endedAt ? "logout" : "login", occurredAt: endedAt ?? startedAt }; }
|
||||
function migratePoint(value: unknown, defaultSubjectId: string, defaultSubjectType: SCUMTrajectoryPoint["subjectType"]): SCUMTrajectoryPoint | null { const record = object(value); if (!record) return null; const x = number(record.mapX) ?? number(record.worldX); const y = number(record.mapY) ?? number(record.worldY); const occurredAt = timestamp(record.recordedAt) ?? timestamp(record.occurredAt); if (x === undefined || y === undefined || !occurredAt) return null; const subjectId = text(record.playerRecordId) ?? text(record.gamePlayerRecordId) ?? text(record.vehicleId) ?? text(record.entityId) ?? defaultSubjectId; const subjectType = trajectorySubjectType(record) ?? defaultSubjectType; return { occurredAt, subjectId, subjectType, x, y, ...(number(record.worldZ) !== undefined ? { z: number(record.worldZ) } : {}), source: "transitional-read-only" }; }
|
||||
function migrateSession(value: unknown, defaultPlayerId: string): SCUMPlayerSession | null { const record = object(value); const id = record && text(record.id); const playerId = record && (text(record.gamePlayerRecordId) ?? text(record.playerId) ?? defaultPlayerId); const startedAt = record && timestamp(record.startedAt); if (!id || !playerId || !startedAt) return null; const endedAt = timestamp(record.endedAt); return { id, playerId, kind: endedAt ? "logout" : "login", occurredAt: endedAt ?? startedAt }; }
|
||||
function migrateRisk(value: unknown): SCUMPlayerRisk | null { const record = object(value); const observedAt = record && (timestamp(record.occurredAt) ?? timestamp(record.lastObservedAt)); const kind = record && (text(record.ruleKey) ?? text(record.outcome)); const summary = record && (text(record.summary) ?? text(record.reason)); if (!observedAt || !kind || !summary) return null; return { kind, level: "medium", observedAt, summary }; }
|
||||
function migrateStateChange(value: unknown): { fieldKey: string; before: number; after: number } | null { const record = object(value); if (!record) return null; const fieldKey = text(record.fieldKey); const before = number(record.before); const after = number(record.after); return fieldKey && before !== undefined && after !== undefined ? { fieldKey, before, after } : null; }
|
||||
function stringFields(value: unknown): Record<string, string> | null { const fields = object(value); if (!fields) return null; const result: Record<string, string> = {}; for (const [key, field] of Object.entries(fields)) { if (!text(key) || !(typeof field === "string" || typeof field === "number" || typeof field === "boolean")) return null; result[key] = String(field); } return result; }
|
||||
function allowlistedConfigFields(version: string, value: unknown): Record<string, string> | null { const fields = object(value); const allowed = new Set(configurationCatalog(version).map((field) => field.configKey)); if (!fields || !allowed.size) return null; const result: Record<string, string> = {}; for (const [key, field] of Object.entries(fields)) { if (allowed.has(key) && (typeof field === "string" || typeof field === "number" || typeof field === "boolean")) result[key] = String(field); } return Object.keys(result).length ? result : null; }
|
||||
function giftStatus(value: unknown): SCUMGiftGrant["status"] | null { return value === "pending-approval" || value === "queued" || value === "delivered" || value === "notification_failed" || value === "failed" || value === "unknown" ? value : null; }
|
||||
function stateStatus(value: unknown): SCUMStatePatch["status"] | null { if (value === "pending-approval" || value === "queued" || value === "unsupported" || value === "unknown") return value; if (value === "confirmed") return "succeeded"; return value === "execution-failed" || value === "execution-unknown" || value === "confirmation-failed" || value === "failed" ? "failed" : null; }
|
||||
function stateStatus(value: unknown): SCUMStatePatch["status"] | null { if (value === "pending-approval" || value === "queued" || value === "unsupported" || value === "unknown" || value === "execution-unknown") return value === "execution-unknown" ? "unknown" : value; if (value === "confirmed") return "succeeded"; return value === "execution-failed" || value === "confirmation-failed" || value === "failed" ? "failed" : null; }
|
||||
function trajectorySubjectType(record: Record<string, unknown>): SCUMTrajectoryPoint["subjectType"] | null { if (record.kind === "player" || record.kind === "vehicle") return record.kind; return text(record.playerRecordId) || text(record.gamePlayerRecordId) ? "player" : text(record.vehicleId) ? "vehicle" : null; }
|
||||
function array(value: unknown): unknown[] { return Array.isArray(value) ? value : []; }
|
||||
function object(value: unknown): Record<string, unknown> | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null; }
|
||||
function text(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value : undefined; }
|
||||
|
||||
Reference in New Issue
Block a user