feat(scum): gate live data on schema evidence
This commit is contained in:
@@ -70,6 +70,7 @@
|
||||
"remote.rsync.write",
|
||||
"remote.run.files.read",
|
||||
"remote.run.files.write",
|
||||
"remote.run.db.sqlite.probe",
|
||||
"remote.run.db.sqlite.query",
|
||||
"remote.run.process.start",
|
||||
"remote.run.process.stop",
|
||||
@@ -95,6 +96,7 @@
|
||||
"runCapabilities": [
|
||||
"remote.run.files.read",
|
||||
"remote.run.files.write",
|
||||
"remote.run.db.sqlite.probe",
|
||||
"remote.run.db.sqlite.query",
|
||||
"remote.run.process.start",
|
||||
"remote.run.process.stop",
|
||||
@@ -556,6 +558,36 @@
|
||||
"requestTimeoutSeconds": 15
|
||||
}
|
||||
},
|
||||
"scumLiveData": {
|
||||
"schemaVersion": "1",
|
||||
"probe": {
|
||||
"capability": "remote.run.db.sqlite.probe",
|
||||
"targetKey": "scum-database",
|
||||
"bounds": {
|
||||
"maxObjects": 256,
|
||||
"maxColumnsPerObject": 128,
|
||||
"maxIndexesPerObject": 64,
|
||||
"maxForeignKeys": 64,
|
||||
"maxCardinalityReads": 64,
|
||||
"maxSampleRows": 3,
|
||||
"timeoutMs": 5000,
|
||||
"maxResultBytes": 524288
|
||||
}
|
||||
},
|
||||
"capabilityGates": [
|
||||
{ "capability": "schema-probe", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前绑定的 Run 证明通用只读 schema probe 可用。" },
|
||||
{ "capability": "players.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务 schema evidence 与版本化 adapter 匹配。" },
|
||||
{ "capability": "player-details.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务玩家详情 join 与字段含义验证。" },
|
||||
{ "capability": "squads.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务队伍表与 rank/leader 含义验证。" },
|
||||
{ "capability": "squad-members.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务队伍成员 join 唯一性验证。" },
|
||||
{ "capability": "vehicles.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务载具 identity、ownership 和坐标验证。" },
|
||||
{ "capability": "flags.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务旗子 ownership 与 territory 关系验证。" },
|
||||
{ "capability": "positions.read", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待当前服务坐标范围、cadence 和 map transform 验证。" },
|
||||
{ "capability": "profile-xml.write", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待真实 XML 来源、字段名、backup 和离线安全证据验证。" },
|
||||
{ "capability": "economy-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待 Fame/currency command 与 readback confirmation 验证。" },
|
||||
{ "capability": "gift-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v0", "evidenceStatus": "missing", "safeReason": "等待 gift item catalog、transport 和 conclusive receipt 验证。" }
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
"server.create",
|
||||
"server.read",
|
||||
@@ -1069,6 +1101,7 @@
|
||||
"kind": "sqlite",
|
||||
"targetKey": "scum-database",
|
||||
"capabilities": [
|
||||
"remote.run.db.sqlite.probe",
|
||||
"remote.run.db.sqlite.query",
|
||||
"remote.run.protected.sql"
|
||||
]
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"gameClientBridge": {
|
||||
"$ref": "#/$defs/gameClientBridgeManifest"
|
||||
},
|
||||
"scumLiveData": { "$ref": "#/$defs/scumLiveDataManifest" },
|
||||
"mapTrajectories": { "$ref": "#/$defs/mapTrajectoryDeclaration" },
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
@@ -287,6 +288,48 @@
|
||||
"companion": { "$ref": "#/$defs/gameClientBridgeCompanion" }
|
||||
}
|
||||
},
|
||||
"scumLiveDataManifest": {
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "probe", "capabilityGates"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": { "const": "1" },
|
||||
"probe": { "$ref": "#/$defs/scumSchemaProbeDeclaration" },
|
||||
"capabilityGates": { "type": "array", "items": { "$ref": "#/$defs/scumLiveDataCapabilityGate" }, "uniqueItems": true, "minItems": 1, "maxItems": 32 }
|
||||
}
|
||||
},
|
||||
"scumSchemaProbeDeclaration": {
|
||||
"type": "object",
|
||||
"required": ["capability", "targetKey", "bounds"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capability": { "const": "remote.run.db.sqlite.probe" },
|
||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"bounds": { "$ref": "#/$defs/scumSchemaProbeBounds" }
|
||||
}
|
||||
},
|
||||
"scumSchemaProbeBounds": {
|
||||
"type": "object",
|
||||
"required": ["maxObjects", "maxColumnsPerObject", "maxIndexesPerObject", "maxForeignKeys", "maxCardinalityReads", "maxSampleRows", "timeoutMs", "maxResultBytes"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"maxObjects": { "type": "integer", "minimum": 1, "maximum": 512 }, "maxColumnsPerObject": { "type": "integer", "minimum": 1, "maximum": 256 }, "maxIndexesPerObject": { "type": "integer", "minimum": 0, "maximum": 128 }, "maxForeignKeys": { "type": "integer", "minimum": 0, "maximum": 128 }, "maxCardinalityReads": { "type": "integer", "minimum": 0, "maximum": 512 }, "maxSampleRows": { "type": "integer", "minimum": 0, "maximum": 3 }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 10000 }, "maxResultBytes": { "type": "integer", "minimum": 1, "maximum": 1048576 }
|
||||
}
|
||||
},
|
||||
"scumLiveDataCapabilityGate": {
|
||||
"type": "object",
|
||||
"required": ["capability", "gate", "adapterVersion", "evidenceStatus", "safeReason"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capability": { "enum": ["schema-probe", "players.read", "player-details.read", "squads.read", "squad-members.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"] },
|
||||
"gate": { "enum": ["disabled", "enabled"] },
|
||||
"adapterVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
|
||||
"requiredSchemaFingerprint": { "type": "string", "pattern": "^(sha256:)?[a-fA-F0-9]{16,128}$" },
|
||||
"requiredAssetDigests": { "type": "array", "items": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" }, "uniqueItems": true, "maxItems": 16 },
|
||||
"evidenceStatus": { "enum": ["missing", "compatible", "incompatible", "failed"] },
|
||||
"safeReason": { "type": "string", "minLength": 1, "maxLength": 240 }
|
||||
}
|
||||
},
|
||||
"gameClientBridgeCompanion": {
|
||||
"type": "object",
|
||||
"required": ["profileKey", "configTemplateKey", "configSchemaRef", "configFormat", "platformBaseUrlSource", "registrationProof", "proofMaterialSource", "proofMaterialEnv", "sessionMode", "tlsPolicy", "heartbeatIntervalSeconds", "commandPollIntervalSeconds", "requestTimeoutSeconds"],
|
||||
@@ -463,6 +506,7 @@
|
||||
"remote.run.process.start",
|
||||
"remote.run.process.stop",
|
||||
"remote.run.db.mysql.query",
|
||||
"remote.run.db.sqlite.probe",
|
||||
"remote.run.db.sqlite.query",
|
||||
"remote.run.logs.transfer",
|
||||
"remote.run.rcon.command",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://browser.local/schemas/scum-live-data.schema.json",
|
||||
"title": "SCUMLiveDataManifestDeclaration",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "probe", "capabilityGates"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": { "const": "1" },
|
||||
"probe": {
|
||||
"type": "object",
|
||||
"required": ["capability", "targetKey", "bounds"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capability": { "const": "remote.run.db.sqlite.probe" },
|
||||
"targetKey": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$", "maxLength": 120 },
|
||||
"bounds": { "$ref": "#/$defs/probeBounds" }
|
||||
}
|
||||
},
|
||||
"capabilityGates": { "type": "array", "items": { "$ref": "#/$defs/capabilityGate" }, "minItems": 1, "maxItems": 32 }
|
||||
},
|
||||
"$defs": {
|
||||
"probeBounds": {
|
||||
"type": "object",
|
||||
"required": ["maxObjects", "maxColumnsPerObject", "maxIndexesPerObject", "maxForeignKeys", "maxCardinalityReads", "maxSampleRows", "timeoutMs", "maxResultBytes"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"maxObjects": { "type": "integer", "minimum": 1, "maximum": 512 }, "maxColumnsPerObject": { "type": "integer", "minimum": 1, "maximum": 256 }, "maxIndexesPerObject": { "type": "integer", "minimum": 0, "maximum": 128 }, "maxForeignKeys": { "type": "integer", "minimum": 0, "maximum": 128 }, "maxCardinalityReads": { "type": "integer", "minimum": 0, "maximum": 512 }, "maxSampleRows": { "type": "integer", "minimum": 0, "maximum": 3 }, "timeoutMs": { "type": "integer", "minimum": 1, "maximum": 10000 }, "maxResultBytes": { "type": "integer", "minimum": 1, "maximum": 1048576 }
|
||||
}
|
||||
},
|
||||
"capabilityGate": {
|
||||
"type": "object",
|
||||
"required": ["capability", "gate", "adapterVersion", "evidenceStatus", "safeReason"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capability": { "enum": ["schema-probe", "players.read", "player-details.read", "squads.read", "squad-members.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"] },
|
||||
"gate": { "enum": ["disabled", "enabled"] },
|
||||
"adapterVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
|
||||
"requiredSchemaFingerprint": { "type": "string", "pattern": "^(sha256:)?[a-fA-F0-9]{16,128}$" },
|
||||
"requiredAssetDigests": { "type": "array", "items": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" }, "uniqueItems": true, "maxItems": 16 },
|
||||
"evidenceStatus": { "enum": ["missing", "compatible", "incompatible", "failed"] },
|
||||
"safeReason": { "type": "string", "minLength": 1, "maxLength": 240 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1054,6 +1054,53 @@ export function validateRuntimeLogEventCatalog(manifest: unknown): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateSCUMLiveDataManifest(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) return [];
|
||||
type Gate = { capability?: string; gate?: string; adapterVersion?: string; requiredSchemaFingerprint?: string; requiredAssetDigests?: string[]; evidenceStatus?: string; safeReason?: string };
|
||||
type Probe = { capability?: string; targetKey?: string; bounds?: { maxSampleRows?: number; timeoutMs?: number; maxResultBytes?: number } };
|
||||
type Transport = { key?: string; kind?: string; targetKey?: string; capabilities?: string[] };
|
||||
const declaration = manifest as { id?: string; capabilities?: string[]; remoteAccess?: { runCapabilities?: string[] }; runtimeProfiles?: { transportProfiles?: Transport[] }; scumLiveData?: { schemaVersion?: string; probe?: Probe; capabilityGates?: Gate[] } };
|
||||
const liveData = declaration.scumLiveData;
|
||||
if (!liveData) return [];
|
||||
|
||||
const errors: string[] = [];
|
||||
const declaredCapabilities = new Set(declaration.capabilities ?? []);
|
||||
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
|
||||
const probe = liveData.probe;
|
||||
const location = "manifest.scumLiveData";
|
||||
if (liveData.schemaVersion !== "1") errors.push(`${location}.schemaVersion: must be 1`);
|
||||
if (probe?.capability !== "remote.run.db.sqlite.probe") errors.push(`${location}.probe.capability: must be remote.run.db.sqlite.probe`);
|
||||
if (!declaredCapabilities.has("remote.run.db.sqlite.probe") || !remoteCapabilities.has("remote.run.db.sqlite.probe")) errors.push(`${location}.probe: plugin and remoteAccess must declare remote.run.db.sqlite.probe`);
|
||||
const transport = (declaration.runtimeProfiles?.transportProfiles ?? []).find((candidate) => candidate.targetKey === probe?.targetKey || candidate.key === probe?.targetKey);
|
||||
if (!transport) {
|
||||
errors.push(`${location}.probe.targetKey: must reference a declared runtime transport profile or target`);
|
||||
} else {
|
||||
if (transport.kind !== "sqlite") errors.push(`${location}.probe.targetKey: schema probe requires a sqlite transport profile`);
|
||||
if (!transport.capabilities?.includes("remote.run.db.sqlite.probe")) errors.push(`${location}.probe.targetKey: sqlite transport must declare remote.run.db.sqlite.probe`);
|
||||
}
|
||||
if ((probe?.bounds?.maxSampleRows ?? 0) > 3) errors.push(`${location}.probe.bounds.maxSampleRows: redacted samples are limited to 3`);
|
||||
if ((probe?.bounds?.timeoutMs ?? 0) > 10000) errors.push(`${location}.probe.bounds.timeoutMs: must be bounded to 10 seconds or less`);
|
||||
if ((probe?.bounds?.maxResultBytes ?? 0) > 1048576) errors.push(`${location}.probe.bounds.maxResultBytes: must be bounded to 1 MiB or less`);
|
||||
|
||||
const gates = liveData.capabilityGates ?? [];
|
||||
const seen = new Set<string>();
|
||||
for (const [index, gate] of gates.entries()) {
|
||||
const gateLocation = `${location}.capabilityGates[${index}]`;
|
||||
const capability = gate.capability ?? "";
|
||||
if (seen.has(capability)) errors.push(`${gateLocation}.capability: duplicate gate ${capability}`);
|
||||
seen.add(capability);
|
||||
const reasonErrors = unsafeStringReasons(gate.safeReason ?? "");
|
||||
errors.push(...reasonErrors.map((reason) => `${gateLocation}.safeReason: ${reason}`));
|
||||
if (gate.gate === "enabled") {
|
||||
if (gate.evidenceStatus !== "compatible") errors.push(`${gateLocation}.evidenceStatus: enabled gates require compatible evidence`);
|
||||
if (!/^(sha256:)?[a-fA-F0-9]{16,128}$/.test(gate.requiredSchemaFingerprint ?? "")) errors.push(`${gateLocation}.requiredSchemaFingerprint: enabled gates require a schema fingerprint`);
|
||||
if (!Array.isArray(gate.requiredAssetDigests) || gate.requiredAssetDigests.length === 0) errors.push(`${gateLocation}.requiredAssetDigests: enabled gates require immutable asset digests`);
|
||||
}
|
||||
if (gate.gate === "disabled" && gate.evidenceStatus === "compatible") errors.push(`${gateLocation}.evidenceStatus: disabled gates must not claim compatible evidence`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
type GameClientBridgeSchemaReference = {
|
||||
location: string;
|
||||
ref: string;
|
||||
@@ -1372,6 +1419,7 @@ export function validateManifestFile(manifestPath: string): string[] {
|
||||
errors.push(...validateClientManagerProfiles(manifest));
|
||||
errors.push(...validateDLLExtensionProfiles(manifest));
|
||||
errors.push(...validateGameClientBridgeCatalog(manifest));
|
||||
errors.push(...validateSCUMLiveDataManifest(manifest));
|
||||
errors.push(...validateGameClientBridgeSchemaFiles(manifest, manifestDir));
|
||||
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
|
||||
errors.push(...validateRuntimeLogEventCatalog(manifest));
|
||||
|
||||
@@ -43,6 +43,7 @@ export type RunCapability =
|
||||
| "remote.run.process.start"
|
||||
| "remote.run.process.stop"
|
||||
| "remote.run.db.mysql.query"
|
||||
| "remote.run.db.sqlite.probe"
|
||||
| "remote.run.db.sqlite.query"
|
||||
| "remote.run.logs.transfer"
|
||||
| "remote.run.rcon.command"
|
||||
@@ -268,6 +269,55 @@ export interface GameClientBridgeQueryTemplateDeclaration {
|
||||
|
||||
export type GameClientBridgeOperationKind = "rcon" | "sqlite-mutation";
|
||||
|
||||
export type SCUMLiveDataCapability =
|
||||
| "schema-probe"
|
||||
| "players.read"
|
||||
| "player-details.read"
|
||||
| "squads.read"
|
||||
| "squad-members.read"
|
||||
| "vehicles.read"
|
||||
| "flags.read"
|
||||
| "positions.read"
|
||||
| "profile-xml.write"
|
||||
| "economy-command.write"
|
||||
| "gift-command.write";
|
||||
|
||||
export type SCUMLiveDataGateState = "disabled" | "enabled";
|
||||
export type SCUMLiveDataEvidenceStatus = "missing" | "compatible" | "incompatible" | "failed";
|
||||
|
||||
export interface SCUMLiveDataCapabilityGateDeclaration {
|
||||
capability: SCUMLiveDataCapability;
|
||||
gate: SCUMLiveDataGateState;
|
||||
adapterVersion: string;
|
||||
requiredSchemaFingerprint?: string;
|
||||
requiredAssetDigests?: `sha256:${string}`[];
|
||||
evidenceStatus: SCUMLiveDataEvidenceStatus;
|
||||
safeReason: string;
|
||||
}
|
||||
|
||||
export interface SCUMSchemaProbeBoundsDeclaration {
|
||||
maxObjects: number;
|
||||
maxColumnsPerObject: number;
|
||||
maxIndexesPerObject: number;
|
||||
maxForeignKeys: number;
|
||||
maxCardinalityReads: number;
|
||||
maxSampleRows: number;
|
||||
timeoutMs: number;
|
||||
maxResultBytes: number;
|
||||
}
|
||||
|
||||
export interface SCUMSchemaProbeDeclaration {
|
||||
capability: Extract<RunCapability, "remote.run.db.sqlite.probe">;
|
||||
targetKey: string;
|
||||
bounds: SCUMSchemaProbeBoundsDeclaration;
|
||||
}
|
||||
|
||||
export interface SCUMLiveDataManifestDeclaration {
|
||||
schemaVersion: "1";
|
||||
probe: SCUMSchemaProbeDeclaration;
|
||||
capabilityGates: SCUMLiveDataCapabilityGateDeclaration[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeOperationSafety {
|
||||
requiresApproval?: boolean;
|
||||
requiresOfflinePlayer?: boolean;
|
||||
@@ -722,6 +772,7 @@ export interface GamePluginManifest {
|
||||
remoteAccess?: GamePluginRemoteAccess;
|
||||
runtimeProfiles?: GamePluginRuntimeProfiles;
|
||||
gameClientBridge?: GameClientBridgeManifest;
|
||||
scumLiveData?: SCUMLiveDataManifestDeclaration;
|
||||
actions?: GamePluginActions;
|
||||
assetFiles?: PluginAssetFile[];
|
||||
productionLifecycle: {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type GameClientBridgeProtectedRequestDeclaration,
|
||||
type GameClientBridgeCompanionDeclaration,
|
||||
type GamePluginManifest,
|
||||
type SCUMLiveDataManifestDeclaration,
|
||||
type RuntimeLogEventDeclaration,
|
||||
type RuntimeClientManagerProfile,
|
||||
type PluginLifecycleActionDeclaration,
|
||||
@@ -202,6 +203,25 @@ describe("plugin manifest validation", () => {
|
||||
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps SCUM database-backed live-data gates disabled until current-service evidence exists", () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration; remoteAccess: { runCapabilities: string[] } };
|
||||
expect(manifest.capabilities).toContain("remote.run.db.sqlite.probe");
|
||||
expect(manifest.remoteAccess.runCapabilities).toContain("remote.run.db.sqlite.probe");
|
||||
expect(manifest.scumLiveData.probe).toMatchObject({ capability: "remote.run.db.sqlite.probe", targetKey: "scum-database" });
|
||||
expect(manifest.scumLiveData.capabilityGates.map((gate) => gate.capability)).toEqual(expect.arrayContaining(["players.read", "squads.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"]));
|
||||
expect(manifest.scumLiveData.capabilityGates.every((gate) => gate.gate === "disabled" && gate.evidenceStatus === "missing")).toBe(true);
|
||||
expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/);
|
||||
});
|
||||
|
||||
it("rejects enabling SCUM live-data gates without compatible evidence and immutable digests", () => {
|
||||
const errors = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
manifest.scumLiveData.capabilityGates[1] = { ...manifest.scumLiveData.capabilityGates[1], gate: "enabled", evidenceStatus: "missing" };
|
||||
});
|
||||
expect(errors.some((error) => error.includes("enabled gates require compatible evidence"))).toBe(true);
|
||||
expect(errors.some((error) => error.includes("enabled gates require a schema fingerprint"))).toBe(true);
|
||||
expect(errors.some((error) => error.includes("enabled gates require immutable asset digests"))).toBe(true);
|
||||
});
|
||||
|
||||
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as any;
|
||||
|
||||
Reference in New Issue
Block a user