feat: 自动更新

This commit is contained in:
npc0-hue
2026-07-15 19:43:06 +08:00
parent f64eb0831f
commit f3b14b7945
54 changed files with 3207 additions and 589 deletions
+167 -49
View File
@@ -1,12 +1,12 @@
package service
import (
"encoding/json"
"errors"
"strings"
"testing"
"browser.local/platform/domain"
"browser.local/platform/repo"
)
func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
@@ -21,9 +21,16 @@ func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.Status != domain.DistributionStatusAvailable {
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" || distribution.Checksum != "" {
t.Fatalf("unexpected run distribution: %+v", distribution)
}
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil || job.Capability != domain.JobCapabilityDistributionBuild || job.State != domain.JobStateQueued {
t.Fatalf("expected queued backend build job, job=%+v err=%v", job, err)
}
if _, err := svc.GetArtifact(distribution.ArtifactID); !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("generation must not publish config JSON as an artifact, got %v", err)
}
keys, err := svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{
ServerInstanceID: instance.ID,
@@ -76,6 +83,78 @@ func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing
}
}
func TestCoreServiceRunDistributionRetryReusesPartialArtifact(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
if err != nil {
t.Fatalf("ensure run key: %v", err)
}
idempotencyKey := "web:run.generate:scum-alpha:1784043453685"
distributionID := distributionID("run-dist", instance.ID, "windows", "amd64", key.Generation, idempotencyKey)
artifactID := artifactIDForDistribution(distributionID)
payload := []byte("legacy generated config is not a binary distribution")
partialArtifact, err := svc.createPlatformArtifactPayload(artifactID, domain.ArtifactOwnerKindServerInstance, instance.ID, payload)
if err != nil {
t.Fatalf("create partial artifact: %v", err)
}
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "windows",
TargetArch: "amd64",
IdempotencyKey: idempotencyKey,
})
if err != nil {
t.Fatalf("retry run generation should recover partial artifact: %v", err)
}
if distribution.ID != distributionID || distribution.ArtifactID == partialArtifact.ID || distribution.Checksum != "" {
t.Fatalf("expected real binary build to ignore legacy config artifact, distribution=%+v artifact=%+v", distribution, partialArtifact)
}
if distribution.PackageFormat != "zip" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" {
t.Fatalf("unexpected recovered distribution: %+v", distribution)
}
recoveredConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
if recoveredConfig.AuthKey != plainKey || recoveredConfig.KeyGeneration != key.Generation {
t.Fatalf("expected recovered artifact payload to remain downloadable, got %+v", recoveredConfig)
}
}
func TestCoreServicePushRunUpdateReusesExistingUpdateJob(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "windows",
TargetArch: "amd64",
IdempotencyKey: "idem-run-before-update",
})
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
distribution = completeDistributionBuild(t, svc, distribution, []byte("compiled run archive"))
request := domain.RunUpdateRequest{
ServerInstanceID: instance.ID,
ArtifactID: distribution.ArtifactID,
Checksum: distribution.Checksum,
IdempotencyKey: "idem-run-update-repeat",
}
first, err := svc.PushRunUpdateForSession(session, request)
if err != nil {
t.Fatalf("push run update: %v", err)
}
second, err := svc.PushRunUpdateForSession(session, request)
if err != nil {
t.Fatalf("push run update again should reuse existing update job: %v", err)
}
if second.ID != first.ID || second.JobID != first.JobID || second.ArtifactID != first.ArtifactID {
t.Fatalf("expected repeated push to return existing update job, first=%+v second=%+v", first, second)
}
if second.Status != domain.DistributionJobStatusQueued {
t.Fatalf("expected queued existing update job, got %+v", second)
}
}
func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
@@ -88,6 +167,7 @@ func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *test
t.Fatalf("generate run distribution: %v", err)
}
oldConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
distribution = completeDistributionBuild(t, svc, distribution, []byte("compiled run archive before reset"))
reset, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
ServerInstanceID: instance.ID,
@@ -161,6 +241,7 @@ func TestCoreServiceBuildsClientManagerWithDistinctKeyAndAuditsSensitiveOperatio
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
runDistribution = completeDistributionBuild(t, svc, runDistribution, []byte("compiled run archive"))
if _, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: runDistribution.ArtifactID}); err != nil {
t.Fatalf("open run download: %v", err)
}
@@ -179,7 +260,7 @@ func TestCoreServiceBuildsClientManagerWithDistinctKeyAndAuditsSensitiveOperatio
t.Fatalf("generate client-manager distribution: %v", err)
}
clientConfig := readGeneratedPackageConfig(t, svc, session, clientDistribution.ArtifactID)
if clientDistribution.KeyGeneration != 1 || clientDistribution.BuildJobID == "" || clientDistribution.Status != domain.DistributionStatusAvailable {
if clientDistribution.KeyGeneration != 1 || clientDistribution.BuildJobID == "" || clientDistribution.Status != domain.DistributionStatusBuilding {
t.Fatalf("unexpected client-manager distribution: %+v", clientDistribution)
}
if clientConfig.AuthKey == runConfig.AuthKey || clientDistribution.SecretRef == runDistribution.SecretRef {
@@ -189,29 +270,13 @@ func TestCoreServiceBuildsClientManagerWithDistinctKeyAndAuditsSensitiveOperatio
if err != nil {
t.Fatalf("get build job: %v", err)
}
if build.Status != domain.DistributionJobStatusSucceeded || build.RepositoryURL != "https://github.com/F88888/scum_client.git" || build.SourceRevision != "main" {
if build.Status != domain.DistributionJobStatusQueued || build.RepositoryURL != "https://github.com/F88888/scum_client.git" || build.SourceRevision != "main" {
t.Fatalf("unexpected build job: %+v", build)
}
if build.LogsRef == "" || !strings.HasPrefix(build.LogsRef, "artifact://") {
t.Fatalf("expected redacted build log artifact ref, got %+v", build)
}
packagePayload := readClientManagerPackage(t, svc, session, clientDistribution.ArtifactID)
if packagePayload.Checkout.CheckoutRef != "branch/main" || packagePayload.Config.AuthKey != clientConfig.AuthKey || packagePayload.KeyFingerprint == "" {
t.Fatalf("expected package checkout metadata and injected config, got %+v", packagePayload)
}
if len(packagePayload.OutputArtifacts) == 0 || packagePayload.BuildLogRef != build.LogsRef {
t.Fatalf("expected output artifacts and build log ref, got %+v build=%+v", packagePayload, build)
}
buildLog := readArtifactString(t, svc, session, strings.TrimPrefix(build.LogsRef, "artifact://"))
for _, expected := range []string{"client-manager checkout prepared", "checkoutRef=branch/main", "dependencyCheck=typed build profile accepted", "configInjection=secret ref"} {
if !strings.Contains(buildLog, expected) {
t.Fatalf("expected build log to contain %q, got %q", expected, buildLog)
}
}
for _, forbidden := range []string{runConfig.AuthKey, clientConfig.AuthKey, "password=", "unix://", "tcp://", "/Users/", "mysql://", "sqlite://"} {
if strings.Contains(buildLog, forbidden) {
t.Fatalf("build log leaked forbidden fragment %q: %s", forbidden, buildLog)
}
clientDistribution = completeClientDistributionBuild(t, svc, clientDistribution, []byte("compiled client archive"))
build, err = svc.store.ClientManagerBuildJobs().Get(clientDistribution.BuildJobID)
if err != nil || build.Status != domain.DistributionJobStatusSucceeded || build.Checksum == "" {
t.Fatalf("expected uploaded client build to project as succeeded, build=%+v err=%v", build, err)
}
_, err = svc.GenerateClientManagerDistributionForSession(session, domain.ClientManagerBuildRequest{
@@ -293,6 +358,7 @@ func newDistributionTestFixture(t *testing.T) (*CoreService, string, domain.Serv
t.Fatalf("update plugin fixture: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities,
domain.JobCapabilityDistributionBuild,
domain.JobCapabilityRunSelfUpdate,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
@@ -323,47 +389,99 @@ func newDistributionTestFixture(t *testing.T) (*CoreService, string, domain.Serv
func readGeneratedPackageConfig(t *testing.T, svc *CoreService, session string, artifactID string) generatedPackageConfig {
t.Helper()
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: artifactID})
_ = session
runs, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{})
if err != nil {
t.Fatalf("read artifact content: %v", err)
t.Fatalf("list run distributions: %v", err)
}
var config generatedPackageConfig
if err := json.Unmarshal(content.Payload, &config); err != nil {
t.Fatalf("unmarshal generated config: %v", err)
}
if config.AuthKey == "" {
var packagePayload generatedClientManagerPackage
if err := json.Unmarshal(content.Payload, &packagePayload); err != nil {
t.Fatalf("unmarshal generated client-manager package: %v", err)
for _, distribution := range runs {
if distribution.ArtifactID != artifactID {
continue
}
config = packagePayload.Config
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentRun, "")
if err != nil {
t.Fatalf("get run key: %v", err)
}
plain, err := decryptRuntimeKey(key.EncryptedKey)
if err != nil {
t.Fatalf("decrypt run key: %v", err)
}
return generatedPackageConfig{Kind: "run", ServerInstanceID: distribution.ServerInstanceID, PluginID: distribution.PluginID, RunEndpointID: distribution.RunEndpointID, TargetOS: distribution.TargetOS, TargetArch: distribution.TargetArch, SecretRef: distribution.SecretRef, KeyGeneration: distribution.KeyGeneration, AuthKey: plain}
}
if config.AuthKey == "" || config.SecretRef == "" || config.KeyGeneration <= 0 {
t.Fatalf("generated package config is incomplete: %+v", config)
clients, err := svc.store.ClientManagerDistributions().List(domain.ClientManagerDistributionFilter{})
if err != nil {
t.Fatalf("list client distributions: %v", err)
}
return config
for _, distribution := range clients {
if distribution.ArtifactID != artifactID {
continue
}
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentClientManager, distribution.ProfileKey)
if err != nil {
t.Fatalf("get client key: %v", err)
}
plain, err := decryptRuntimeKey(key.EncryptedKey)
if err != nil {
t.Fatalf("decrypt client key: %v", err)
}
return generatedPackageConfig{Kind: "client-manager", ServerInstanceID: distribution.ServerInstanceID, PluginID: distribution.PluginID, ProfileKey: distribution.ProfileKey, TargetOS: distribution.TargetOS, TargetArch: distribution.TargetArch, SecretRef: distribution.SecretRef, KeyGeneration: distribution.KeyGeneration, AuthKey: plain}
}
t.Fatalf("distribution for artifact %s was not found", artifactID)
return generatedPackageConfig{}
}
func readClientManagerPackage(t *testing.T, svc *CoreService, session string, artifactID string) generatedClientManagerPackage {
func completeDistributionBuild(t *testing.T, svc *CoreService, distribution domain.RunDistribution, payload []byte) domain.RunDistribution {
t.Helper()
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: artifactID})
artifact, err := svc.createPlatformArtifactPayload(distribution.ArtifactID, domain.ArtifactOwnerKindJob, distribution.BuildJobID, payload)
if err != nil {
t.Fatalf("read client-manager package content: %v", err)
t.Fatalf("publish run build artifact: %v", err)
}
var packagePayload generatedClientManagerPackage
if err := json.Unmarshal(content.Payload, &packagePayload); err != nil {
t.Fatalf("unmarshal generated client-manager package: %v", err)
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil {
t.Fatalf("get run build job: %v", err)
}
return packagePayload
job.State = domain.JobStateSucceeded
job.Progress = domain.JobProgress{Percent: 100, Message: "package_finalize: build artifact available"}
job.ResultRef = "artifact://" + artifact.ID
job.UpdatedAt = svc.now()
if err := svc.store.Jobs().Update(job); err != nil {
t.Fatalf("update run build job: %v", err)
}
if err := svc.projectDistributionBuildResult(job, svc.now()); err != nil {
t.Fatalf("project run build: %v", err)
}
updated, err := svc.store.RunDistributions().Get(distribution.ID)
if err != nil {
t.Fatalf("get completed run distribution: %v", err)
}
return updated
}
func readArtifactString(t *testing.T, svc *CoreService, session string, artifactID string) string {
func completeClientDistributionBuild(t *testing.T, svc *CoreService, distribution domain.ClientManagerDistribution, payload []byte) domain.ClientManagerDistribution {
t.Helper()
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: artifactID})
artifact, err := svc.createPlatformArtifactPayload(distribution.ArtifactID, domain.ArtifactOwnerKindJob, distribution.BuildJobID, payload)
if err != nil {
t.Fatalf("read artifact content: %v", err)
t.Fatalf("publish client build artifact: %v", err)
}
return string(content.Payload)
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil {
t.Fatalf("get client build job: %v", err)
}
job.State = domain.JobStateSucceeded
job.Progress = domain.JobProgress{Percent: 100, Message: "package_finalize: build artifact available"}
job.ResultRef = "artifact://" + artifact.ID
job.UpdatedAt = svc.now()
if err := svc.store.Jobs().Update(job); err != nil {
t.Fatalf("update client build job: %v", err)
}
if err := svc.projectDistributionBuildResult(job, svc.now()); err != nil {
t.Fatalf("project client build: %v", err)
}
updated, err := svc.store.ClientManagerDistributions().Get(distribution.ID)
if err != nil {
t.Fatalf("get completed client distribution: %v", err)
}
return updated
}
func TestCoreServiceDeniesRunDistributionWithoutPluginDeclaration(t *testing.T) {