feat: harden runtime import and frontend verification workflows
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled

This commit is contained in:
phamnazage-jpg
2026-06-04 20:02:36 +08:00
parent 7ce72cbc35
commit 77b7f7f660
32 changed files with 2657 additions and 109 deletions

View File

@@ -99,6 +99,7 @@ func (r AccountImportResult) HasAdvisoryWarning() bool {
type hostAdapter interface {
sub2api.HostAdapter
CheckGatewayAccess(ctx context.Context, req sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error)
CheckGatewayCompletion(ctx context.Context, req sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error)
}
func GatewayAccessReady(result sub2api.GatewayAccessResult) bool {

View File

@@ -911,7 +911,15 @@ func (f *fakeHostAdapter) GetHostVersion(context.Context) (string, error) {
return f.hostVersion, nil
}
func (f *fakeHostAdapter) ProbeCapabilities(context.Context) (sub2api.HostCapabilities, error) {
return sub2api.HostCapabilities{}, nil
return sub2api.HostCapabilities{
Groups: true,
Channels: true,
Plans: true,
Accounts: true,
AccountTest: true,
AccountModels: true,
Subscriptions: true,
}, nil
}
func (f *fakeHostAdapter) CreateGroup(_ context.Context, req sub2api.CreateGroupRequest) (sub2api.GroupRef, error) {
f.createGroupCalls++

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/pack"
"sub2api-cn-relay-manager/internal/store/sqlite"
)
@@ -66,6 +67,12 @@ func (s *RuntimeImportService) Import(ctx context.Context, req RuntimeImportRequ
if err != nil {
return RuntimeImportResult{}, fmt.Errorf("probe host capabilities: %w", err)
}
// Host readiness preflight check
if err := validateHostReadiness(capabilities); err != nil {
return RuntimeImportResult{}, fmt.Errorf("host readiness preflight failed: %w", err)
}
capabilityProbeJSON, err := json.Marshal(capabilities)
if err != nil {
return RuntimeImportResult{}, fmt.Errorf("marshal host capabilities: %w", err)
@@ -302,3 +309,26 @@ func firstNonEmpty(values ...string) string {
}
return ""
}
// validateHostReadiness performs preflight checks on host capabilities
// to ensure the host is ready for import operations.
func validateHostReadiness(caps sub2api.HostCapabilities) error {
var missing []string
if !caps.Groups {
missing = append(missing, "groups")
}
if !caps.Channels {
missing = append(missing, "channels")
}
if !caps.Accounts {
missing = append(missing, "accounts")
}
if !caps.AccountTest {
missing = append(missing, "account_test")
}
if len(missing) > 0 {
return fmt.Errorf("host missing required capabilities: %v", missing)
}
return nil
}

View File

@@ -806,3 +806,85 @@ func queryCount(t *testing.T, db *sql.DB, table string) int {
}
return count
}
func TestValidateHostReadiness(t *testing.T) {
t.Run("all capabilities present", func(t *testing.T) {
caps := sub2api.HostCapabilities{
Groups: true,
Channels: true,
Accounts: true,
AccountTest: true,
AccountModels: true,
Plans: true,
Subscriptions: true,
}
if err := validateHostReadiness(caps); err != nil {
t.Fatalf("validateHostReadiness() = %v, want nil", err)
}
})
t.Run("missing groups", func(t *testing.T) {
caps := sub2api.HostCapabilities{
Groups: false,
Channels: true,
Accounts: true,
AccountTest: true,
}
if err := validateHostReadiness(caps); err == nil {
t.Fatal("validateHostReadiness() = nil, want error for missing groups")
} else if !strings.Contains(err.Error(), "groups") {
t.Fatalf("validateHostReadiness() = %v, want error mentioning groups", err)
}
})
t.Run("missing multiple capabilities", func(t *testing.T) {
caps := sub2api.HostCapabilities{
Groups: false,
Channels: false,
Accounts: false,
AccountTest: false,
}
if err := validateHostReadiness(caps); err == nil {
t.Fatal("validateHostReadiness() = nil, want error for multiple missing capabilities")
}
})
t.Run("missing accounts", func(t *testing.T) {
caps := sub2api.HostCapabilities{
Groups: true,
Channels: true,
Accounts: false,
AccountTest: true,
}
if err := validateHostReadiness(caps); err == nil {
t.Fatal("validateHostReadiness() = nil, want error for missing accounts")
}
})
t.Run("missing test account", func(t *testing.T) {
caps := sub2api.HostCapabilities{
Groups: true,
Channels: true,
Accounts: true,
AccountTest: false,
}
if err := validateHostReadiness(caps); err == nil {
t.Fatal("validateHostReadiness() = nil, want error for missing account_test")
}
})
t.Run("plans and subscriptions not required", func(t *testing.T) {
caps := sub2api.HostCapabilities{
Groups: true,
Channels: true,
Accounts: true,
AccountTest: true,
AccountModels: false,
Plans: false,
Subscriptions: false,
}
if err := validateHostReadiness(caps); err != nil {
t.Fatalf("validateHostReadiness() = %v, want nil (plans/subscriptions are optional)", err)
}
})
}