Restore durable log ingest and typed plugin projections

This commit is contained in:
npc0-hue
2026-09-02 10:20:30 +08:00
parent 40ac46ba17
commit 6018d8f0fc
61 changed files with 809 additions and 3157 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ export const scumMigrationParityFixtures = {
expected: { provenance: "transitional-read-only", readOnly: true, sourceRecordId: "patch-1", recordedAt: "2026-07-29T00:30:00Z", payload: { id: "patch-1", playerId: "player-1", expectedStateVersion: "state-1", safetyWindow: "maintenance", reason: "verified test", status: "unknown", createdAt: "2026-07-29T00:30:00Z", changes: [{ fieldKey: "skills.running", before: 1, after: 2 }] } }
},
trajectory: {
source: { id: "trajectory-1", updatedAt: "2026-07-29T00:40:00Z", kind: "player", entityId: "steam-1", gamePlayerRecordId: "player-1", points: [{ mapX: 10, mapY: 20, occurredAt: "2026-07-29T00:39:00Z", source: "legacy-log" }, { mapX: 30, mapY: 40, occurredAt: "not-a-timestamp" }] },
source: { id: "trajectory-1", updatedAt: "2026-07-29T00:40:00Z", kind: "player", entityId: "steam-1", gamePlayerRecordId: "player-1", points: [{ mapX: 10, mapY: 20, occurredAt: "2026-07-29T00:39:00Z", source: "log-projection" }, { mapX: 30, mapY: 40, occurredAt: "not-a-timestamp" }] },
expected: { provenance: "transitional-read-only", readOnly: true, sourceRecordId: "trajectory-1", recordedAt: "2026-07-29T00:40:00Z", payload: { subjectId: "player-1", subjectType: "player", provenance: "transitional-read-only", points: [{ occurredAt: "2026-07-29T00:39:00Z", subjectId: "player-1", subjectType: "player", x: 10, y: 20, source: "transitional-read-only" }] } }
}
} as const;
+19 -214
View File
@@ -26,15 +26,13 @@ import {
parseBridgeExecutionResponse,
parseAIInvocationResponse,
type GameClientBridgeQueryTemplateDeclaration,
type GameClientBridgeLogProjectionDeclaration,
type GameClientBridgeCompanionDeclaration,
type GamePluginManifest,
type RuntimeLogEventDeclaration,
type RuntimeClientManagerProfile,
type PluginLifecycleActionDeclaration,
type PluginBridgeContext
} from "../sdk/index.js";
import { validateGameClientBridgeCatalog, validateLifecycleActionFile, validateManifestFile } from "../scripts/validate-manifest.js";
import { validateLifecycleActionFile, validateManifestFile } from "../scripts/validate-manifest.js";
const pluginsRoot = fileURLToPath(new URL("..", import.meta.url));
@@ -131,33 +129,6 @@ function validateTemporaryBridgeManifest(mutate?: (manifest: MutableBridgeManife
}
}
type MutableLogEventManifest = {
permissions: string[];
runtimeProfiles?: {
logSources?: Array<Record<string, unknown>>;
logEvents?: Array<Record<string, unknown>>;
};
};
function validateTemporaryLogEventManifest(mutate?: (manifest: MutableLogEventManifest, fixtureDir: string) => void): string[] {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-log-event-manifest-"));
try {
fs.cpSync(path.join(pluginsRoot, "examples/dev-game-plugin"), fixtureDir, { recursive: true });
const manifestPath = path.join(fixtureDir, "manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MutableLogEventManifest;
manifest.runtimeProfiles = {
logSources: [{ key: "server-events", kind: "file.tail", targetKey: "logs/server", streamKey: "game.server", cursorKind: "fingerprint", retentionDays: 30 }],
logEvents: [{ key: "player-login", title: "Player login", sourceKey: "server-events", eventType: "game.login", permission: "server.logs.read", schemaRef: "schemas/log-events/login.event.schema.json", retentionDays: 30, severity: "info" }]
};
writeFixtureJSON(fixtureDir, "schemas/log-events/login.event.schema.json", bridgeObjectSchema({ occurredAt: { type: "string", minLength: 1, maxLength: 40 }, playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["occurredAt", "playerId"]));
mutate?.(manifest, fixtureDir);
writeFixtureJSON(fixtureDir, "manifest.json", manifest);
return validateManifestFile(manifestPath);
} finally {
fs.rmSync(fixtureDir, { recursive: true, force: true });
}
}
function validateTemporaryScumCompanionManifest(mutate: (manifest: Record<string, any>, fixtureDir: string) => void): string[] {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-scum-companion-manifest-"));
try {
@@ -190,24 +161,13 @@ describe("plugin manifest validation", () => {
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
});
it("declares BattlEye login projection and presence deduplication", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as any;
const projection = manifest.gameClientBridge.logProjections.find((candidate: { key: string }) => candidate.key === "scum.battleye.login");
expect(projection).toMatchObject({
streamKeys: ["scum.console.stdout"], correlationFields: ["slot"], maxInterveningLines: 8,
target: { collection: "scum_users", upsertKeys: ["steamId"], captureMappings: { steamId: "steamId", displayName: "displayName", slot: "slot" }, fixedValues: { online: "true", source: "process.stdout" }, observedAtField: "lastLoginObservedAt" },
presence: { timestampField: "lastLoginObservedAt", activeWindowSeconds: 600, activityTarget: { collection: "scum_activity_events", upsertKeys: ["steamId", "observedAt"], captureMappings: { steamId: "steamId", displayName: "displayName" }, fixedValues: { eventType: "login", source: "process.stdout" }, observedAtField: "observedAt" } }
});
expect(projection.steps.map((step: { pattern: string }) => step.pattern)).toEqual([
'Player "(?P<displayName>[^\"]+)" reported as player (?P<slot>\\d+)',
"Player (?P<slot>\\d+) SteamID \\(assumed\\): (?P<steamId>\\d+)"
]);
const compile = (pattern: string) => new RegExp(pattern.replaceAll("(?P<", "(?<"));
expect(compile(projection.steps[0].pattern).exec('LogBattlEye: Display: Player "love_fitting" reported as player 0')?.groups).toMatchObject({ displayName: "love_fitting", slot: "0" });
expect(compile(projection.steps[1].pattern).exec("LogBattlEye: Display: Player 0 SteamID (assumed): 76561199510658111")?.groups).toMatchObject({ slot: "0", steamId: "76561199510658111" });
const loginLogProjection = manifest.gameClientBridge.logProjections.find((candidate: { key: string }) => candidate.key === "scum.login-log.login");
expect(loginLogProjection.target.captureMappings).toMatchObject({ steamId: "steamId", displayName: "displayName", lastLoginIp: "ip" });
expect(loginLogProjection.presence.activityTarget.captureMappings).toMatchObject({ steamId: "steamId", displayName: "displayName", lastLoginIp: "ip" });
it("declares SCUM user projection through SQLite", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
gameClientBridge: { queryTemplates: Array<{ key: string; engine: string; transportKey: string; targetKey: string; projections?: Array<{ collection: string; fixedValues?: Record<string, string> }> }> };
};
const users = manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.player.profile");
expect(users).toMatchObject({ engine: "sqlite", transportKey: "scum-database", targetKey: "scum-database" });
expect(users?.projections).toEqual(expect.arrayContaining([expect.objectContaining({ collection: "scum_users", fixedValues: { source: "run.sqlite.scum.player.profile" } })]));
});
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
@@ -309,7 +269,6 @@ describe("plugin manifest validation", () => {
expect(validate(example), JSON.stringify(validate.errors)).toBe(true);
expect(JSON.stringify(example)).not.toMatch(/authKey|componentKey|credential|password|sessionToken|secret|\/api\/v1\/scum-clients\//i);
expect(example).toMatchObject({ proof: { materialEnv: "SCUM_COMPONENT_PROOF" }, session: { mode: "component-session" }, tls: { policy: "verify-system-roots" } });
expect(example).toMatchObject({ trajectory: { enabled: true, source: "scum-sqlite", store: "shared-platform-mysql", fileEnv: "SCUM_DB_FILE", intervalSeconds: 3, maxRows: 500 } });
});
it("rejects unsafe SCUM companion bootstrap policy and inline session material", () => {
@@ -461,7 +420,6 @@ describe("plugin manifest validation", () => {
}>;
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
queryTemplates: Array<{ key: string; projections?: Array<{ collection?: string; fixedValues?: Record<string, string>; mergeExisting?: boolean }> }>;
logProjections?: Array<{ key: string; streamKeys?: string[]; target?: { collection?: string; upsertKeys?: string[]; captureMappings?: Record<string, string> } }>;
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
};
pages: Array<{ key: string; permissions?: string[] }>;
@@ -488,7 +446,7 @@ describe("plugin manifest validation", () => {
const serialized = JSON.stringify(manifest).toLowerCase();
expect(serialized).not.toContain("local-proof");
expect(manifest.version).toBe("0.1.14");
expect(manifest.version).toBe("0.1.15");
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
@@ -502,10 +460,9 @@ describe("plugin manifest validation", () => {
"maintenance.prepare"
]));
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"]));
expect(manifest.gameClientBridge.queryTemplates.every((template) => !template.projections?.length)).toBe(true);
expect(manifest.gameClientBridge.logProjections?.map((projection) => projection.key)).toEqual(expect.arrayContaining(["scum.trade.catalog", "scum.trade.events"]));
expect(manifest.gameClientBridge.logProjections?.find((projection) => projection.key === "scum.trade.catalog")).toMatchObject({ streamKeys: ["scum.trade"], target: { collection: "scum_trade_goods", upsertKeys: ["code"], captureMappings: { code: "itemCode" } } });
expect(manifest.gameClientBridge.logProjections?.find((projection) => projection.key === "scum.trade.events")?.target?.collection).toBe("scum_trade_events");
expect(manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.vehicles")?.projections).toEqual(expect.arrayContaining([
expect.objectContaining({ collection: "scum_trade_goods", mergeExisting: true, fixedValues: expect.objectContaining({ catalogType: "vehicle", type: "21", typeName: "其他载具" }) })
]));
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
@@ -594,7 +551,7 @@ describe("plugin manifest validation", () => {
}
});
it("declares bounded SCUM snapshot schemas for operations views", () => {
it("declares bounded SCUM snapshot schemas for operations projections", () => {
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
gameClientBridge: {
@@ -672,7 +629,7 @@ describe("plugin manifest validation", () => {
const fastTemplates = new Set(["scum.player.profile", "scum.vehicles", "scum.positions"]);
const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template]));
expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
expect([...templatesByKey.values()].every((template) => !template.projections?.length)).toBe(true);
expect(templatesByKey.get("scum.player.profile")?.projections).toEqual([expect.objectContaining({ collection: "scum_users", rowPath: "rows", upsertKeys: ["steamId"], observedAtField: "profileSampledAt", mergeExisting: true })]);
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
expect(manifest.capabilities).toContain("remote.run.db.sqlite.execute");
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query");
@@ -682,6 +639,7 @@ describe("plugin manifest validation", () => {
const sqliteTransport = manifest.runtimeProfiles?.transportProfiles?.find((profile) => profile.key === "scum-database");
expect(sqliteTransport).toMatchObject({ kind: "sqlite", targetKey: "scum-database" });
expect(sqliteTransport?.capabilities).toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
expect(manifest.runtimeProfiles?.dataTargets).toEqual(expect.arrayContaining([expect.objectContaining({ key: "scum-database", kind: "sqlite.snapshot", sourcePath: "SCUM/Saved/SaveFiles/SCUM.db", workspaceKey: "databases/scum-database" })]));
for (const key of expectedKeys) {
const template = templatesByKey.get(key)!;
expect(template.engine).toBe("sqlite");
@@ -729,122 +687,26 @@ describe("plugin manifest validation", () => {
}
});
it("packages SCUM v57 config, UTF-16LE logs, and gift metadata inside the plugin", () => {
it("packages SCUM v57 config and gift metadata inside the plugin", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
gameClientBridge: { dataPacks: Array<{ key: string; databaseUserVersion: number; logParserRefs: string[]; configMapRefs: string[]; dataRefs?: string[] }> };
gameClientBridge: { dataPacks: Array<{ key: string; databaseUserVersion: number; configMapRefs: string[]; dataRefs?: string[] }> };
};
const pack = manifest.gameClientBridge.dataPacks.find((candidate) => candidate.key === "scum-db-v57");
expect(pack).toMatchObject({ databaseUserVersion: 57 });
const logParsers = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.logParserRefs[0]), "utf8"));
const configMaps = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.configMapRefs[0]), "utf8"));
const giftMetadata = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![0]), "utf8"));
const mapGeometry = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![1]), "utf8"));
const storageModel = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![2]), "utf8"));
expect(logParsers).toMatchObject({ encoding: "utf-16le", lineEnding: "lf", continuationPolicy: "append-to-previous-timestamped-record", timestampFormat: "yyyy.MM.dd-HH.mm.ss" });
expect(logParsers.parsers.map((parser: { key: string }) => parser.key)).toEqual(expect.arrayContaining(["login", "chat", "admin", "kill", "event-kill", "quests", "vehicle-destruction"]));
expect(configMaps.maps.map((map: { key: string }) => map.key)).toEqual(expect.arrayContaining(["server-settings", "economy-override", "raid-times", "notifications", "admin-users", "banned-users"]));
expect(giftMetadata).toMatchObject({ databaseUserVersion: 57, catalogSource: { configMapKey: "economy-override" } });
expect(mapGeometry).toMatchObject({ databaseUserVersion: 57, image: { path: "assets/map/scum-map-overview.jpg", width: 256, height: 256 }, runtimeOverride: { kilometersToWorldUnits: 100000 } });
expect(storageModel).toMatchObject({ databaseUserVersion: 57, store: "plugin-shared-platform-mysql", tables: [expect.objectContaining({ name: "scum_trajectories", writer: "companion.SCUMSQLStore.StoreTrajectorySamples", coordinateColumns: ["world_x", "world_y", "world_z"], coordinatePolicy: "store-game-world-coordinates-only" })] });
});
it("declares typed SCUM semantic log events with bounded schemas", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
permissions: string[];
runtimeProfiles?: {
logSources?: Array<{ key: string; kind?: string; streamKey?: string; retentionDays?: number }>;
logEvents?: Array<RuntimeLogEventDeclaration>;
};
};
const expectedTypes = ["scum.chat", "scum.login", "scum.logout", "scum.kill", "scum.trade", "scum.mine", "scum.unlock", "scum.admin", "scum.performance"];
const logSources = new Map((manifest.runtimeProfiles?.logSources ?? []).map((source) => [source.key, source]));
const logEvents = manifest.runtimeProfiles?.logEvents ?? [];
expect(logEvents.map((event) => event.eventType)).toEqual(expect.arrayContaining(expectedTypes));
expect(logSources.get("scum-console-stdout")).toMatchObject({ kind: "process.stdout", streamKey: "scum.console.stdout" });
expect(logSources.get("scum-console-stderr")).toMatchObject({ kind: "process.stderr", streamKey: "scum.console.stderr" });
expect(new Set(logEvents.map((event) => event.key)).size).toBe(logEvents.length);
expect(new Set(logEvents.map((event) => event.eventType)).size).toBe(logEvents.length);
for (const event of logEvents) {
const source = logSources.get(event.sourceKey);
expect(source).toBeDefined();
expect(manifest.permissions).toContain(event.permission);
expect(event.retentionDays).toBeGreaterThanOrEqual(1);
expect(event.retentionDays).toBeLessThanOrEqual(source?.retentionDays ?? 365);
expect(["info", "notice", "warning", "critical"]).toContain(event.severity);
expect(event.schemaRef).toMatch(/^schemas\/log-events\/[a-z-]+\.event\.schema\.json$/);
const schema = JSON.parse(fs.readFileSync(path.join(pluginDir, event.schemaRef), "utf8")) as Record<string, unknown>;
expect((schema.properties as Record<string, Record<string, unknown>>).occurredAt).toMatchObject({ type: "string", format: "date-time" });
const visit = (value: unknown): void => {
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (typeof value !== "object" || value === null) {
return;
}
const record = value as Record<string, unknown>;
if (record.type === "object" || Object.hasOwn(record, "properties")) {
expect(record.additionalProperties).toBe(false);
}
if (record.type === "array") {
expect(record.maxItems).toBeGreaterThan(0);
}
if (record.type === "string" && !Object.hasOwn(record, "enum") && !Object.hasOwn(record, "const")) {
expect(record.maxLength).toBeGreaterThan(0);
}
if (record.type === "integer" || record.type === "number") {
expect(record.minimum).toBeDefined();
expect(record.maximum).toBeDefined();
}
Object.values(record).forEach(visit);
};
expect(schema.type).toBe("object");
expect(JSON.stringify(schema).toLowerCase()).not.toMatch(/sqltext|shellcommand|hostpath|rawpath|password|credential|runsocket|directsocket/);
visit(schema);
}
const login = logEvents.find((event) => event.eventType === "scum.login");
const loginSchema = JSON.parse(fs.readFileSync(path.join(pluginDir, login?.schemaRef ?? ""), "utf8")) as { properties?: Record<string, Record<string, unknown>> };
expect(loginSchema.properties?.networkFingerprint).toMatchObject({ type: "string", writeOnly: true });
});
it("rejects unsafe semantic log declarations and missing references", () => {
const errors = validateTemporaryLogEventManifest((manifest) => {
const event = manifest.runtimeProfiles!.logEvents![0];
event.eventType = "shell.execute";
event.sourceKey = "missing-source";
event.permission = "server.game-client.read";
event.schemaRef = "schemas/log-events/missing.event.schema.json";
event.retentionDays = 366;
event.severity = "urgent";
});
expect(errors.some((error) => error.includes("eventType") && error.includes("not allowed"))).toBe(true);
expect(errors.some((error) => error.includes("sourceKey") && error.includes("undeclared log source"))).toBe(true);
expect(errors.some((error) => error.includes("permission") && error.includes("declared"))).toBe(true);
expect(errors.some((error) => error.includes("schemaRef") && error.includes("missing semantic log event schema"))).toBe(true);
expect(errors.some((error) => error.includes("retentionDays"))).toBe(true);
expect(errors.some((error) => error.includes("severity"))).toBe(true);
});
it("rejects unsafe or unbounded semantic log event schemas", () => {
const errors = validateTemporaryLogEventManifest((_manifest, fixtureDir) => {
writeFixtureJSON(fixtureDir, "schemas/log-events/login.event.schema.json", bridgeObjectSchema({ hostPath: { type: "string" }, details: { type: "string" }, count: { type: "integer" } }, ["hostPath", "details", "count"]));
});
expect(errors.some((error) => error.includes("schemaRef") && error.includes("raw host path"))).toBe(true);
expect(errors.some((error) => error.includes("maxLength") && error.includes("bounded event strings"))).toBe(true);
expect(errors.some((error) => error.includes("bounded event numbers"))).toBe(true);
});
it("aligns the SCUM Client Manager declaration with the real Go bootstrap", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
runtimeProfiles?: { clientManagers?: Array<{
key: string;
repository?: { url?: string; branch?: string };
build?: { workspaceRef?: string; entryRef?: string };
configTemplates?: Array<{ key?: string; templateRef?: string; outputRef?: string }>;
deployment?: { arguments?: string[] };
@@ -852,8 +714,8 @@ describe("plugin manifest validation", () => {
}> };
};
const manager = manifest.runtimeProfiles?.clientManagers?.find((profile) => profile.key === "scum-client-manager");
expect(manager?.repository).toMatchObject({ url: "https://git.npc0.com/admin343/browser.git", branch: "main" });
expect(manager?.build).toMatchObject({ workspaceRef: "plugins/examples/scum-server-plugin/companion", entryRef: "cmd/scum-companion" });
expect(manager?.build).toMatchObject({ entryRef: "main.go" });
expect(manager?.build).not.toHaveProperty("workspaceRef");
expect(manager?.configTemplates).toEqual([{ key: "client-config", templateRef: "config.yaml.example", outputRef: "config.yaml" }]);
expect(manager?.deployment?.arguments).toBeUndefined();
expect(manager?.health).toMatchObject({ intervalSeconds: 30, degradedAfterSeconds: 90, offlineAfterSeconds: 120 });
@@ -873,7 +735,6 @@ describe("plugin manifest validation", () => {
manifest.gameClientBridge = {
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", resultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }],
snapshots: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }],
logProjections: [{ key: "player.login", streamKeys: ["process.stdout"], steps: [{ pattern: "Player \\\"(?<name>[^\\\"]+)\\\" reported as player (?<slot>\\\\d+)" }, { pattern: "Player (?<slot>\\\\d+) SteamID: (?<steamId>\\\\d+)" }], correlationFields: ["slot"], maxInterveningLines: 16, target: { collection: "users", upsertKeys: ["steamId"], captureMappings: { steamId: "steamId", name: "name" }, observedAtField: "lastLoginAt" }, presence: { timestampField: "lastLoginAt", activeWindowSeconds: 600 } }],
commandRetentionSeconds: 86400,
maxCommands: 1000,
pages: []
@@ -884,38 +745,6 @@ describe("plugin manifest validation", () => {
expect(validate(manifest)).toBe(false);
});
it("validates ordered log projections and repeated correlation captures", () => {
const projection = {
key: "player.login",
streamKeys: ["process.stdout"],
steps: [
{ pattern: "Player \\\"(?<name>[^\\\"]+)\\\" reported as player (?<slot>\\\\d+)" },
{ pattern: "Player (?<slot>\\\\d+) SteamID: (?<steamId>\\\\d+)" }
],
correlationFields: ["slot"],
maxInterveningLines: 16,
target: { collection: "users", upsertKeys: ["steamId"], captureMappings: { steamId: "steamId", name: "name" }, observedAtField: "lastLoginAt" },
presence: {
timestampField: "lastLoginAt",
activeWindowSeconds: 600,
activityTarget: { collection: "activity", upsertKeys: ["steamId"], captureMappings: { steamId: "steamId" }, observedAtField: "observedAt" }
}
};
const manifest = {
permissions: ["server.game-client.command"],
runtimeProfiles: { clientManagers: [{ key: "scum-client", health: { requiredCapabilities: ["game-client.bridge"] } }] },
gameClientBridge: {
commands: [{ type: "diagnostic.ping", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json" }],
snapshots: [],
logProjections: [projection]
}
};
expect(validateGameClientBridgeCatalog(manifest)).toEqual([]);
projection.target.captureMappings.steamId = "missing";
const errors = validateGameClientBridgeCatalog(manifest);
expect(errors.some((error) => error.includes("references undeclared capture missing"))).toBe(true);
});
it("loads and validates every schema referenced by a safe game-client bridge manifest", () => {
expect(validateTemporaryBridgeManifest()).toEqual([]);
@@ -1102,19 +931,6 @@ describe("plugin manifest validation", () => {
describe("plugin SDK", () => {
it("types generic runtime semantic log event declarations", () => {
const declaration: RuntimeLogEventDeclaration = {
key: "scum-performance",
title: "SCUM server performance",
sourceKey: "scum-performance-events",
eventType: "scum.performance",
permission: "server.logs.read",
schemaRef: "schemas/log-events/performance.event.schema.json",
retentionDays: 30,
severity: "info"
};
expect(declaration).toMatchObject({ eventType: "scum.performance", permission: "server.logs.read", severity: "info" });
});
it("types read-only SQLite query template declarations", () => {
const declaration: GameClientBridgeQueryTemplateDeclaration = {
@@ -1134,17 +950,6 @@ describe("plugin SDK", () => {
expect(JSON.stringify(declaration).toLowerCase()).not.toMatch(/sqltext|dsn|hostpath|socket|credential/);
});
it("types plugin-declared ordered log projections", () => {
const declaration: GameClientBridgeLogProjectionDeclaration = {
key: "scum.player.login",
streamKeys: ["process.stdout"],
steps: [{ pattern: "Player (?<slot>\\d+) SteamID: (?<steamId>\\d+)" }],
correlationFields: ["slot"],
maxInterveningLines: 16,
target: { collection: "scum_users", upsertKeys: ["steamId"], captureMappings: { steamId: "steamId" }, observedAtField: "lastLoginAt" }
};
expect(declaration).toMatchObject({ key: "scum.player.login", correlationFields: ["slot"] });
});
it("builds safe game-client bridge requests without component transport material", () => {
const request = createGameClientBridgeQueueRequest({
+2 -2
View File
@@ -39,8 +39,8 @@ const surfaceData: SCUMSurfaceData = {
mapSettings: [],
vehicles: [{ vehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", position: { x: 400, y: 200, z: 0 }, freshness: { status: "fresh" } }],
trajectories: [
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, sampledAt: "2026-08-10T00:00:03Z", source: "plugin.sql.scum.positions" },
{ subjectType: "vehicle", subjectId: "veh-1", vehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", x: 400, y: 200, z: 0, sampledAt: "2026-08-10T00:00:03Z", source: "plugin.sql.scum.vehicles" }
{ subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, sampledAt: "2026-08-10T00:00:03Z", source: "run.sqlite.scum.positions" },
{ subjectType: "vehicle", subjectId: "veh-1", vehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", x: 400, y: 200, z: 0, sampledAt: "2026-08-10T00:00:03Z", source: "run.sqlite.scum.vehicles" }
],
flags: [{ flagId: "flag-1", name: "Wolves Flag", ownerSquadId: "squad-1", ownershipConfidence: "verified", position: { x: 100, y: 80, z: 0 }, freshness: { status: "fresh" } }]
};