Fix server file list fallback
This commit is contained in:
@@ -1165,6 +1165,50 @@ func TestServerFileListFallsBackToPluginWorkspaceWithoutRunListCapability(t *tes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerFileListFallsBackToPluginWorkspaceWhenRunEndpointUnavailable(t *testing.T) {
|
||||||
|
svc := newTestCoreService()
|
||||||
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||||
|
plugin.FileWorkspace = scumTestFileWorkspace()
|
||||||
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||||
|
t.Fatalf("update plugin workspace: %v", err)
|
||||||
|
}
|
||||||
|
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-list-run-missing", DisplayName: "File List Run Missing", Email: "file-list-run-missing@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||||
|
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-run-missing", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File List Missing Run", State: domain.ServerInstanceStateRunning})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create server: %v", err)
|
||||||
|
}
|
||||||
|
if err := svc.store.RunEndpoints().Delete(endpoint.ID); err != nil {
|
||||||
|
t.Fatalf("delete run endpoint: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "scum-config", IdempotencyKey: "idem-file-list-run-missing"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("refresh file list without run endpoint: %v", err)
|
||||||
|
}
|
||||||
|
if refresh.State != "ready" || refresh.Job.ID != "" || len(refresh.Entries) != 2 || refresh.Entries[1].LogicalKey != "scum-server-settings" || !strings.Contains(refresh.Reason, "Run 注册记录不存在") {
|
||||||
|
t.Fatalf("expected declared workspace fallback without run endpoint, got %+v", refresh)
|
||||||
|
}
|
||||||
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list missing-run jobs: %v", err)
|
||||||
|
}
|
||||||
|
if len(jobs) != 0 {
|
||||||
|
t.Fatalf("missing Run endpoint must not create a failed refresh job, got %+v", jobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
draft, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-run-unbound", PluginID: plugin.ID, Name: "File List Unbound Run", State: domain.ServerInstanceStateDraft})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create unbound draft server: %v", err)
|
||||||
|
}
|
||||||
|
unbound, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: draft.ID, DirectoryKey: "scum-config", IdempotencyKey: "idem-file-list-run-unbound"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("refresh file list without run binding: %v", err)
|
||||||
|
}
|
||||||
|
if unbound.State != "ready" || unbound.Job.ID != "" || len(unbound.Entries) != 2 || unbound.Entries[1].LogicalKey != "scum-server-settings" || !strings.Contains(unbound.Reason, "尚未绑定 Run") {
|
||||||
|
t.Fatalf("expected declared workspace fallback without run binding, got %+v", unbound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeclaredFileReadSnapshotProjectionStatesAndPassThroughContent(t *testing.T) {
|
func TestDeclaredFileReadSnapshotProjectionStatesAndPassThroughContent(t *testing.T) {
|
||||||
svc := newTestCoreService()
|
svc := newTestCoreService()
|
||||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||||
|
|||||||
@@ -152,11 +152,33 @@ func (svc *CoreService) RefreshServerFileListForSession(sessionID string, reques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ServerFileListResult{}, err
|
return domain.ServerFileListResult{}, err
|
||||||
}
|
}
|
||||||
|
entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Workspace, request.DirectoryKey), request.Query)
|
||||||
|
if strings.TrimSpace(ctx.Instance.RunEndpointID) == "" {
|
||||||
|
return domain.CopyServerFileListResult(domain.ServerFileListResult{
|
||||||
|
ServerInstanceID: ctx.Instance.ID,
|
||||||
|
PluginID: ctx.Plugin.ID,
|
||||||
|
DirectoryKey: request.DirectoryKey,
|
||||||
|
Path: request.Path,
|
||||||
|
State: "ready",
|
||||||
|
Entries: entries,
|
||||||
|
Reason: "当前服务器尚未绑定 Run,已展示插件声明的逻辑文件。",
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
endpoint, err := svc.GetRunEndpoint(ctx.Instance.RunEndpointID)
|
endpoint, err := svc.GetRunEndpoint(ctx.Instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, repo.ErrNotFound) {
|
||||||
|
return domain.CopyServerFileListResult(domain.ServerFileListResult{
|
||||||
|
ServerInstanceID: ctx.Instance.ID,
|
||||||
|
PluginID: ctx.Plugin.ID,
|
||||||
|
DirectoryKey: request.DirectoryKey,
|
||||||
|
Path: request.Path,
|
||||||
|
State: "ready",
|
||||||
|
Entries: entries,
|
||||||
|
Reason: "当前 Run 注册记录不存在,已展示插件声明的逻辑文件。",
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
return domain.ServerFileListResult{}, err
|
return domain.ServerFileListResult{}, err
|
||||||
}
|
}
|
||||||
entries := filterServerFileEntries(serverFileEntriesFromDeclaredWorkspace(ctx.Workspace, request.DirectoryKey), request.Query)
|
|
||||||
if !containsString(endpoint.Capabilities, domain.JobCapabilityFilesList) {
|
if !containsString(endpoint.Capabilities, domain.JobCapabilityFilesList) {
|
||||||
return domain.CopyServerFileListResult(domain.ServerFileListResult{
|
return domain.CopyServerFileListResult(domain.ServerFileListResult{
|
||||||
ServerInstanceID: ctx.Instance.ID,
|
ServerInstanceID: ctx.Instance.ID,
|
||||||
|
|||||||
@@ -442,6 +442,9 @@ const bridgeActions = Array.isArray(plugin.bridgeActions) ? plugin.bridgeActions
|
|||||||
const lifecycleProfiles = plugin.runtimeProfiles?.lifecycleProfiles ?? [];
|
const lifecycleProfiles = plugin.runtimeProfiles?.lifecycleProfiles ?? [];
|
||||||
const logSources = plugin.runtimeProfiles?.logSources ?? [];
|
const logSources = plugin.runtimeProfiles?.logSources ?? [];
|
||||||
const pageKeys = (plugin.pages ?? []).map((page) => page.key);
|
const pageKeys = (plugin.pages ?? []).map((page) => page.key);
|
||||||
|
const fileWorkspace = plugin.fileWorkspace ?? {};
|
||||||
|
const fileWorkspaceDirectoryKeys = (fileWorkspace.directories ?? []).map((directory) => directory.key);
|
||||||
|
const fileWorkspaceFileKeys = (fileWorkspace.files ?? []).map((file) => file.key);
|
||||||
if (plugin.version !== expectedVersion) {
|
if (plugin.version !== expectedVersion) {
|
||||||
missing.push(`version ${expectedVersion}`);
|
missing.push(`version ${expectedVersion}`);
|
||||||
}
|
}
|
||||||
@@ -473,6 +476,19 @@ if (!createFields.some((field) => field.key === "queryPort" && field.defaultValu
|
|||||||
if (!createFields.some((field) => field.key === "maxPlayers" && field.defaultValue === "128")) {
|
if (!createFields.some((field) => field.key === "maxPlayers" && field.defaultValue === "128")) {
|
||||||
missing.push("SCUM max players create field");
|
missing.push("SCUM max players create field");
|
||||||
}
|
}
|
||||||
|
if (fileWorkspace.defaultDirectoryKey !== "scum-config") {
|
||||||
|
missing.push("SCUM file workspace default directory");
|
||||||
|
}
|
||||||
|
for (const directoryKey of ["scum-config", "scum-logs"]) {
|
||||||
|
if (!fileWorkspaceDirectoryKeys.includes(directoryKey)) {
|
||||||
|
missing.push(`SCUM file workspace directory ${directoryKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fileKey of ["scum-server-settings", "scum-chat-log"]) {
|
||||||
|
if (!fileWorkspaceFileKeys.includes(fileKey)) {
|
||||||
|
missing.push(`SCUM file workspace file ${fileKey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!plugin.gameClientBridge?.commands?.length) {
|
if (!plugin.gameClientBridge?.commands?.length) {
|
||||||
missing.push("game client bridge declarations");
|
missing.push("game client bridge declarations");
|
||||||
}
|
}
|
||||||
@@ -1146,6 +1162,7 @@ const manifest = {
|
|||||||
ai: source.ai,
|
ai: source.ai,
|
||||||
productionLifecycle: source.productionLifecycle,
|
productionLifecycle: source.productionLifecycle,
|
||||||
remoteAccess: source.remoteAccess,
|
remoteAccess: source.remoteAccess,
|
||||||
|
fileWorkspace: source.fileWorkspace,
|
||||||
runtimeProfiles: localRuntimeProfiles,
|
runtimeProfiles: localRuntimeProfiles,
|
||||||
gameClientBridge: localGameClientBridge
|
gameClientBridge: localGameClientBridge
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user