Fix server create plugin filtering
This commit is contained in:
@@ -48,6 +48,70 @@ afterEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("ServerDeploymentWorkflow", () => {
|
describe("ServerDeploymentWorkflow", () => {
|
||||||
|
it("merges plugin defaults into guided edit inputs", async () => {
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.append(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
const scumPlugin: GamePluginResponse = { ...plugin, id: "game.scum", name: "SCUM Server", serverType: "scum", serverDisplayName: "SCUM Dedicated Server", createFields: [{ key: "gamePort", label: "游戏端口", type: "port", required: true, defaultValue: "7779" }, { key: "queryPort", label: "查询端口", type: "port", required: true, defaultValue: "27015" }] };
|
||||||
|
const initialForm = { ...defaultServerCreateForm([scumPlugin]), name: "Moon Base", createInputs: { gamePort: "28000" } };
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root?.render(
|
||||||
|
<ServerDeploymentWorkflow
|
||||||
|
open
|
||||||
|
kind="edit"
|
||||||
|
plugins={[scumPlugin]}
|
||||||
|
initialForm={initialForm}
|
||||||
|
deployment={{ serverInstanceId: "server-scum", mode: "guided-install", createInputs: { gamePort: "28000" }, serverRootConfigured: false, workingDirectoryConfigured: false, installCommandConfigured: false, startCommandConfigured: false, stopCommandConfigured: false, statusCommandConfigured: false, revision: 1 }}
|
||||||
|
onClose={() => undefined}
|
||||||
|
onSubmit={async () => undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.textContent).toContain("游戏端口");
|
||||||
|
expect(container.textContent).toContain("查询端口");
|
||||||
|
expect(Array.from(container.querySelectorAll<HTMLInputElement>('input[type="number"]')).map((input) => input.value)).toEqual(["28000", "27015"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes the create wizard to canonical plugins with visible guided inputs", async () => {
|
||||||
|
container = document.createElement("div");
|
||||||
|
document.body.append(container);
|
||||||
|
root = createRoot(container);
|
||||||
|
const examplePlugin: GamePluginResponse = { ...plugin, id: "game.example", name: "Example Server", serverType: "example", serverDisplayName: "Example Server", createFields: [], tags: ["development"] };
|
||||||
|
const staleScumPlugin: GamePluginResponse = { ...plugin, id: "game.scum.codex.20260804095301", name: "SCUM Server", version: "0.1.4", serverType: "scum", serverDisplayName: "SCUM Dedicated Server", manifestRef: "plugins/examples/scum-server-plugin/game.scum.codex.20260804095301/manifest.json", tags: ["scum"], createFields: [{ key: "gamePort", label: "游戏端口", type: "port", required: true, defaultValue: "27000" }] };
|
||||||
|
const scumPlugin: GamePluginResponse = { ...staleScumPlugin, id: "game.scum", version: "0.1.15", manifestRef: "artifact://manifests/game.scum/0.1.15", createFields: [{ key: "gamePort", label: "游戏端口", type: "port", required: true, defaultValue: "7779" }, { key: "queryPort", label: "查询端口", type: "port", required: true, defaultValue: "27015" }] };
|
||||||
|
const initialForm = defaultServerCreateForm([examplePlugin, staleScumPlugin, scumPlugin]);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root?.render(
|
||||||
|
<ServerDeploymentWorkflow
|
||||||
|
open
|
||||||
|
kind="create"
|
||||||
|
plugins={[examplePlugin, staleScumPlugin, scumPlugin]}
|
||||||
|
initialForm={initialForm}
|
||||||
|
onClose={() => undefined}
|
||||||
|
onSubmit={async () => undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pluginSelect = container.querySelector<HTMLSelectElement>('select[name="pluginId"]');
|
||||||
|
expect(pluginSelect?.value).toBe("game.scum");
|
||||||
|
expect(Array.from(pluginSelect?.options ?? []).map((option) => option.value)).toEqual(["game.scum", "game.example"]);
|
||||||
|
|
||||||
|
const nameInput = container.querySelector<HTMLInputElement>('input[name="name"]');
|
||||||
|
if (!nameInput) throw new Error("server name input not found");
|
||||||
|
await act(async () => {
|
||||||
|
setInputValue(nameInput, "Moon Base");
|
||||||
|
});
|
||||||
|
|
||||||
|
await submitWorkflow(container);
|
||||||
|
await submitWorkflow(container);
|
||||||
|
expect(container.querySelector<HTMLInputElement>('input[value="7779"]')).not.toBeNull();
|
||||||
|
expect(container.querySelector<HTMLInputElement>('input[value="27015"]')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("submits deployment mode and matching startup inputs from the create wizard", async () => {
|
it("submits deployment mode and matching startup inputs from the create wizard", async () => {
|
||||||
container = document.createElement("div");
|
container = document.createElement("div");
|
||||||
document.body.append(container);
|
document.body.append(container);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from "
|
|||||||
|
|
||||||
import type { GamePluginResponse, ServerDeploymentResponse, ServerDeploymentRevealResponse } from "../api/types";
|
import type { GamePluginResponse, ServerDeploymentResponse, ServerDeploymentRevealResponse } from "../api/types";
|
||||||
import { ManagementDialog } from "./OperationControls";
|
import { ManagementDialog } from "./OperationControls";
|
||||||
import { pluginCreateInputDefaults, pluginLabel, type ServerCreateFormState } from "../contracts/serverManagement";
|
import { pluginCreateInputDefaults, pluginCreateInputValues, pluginLabel, serverCreatePluginOptions, type ServerCreateFormState } from "../contracts/serverManagement";
|
||||||
import { cx } from "../utils/classes";
|
import { cx } from "../utils/classes";
|
||||||
|
|
||||||
type WorkflowKind = "create" | "edit";
|
type WorkflowKind = "create" | "edit";
|
||||||
@@ -25,7 +25,8 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
const [form, setForm] = useState<ServerCreateFormState>(initialForm);
|
const [form, setForm] = useState<ServerCreateFormState>(initialForm);
|
||||||
const [revealBusy, setRevealBusy] = useState(false);
|
const [revealBusy, setRevealBusy] = useState(false);
|
||||||
const [revealError, setRevealError] = useState("");
|
const [revealError, setRevealError] = useState("");
|
||||||
const selectedPlugin = useMemo(() => plugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, plugins]);
|
const selectablePlugins = useMemo(() => kind === "create" ? serverCreatePluginOptions(plugins) : plugins, [kind, plugins]);
|
||||||
|
const selectedPlugin = useMemo(() => selectablePlugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, selectablePlugins]);
|
||||||
const pluginFields = selectedPlugin?.createFields ?? [];
|
const pluginFields = selectedPlugin?.createFields ?? [];
|
||||||
const workflowSteps = kind === "create"
|
const workflowSteps = kind === "create"
|
||||||
? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
|
? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
|
||||||
@@ -38,10 +39,10 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
setStep(0);
|
setStep(0);
|
||||||
setForm(initialForm);
|
setForm(normalizeWorkflowForm(initialForm, selectablePlugins, kind === "create"));
|
||||||
setRevealBusy(false);
|
setRevealBusy(false);
|
||||||
setRevealError("");
|
setRevealError("");
|
||||||
}, [initialForm, kind, open]);
|
}, [initialForm, kind, open, selectablePlugins]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || kind !== "edit" || !onReveal) return;
|
if (!open || kind !== "edit" || !onReveal) return;
|
||||||
@@ -52,7 +53,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
const { name, value } = event.target;
|
const { name, value } = event.target;
|
||||||
setForm((current) => {
|
setForm((current) => {
|
||||||
if (name === "pluginId") {
|
if (name === "pluginId") {
|
||||||
const plugin = plugins.find((item) => item.id === value);
|
const plugin = selectablePlugins.find((item) => item.id === value);
|
||||||
return { ...current, pluginId: value, createInputs: pluginCreateInputDefaults(plugin) };
|
return { ...current, pluginId: value, createInputs: pluginCreateInputDefaults(plugin) };
|
||||||
}
|
}
|
||||||
return { ...current, [name]: value };
|
return { ...current, [name]: value };
|
||||||
@@ -107,7 +108,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
<ol className="deployment-workflow-steps" style={{ gridTemplateColumns: `repeat(${workflowSteps.length}, minmax(0, 1fr))` }} aria-label="部署步骤">{workflowSteps.map((item, index) => { const Icon = item.icon; return <li key={item.label} className={cx(index === step && "deployment-workflow-step-active", index < step && "deployment-workflow-step-complete")}><span>{index < step ? <CheckCircle2 size={15} /> : <Icon size={15} />}</span><strong>{index + 1}. {item.label}</strong></li>; })}</ol>
|
<ol className="deployment-workflow-steps" style={{ gridTemplateColumns: `repeat(${workflowSteps.length}, minmax(0, 1fr))` }} aria-label="部署步骤">{workflowSteps.map((item, index) => { const Icon = item.icon; return <li key={item.label} className={cx(index === step && "deployment-workflow-step-active", index < step && "deployment-workflow-step-complete")}><span>{index < step ? <CheckCircle2 size={15} /> : <Icon size={15} />}</span><strong>{index + 1}. {item.label}</strong></li>; })}</ol>
|
||||||
{step === pluginStep && <div className="deployment-workflow-body">
|
{step === pluginStep && <div className="deployment-workflow-body">
|
||||||
<div className="workflow-hint-grid"><div className="workflow-hint-card"><strong>创建基础信息</strong><span>插件决定下一步显示哪些部署方式和游戏参数。</span></div><div className="workflow-hint-card"><strong>配置启动项</strong><span>新建安装、接管已有和自定义启动分别填写自己的字段。</span></div><div className="workflow-hint-card"><strong>平台构建 Run 包</strong><span>平台在自有构建器中打包,Run 启动后自动上报心跳。</span></div></div>
|
<div className="workflow-hint-grid"><div className="workflow-hint-card"><strong>创建基础信息</strong><span>插件决定下一步显示哪些部署方式和游戏参数。</span></div><div className="workflow-hint-card"><strong>配置启动项</strong><span>新建安装、接管已有和自定义启动分别填写自己的字段。</span></div><div className="workflow-hint-card"><strong>平台构建 Run 包</strong><span>平台在自有构建器中打包,Run 启动后自动上报心跳。</span></div></div>
|
||||||
<div className="form-grid"><label>插件类型<select name="pluginId" value={form.pluginId} onChange={updateForm} required>{plugins.map((plugin) => <option key={plugin.id} value={plugin.id}>{pluginLabel(plugin, plugin.id)}</option>)}</select></label><label>服务器名称<input name="name" value={form.name} onChange={updateForm} placeholder="Example Survival #3" required /></label></div>
|
<div className="form-grid"><label>插件类型<select name="pluginId" value={form.pluginId} onChange={updateForm} required>{selectablePlugins.length === 0 && <option value="">暂无可创建插件</option>}{selectablePlugins.map((plugin) => <option key={plugin.id} value={plugin.id}>{pluginLabel(plugin, plugin.id)}</option>)}</select></label><label>服务器名称<input name="name" value={form.name} onChange={updateForm} placeholder="Example Survival #3" required /></label></div>
|
||||||
</div>}
|
</div>}
|
||||||
{step === modeStep && <div className="deployment-workflow-body"><p className="section-copy">选择这台服务器的创建方式;下一步只显示该方式需要的启动项。</p><div className="form-guidance"><strong>插件生命周期链路</strong><span>Run 会按插件声明执行预检、安装或扫描、配置映射与健康验证;目录本身不代表安装完成。</span></div><div className="deployment-mode-grid">
|
{step === modeStep && <div className="deployment-workflow-body"><p className="section-copy">选择这台服务器的创建方式;下一步只显示该方式需要的启动项。</p><div className="form-guidance"><strong>插件生命周期链路</strong><span>Run 会按插件声明执行预检、安装或扫描、配置映射与健康验证;目录本身不代表安装完成。</span></div><div className="deployment-mode-grid">
|
||||||
<ModeOption active={form.deploymentMode === "guided-install"} title="新建并安装" copy="按插件的推荐方案安装并写入游戏配置。适合绝大多数新服务器。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "guided-install" }))} />
|
<ModeOption active={form.deploymentMode === "guided-install"} title="新建并安装" copy="按插件的推荐方案安装并写入游戏配置。适合绝大多数新服务器。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "guided-install" }))} />
|
||||||
@@ -127,6 +128,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
)}</label>
|
)}</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{form.deploymentMode === "guided-install" && pluginFields.length === 0 && <div className="form-guidance"><strong>当前插件没有声明可填写的创建项</strong><span>可以返回选择其他插件,或改用自定义启动方式填写命令、目录和启动参数。</span></div>}
|
||||||
{form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
|
{form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
|
||||||
{form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
|
{form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
|
||||||
{form.deploymentMode === "custom-command" && <details className="provider-advanced-settings" open><summary>高级启动设置</summary><p className="field-help">只有自定义启动器需要这些设置。执行目录留空时,Run 以服务器目录执行。</p><div className="form-grid"><label>启动命令<input name="startCommand" value={form.startCommand} onChange={updateForm} placeholder={deployment?.startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server"} autoComplete="off" required={!deployment?.startCommandConfigured} /></label><label>命令解释器<select name="shell" value={form.shell} onChange={updateForm}><option value="">直接 argv(推荐)</option><option value="posix-sh">POSIX sh</option><option value="powershell">PowerShell</option><option value="cmd">Windows cmd</option></select></label><label>执行目录(可选)<input name="workingDirectory" value={form.workingDirectory} onChange={updateForm} placeholder={deployment?.workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录"} autoComplete="off" /></label><label>安装命令(可选)<input name="installCommand" value={form.installCommand} onChange={updateForm} autoComplete="off" placeholder="留空保持原值或不使用" /></label><label>停止命令(可选)<input name="stopCommand" value={form.stopCommand} onChange={updateForm} autoComplete="off" /></label><label>状态命令<input name="statusCommand" value={form.statusCommand} onChange={updateForm} autoComplete="off" /></label></div></details>}
|
{form.deploymentMode === "custom-command" && <details className="provider-advanced-settings" open><summary>高级启动设置</summary><p className="field-help">只有自定义启动器需要这些设置。执行目录留空时,Run 以服务器目录执行。</p><div className="form-grid"><label>启动命令<input name="startCommand" value={form.startCommand} onChange={updateForm} placeholder={deployment?.startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server"} autoComplete="off" required={!deployment?.startCommandConfigured} /></label><label>命令解释器<select name="shell" value={form.shell} onChange={updateForm}><option value="">直接 argv(推荐)</option><option value="posix-sh">POSIX sh</option><option value="powershell">PowerShell</option><option value="cmd">Windows cmd</option></select></label><label>执行目录(可选)<input name="workingDirectory" value={form.workingDirectory} onChange={updateForm} placeholder={deployment?.workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录"} autoComplete="off" /></label><label>安装命令(可选)<input name="installCommand" value={form.installCommand} onChange={updateForm} autoComplete="off" placeholder="留空保持原值或不使用" /></label><label>停止命令(可选)<input name="stopCommand" value={form.stopCommand} onChange={updateForm} autoComplete="off" /></label><label>状态命令<input name="statusCommand" value={form.statusCommand} onChange={updateForm} autoComplete="off" /></label></div></details>}
|
||||||
@@ -137,6 +139,11 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
</ManagementDialog>;
|
</ManagementDialog>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeWorkflowForm(form: ServerCreateFormState, plugins: GamePluginResponse[], preferFirstPlugin = false): ServerCreateFormState {
|
||||||
|
const plugin = preferFirstPlugin ? plugins[0] : plugins.find((item) => item.id === form.pluginId) ?? plugins[0];
|
||||||
|
return { ...form, pluginId: plugin?.id ?? "", createInputs: pluginCreateInputValues(plugin, plugin?.id === form.pluginId ? form.createInputs : undefined) };
|
||||||
|
}
|
||||||
|
|
||||||
function ModeOption({ active, title, copy, onClick }: { active: boolean; title: string; copy: string; onClick: () => void }) { return <button type="button" className={cx("deployment-mode-option", active && "deployment-mode-option-active")} onClick={onClick}><strong>{title}</strong><span>{copy}</span></button>; }
|
function ModeOption({ active, title, copy, onClick }: { active: boolean; title: string; copy: string; onClick: () => void }) { return <button type="button" className={cx("deployment-mode-option", active && "deployment-mode-option-active")} onClick={onClick}><strong>{title}</strong><span>{copy}</span></button>; }
|
||||||
|
|
||||||
function GuidedInstallPlan({ pluginName }: { pluginName: string }) {
|
function GuidedInstallPlan({ pluginName }: { pluginName: string }) {
|
||||||
|
|||||||
@@ -94,6 +94,24 @@ export function pluginLabel(plugin: GamePluginResponse | undefined, pluginId: st
|
|||||||
return plugin.serverDisplayName || plugin.name || plugin.id;
|
return plugin.serverDisplayName || plugin.name || plugin.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function serverCreatePluginOptions(plugins: GamePluginResponse[]): GamePluginResponse[] {
|
||||||
|
const selected = new Map<string, GamePluginResponse>();
|
||||||
|
for (const plugin of plugins.filter(pluginCanCreateServer)) {
|
||||||
|
const key = serverCreatePluginIdentity(plugin);
|
||||||
|
const current = selected.get(key);
|
||||||
|
if (!current || compareServerCreatePlugins(plugin, current) < 0) selected.set(key, plugin);
|
||||||
|
}
|
||||||
|
return Array.from(selected.values()).sort(compareServerCreatePlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pluginCanCreateServer(plugin: GamePluginResponse): boolean {
|
||||||
|
return plugin.status === "installed" && plugin.declaredPermissions.includes("server.create");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pluginCreateInputValues(plugin: GamePluginResponse | undefined, values: Record<string, string> | undefined): Record<string, string> {
|
||||||
|
return { ...pluginCreateInputDefaults(plugin), ...(values ?? {}) };
|
||||||
|
}
|
||||||
|
|
||||||
export function canStartServer(state: ServerInstanceState): boolean {
|
export function canStartServer(state: ServerInstanceState): boolean {
|
||||||
return state === "ready" || state === "stopped" || state === "failed";
|
return state === "ready" || state === "stopped" || state === "failed";
|
||||||
}
|
}
|
||||||
@@ -119,6 +137,35 @@ export function pluginCreateInputDefaults(plugin: GamePluginResponse | undefined
|
|||||||
return Object.fromEntries((plugin?.createFields ?? []).map((field) => [field.key, field.defaultValue ?? ""]));
|
return Object.fromEntries((plugin?.createFields ?? []).map((field) => [field.key, field.defaultValue ?? ""]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serverCreatePluginIdentity(plugin: GamePluginResponse): string {
|
||||||
|
return `${plugin.serverType}:${plugin.serverDisplayName || plugin.name || plugin.id}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareServerCreatePlugins(left: GamePluginResponse, right: GamePluginResponse): number {
|
||||||
|
const leftScore = serverCreatePluginScore(left);
|
||||||
|
const rightScore = serverCreatePluginScore(right);
|
||||||
|
for (let index = 0; index < leftScore.length; index += 1) {
|
||||||
|
const delta = leftScore[index]-rightScore[index];
|
||||||
|
if (delta !== 0) return delta;
|
||||||
|
}
|
||||||
|
const labelDelta = pluginLabel(left, left.id).localeCompare(pluginLabel(right, right.id));
|
||||||
|
return labelDelta || left.id.localeCompare(right.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverCreatePluginScore(plugin: GamePluginResponse): number[] {
|
||||||
|
return [
|
||||||
|
(plugin.createFields?.length ?? 0) > 0 ? 0 : 1,
|
||||||
|
plugin.tags.includes("development") ? 1 : 0,
|
||||||
|
plugin.id === `game.${plugin.serverType}` ? 0 : 1,
|
||||||
|
plugin.id.includes(".codex.") ? 1 : 0,
|
||||||
|
-versionWeight(plugin.version)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function versionWeight(version: string): number {
|
||||||
|
return version.split(/[^0-9]+/).filter(Boolean).slice(0, 3).reduce((weight, part, index) => weight+Number(part || 0)*Math.pow(1000, 2-index), 0);
|
||||||
|
}
|
||||||
|
|
||||||
export function serverMetadataFormFromInstance(instance: ServerInstanceResponse): ServerMetadataFormState {
|
export function serverMetadataFormFromInstance(instance: ServerInstanceResponse): ServerMetadataFormState {
|
||||||
return { name: instance.name };
|
return { name: instance.name };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import type { GamePluginResponse, ServerInstanceResponse } from "../api/types";
|
import type { GamePluginResponse, ServerInstanceResponse } from "../api/types";
|
||||||
import { defaultServerCreateForm } from "../contracts/serverManagement";
|
import { defaultServerCreateForm, pluginCreateInputValues, serverCreatePluginOptions } from "../contracts/serverManagement";
|
||||||
import { minimalServerCreateRequestFromForm, serverCreateRequestFromForm, serverInstanceIdFromName, serverLifecycleCommandRequest } from "./serverManagement";
|
import { minimalServerCreateRequestFromForm, serverCreateRequestFromForm, serverInstanceIdFromName, serverLifecycleCommandRequest } from "./serverManagement";
|
||||||
|
|
||||||
const plugin: GamePluginResponse = {
|
const plugin: GamePluginResponse = {
|
||||||
@@ -112,4 +112,18 @@ describe("runtime profile server creation contracts", () => {
|
|||||||
expect("profileKey" in (request.deployment ?? {})).toBe(false);
|
expect("profileKey" in (request.deployment ?? {})).toBe(false);
|
||||||
expect("runtimeBindings" in (request.deployment ?? {})).toBe(false);
|
expect("runtimeBindings" in (request.deployment ?? {})).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the create picker focused on canonical create-capable plugins", () => {
|
||||||
|
const examplePlugin = { ...plugin, id: "game.example", name: "Example Server", serverType: "example", serverDisplayName: "Example Server", createFields: [], tags: ["development"] };
|
||||||
|
const staleScumPlugin = { ...plugin, id: "game.scum.codex.20260804095301", name: "SCUM Server", version: "0.1.4", serverType: "scum", serverDisplayName: "SCUM Dedicated Server", manifestRef: "plugins/examples/scum-server-plugin/game.scum.codex.20260804095301/manifest.json", tags: ["scum"] };
|
||||||
|
const scumPlugin = { ...staleScumPlugin, id: "game.scum", version: "0.1.15", manifestRef: "artifact://manifests/game.scum/0.1.15", createFields: [{ key: "gamePort", label: "游戏端口", type: "port" as const, required: true, defaultValue: "7779" }] };
|
||||||
|
|
||||||
|
const options = serverCreatePluginOptions([examplePlugin, staleScumPlugin, scumPlugin]);
|
||||||
|
expect(options.map((item) => item.id)).toEqual(["game.scum", "game.example"]);
|
||||||
|
expect(defaultServerCreateForm(options)).toMatchObject({ pluginId: "game.scum", createInputs: { gamePort: "7779" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges saved create inputs over plugin defaults", () => {
|
||||||
|
expect(pluginCreateInputValues(plugin, { maxPlayers: "96" })).toEqual({ gamePort: "7777", maxPlayers: "96" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -460,6 +460,19 @@ if (!logSources.some((source) => source.kind === "process.stdout" && source.stre
|
|||||||
if (!logSources.some((source) => source.kind === "process.stderr" && source.streamKey === "scum.console.stderr")) {
|
if (!logSources.some((source) => source.kind === "process.stderr" && source.streamKey === "scum.console.stderr")) {
|
||||||
missing.push("SCUM process stderr log source");
|
missing.push("SCUM process stderr log source");
|
||||||
}
|
}
|
||||||
|
const createFields = plugin.createFields ?? [];
|
||||||
|
if (!createFields.some((field) => field.key === "serverName")) {
|
||||||
|
missing.push("SCUM server name create field");
|
||||||
|
}
|
||||||
|
if (!createFields.some((field) => field.key === "gamePort" && field.defaultValue === "7779")) {
|
||||||
|
missing.push("SCUM game port create field");
|
||||||
|
}
|
||||||
|
if (!createFields.some((field) => field.key === "queryPort" && field.defaultValue === "27015")) {
|
||||||
|
missing.push("SCUM query port create field");
|
||||||
|
}
|
||||||
|
if (!createFields.some((field) => field.key === "maxPlayers" && field.defaultValue === "128")) {
|
||||||
|
missing.push("SCUM max players create field");
|
||||||
|
}
|
||||||
if (!plugin.gameClientBridge?.commands?.length) {
|
if (!plugin.gameClientBridge?.commands?.length) {
|
||||||
missing.push("game client bridge declarations");
|
missing.push("game client bridge declarations");
|
||||||
}
|
}
|
||||||
@@ -1121,7 +1134,8 @@ const manifest = {
|
|||||||
type: source.server.type,
|
type: source.server.type,
|
||||||
displayName: source.server.displayName,
|
displayName: source.server.displayName,
|
||||||
supportedOs: source.server.supportedOS || source.server.supportedOs || [],
|
supportedOs: source.server.supportedOS || source.server.supportedOs || [],
|
||||||
createFormSchema: source.server.createFormSchema
|
createFormSchema: source.server.createFormSchema,
|
||||||
|
createFields: source.server.createFields ?? []
|
||||||
},
|
},
|
||||||
capabilities: localRunCapabilities,
|
capabilities: localRunCapabilities,
|
||||||
permissions: source.permissions,
|
permissions: source.permissions,
|
||||||
|
|||||||
Reference in New Issue
Block a user