mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 02:16:45 +08:00
* feat(server): add Plugin V1 lifecycle foundation Co-authored-by: multica-agent <[email protected]> * feat(server): complete Plugin V1 execution chain Co-authored-by: multica-agent <[email protected]> * feat(web): add temporary plugin settings demo Co-authored-by: multica-agent <[email protected]> * refactor(plugin): keep foundation free of demo content Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]>
229 lines
7.8 KiB
Go
229 lines
7.8 KiB
Go
package db_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
db "github.com/multica-ai/multica/server/pkg/db/generated"
|
|
"github.com/multica-ai/multica/server/pkg/plugincontract"
|
|
)
|
|
|
|
func TestPluginLifecycleQueries(t *testing.T) {
|
|
databaseURL := os.Getenv("DATABASE_URL")
|
|
if databaseURL == "" {
|
|
t.Skip("DATABASE_URL not set; skipping live-Postgres plugin lifecycle test")
|
|
}
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.New(ctx, databaseURL)
|
|
if err != nil {
|
|
t.Fatalf("connect database: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
var schemaReady bool
|
|
if err := pool.QueryRow(ctx, `SELECT to_regclass('plugin_identity') IS NOT NULL`).Scan(&schemaReady); err != nil {
|
|
t.Fatalf("check plugin schema: %v", err)
|
|
}
|
|
if !schemaReady {
|
|
t.Skip("plugin lifecycle migrations are not applied")
|
|
}
|
|
|
|
tx, err := pool.Begin(ctx)
|
|
if err != nil {
|
|
t.Fatalf("begin: %v", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
queries := db.New(tx)
|
|
|
|
suffix := uuid.NewString()
|
|
var workspaceID pgtype.UUID
|
|
if err := tx.QueryRow(ctx, `
|
|
INSERT INTO workspace (name, slug, description, issue_prefix)
|
|
VALUES ($1, $2, '', 'PLG')
|
|
RETURNING id
|
|
`, "Plugin lifecycle query test", "plugin-query-"+suffix).Scan(&workspaceID); err != nil {
|
|
t.Fatalf("create workspace: %v", err)
|
|
}
|
|
|
|
identity, err := queries.CreatePluginIdentity(ctx, db.CreatePluginIdentityParams{
|
|
PluginKey: "test.multica.plugin-" + suffix,
|
|
DisplayName: "Plugin Query Test",
|
|
PublisherID: "multica-test",
|
|
PublisherType: "official",
|
|
TrustTier: "official",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginIdentity: %v", err)
|
|
}
|
|
|
|
digest := func(value string) string { return plugincontract.DigestBytes([]byte(value)) }
|
|
release, err := queries.CreatePluginRelease(ctx, db.CreatePluginReleaseParams{
|
|
Version: "1.0.0",
|
|
Manifest: []byte(`{}`),
|
|
ManifestDigest: digest("manifest"),
|
|
SourceKind: plugincontract.SourceBundled,
|
|
SourceRef: "embedded://plugin-query-test/1.0.0",
|
|
ArchiveDigest: digest("archive"),
|
|
ArtifactRef: "cas://plugin-query-test/1.0.0",
|
|
ArtifactDigest: digest("artifact"),
|
|
ArtifactSize: 128,
|
|
Signature: make([]byte, 64),
|
|
SignatureKeyID: pgtype.Text{String: "test-release-key", Valid: true},
|
|
PluginID: identity.ID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginRelease: %v", err)
|
|
}
|
|
|
|
contribution, err := queries.CreatePluginContribution(ctx, db.CreatePluginContributionParams{
|
|
ContributionKey: "review-readiness",
|
|
Type: plugincontract.ContributionAgentSkillV1,
|
|
SchemaVersion: 1,
|
|
DisplayName: "Review Readiness",
|
|
Description: "Review evidence",
|
|
EntryPath: "skills/review-readiness/SKILL.md",
|
|
EntryDigest: digest("entry"),
|
|
RequiredDaemonFeatures: []byte(`[
|
|
"execution-manifest-v1",
|
|
"agent-skill-v1"
|
|
]`),
|
|
Ordinal: 0,
|
|
ReleaseID: release.ID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginContribution: %v", err)
|
|
}
|
|
|
|
installation, err := queries.CreatePluginInstallation(ctx, db.CreatePluginInstallationParams{
|
|
PluginID: identity.ID,
|
|
ReleaseID: release.ID,
|
|
WorkspaceID: workspaceID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginInstallation: %v", err)
|
|
}
|
|
if installation.Enabled || installation.ActiveGeneration != 0 || installation.LifecycleStatus != "installed" {
|
|
t.Fatalf("new installation is not disabled: %#v", installation)
|
|
}
|
|
|
|
firstGrant, err := queries.CreatePluginGrantRevision(ctx, db.CreatePluginGrantRevisionParams{
|
|
Capability: plugincontract.CapabilityAgentSkillContribute,
|
|
Decision: "granted",
|
|
Limits: []byte(`{}`),
|
|
InstallationID: installation.ID,
|
|
WorkspaceID: workspaceID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginGrantRevision first: %v", err)
|
|
}
|
|
secondGrant, err := queries.CreatePluginGrantRevision(ctx, db.CreatePluginGrantRevisionParams{
|
|
Capability: plugincontract.CapabilityAgentSkillContribute,
|
|
Decision: "denied",
|
|
Limits: []byte(`{}`),
|
|
InstallationID: installation.ID,
|
|
WorkspaceID: workspaceID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginGrantRevision second: %v", err)
|
|
}
|
|
if firstGrant.GrantRevision != 1 || secondGrant.GrantRevision != 2 {
|
|
t.Fatalf("grant revisions = %d, %d", firstGrant.GrantRevision, secondGrant.GrantRevision)
|
|
}
|
|
|
|
firstBinding, err := queries.CreatePluginBindingRevision(ctx, db.CreatePluginBindingRevisionParams{
|
|
ScopeType: "workspace",
|
|
ScopeID: workspaceID,
|
|
Enabled: true,
|
|
InstallationID: installation.ID,
|
|
WorkspaceID: workspaceID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginBindingRevision first: %v", err)
|
|
}
|
|
secondBinding, err := queries.CreatePluginBindingRevision(ctx, db.CreatePluginBindingRevisionParams{
|
|
ScopeType: "workspace",
|
|
ScopeID: workspaceID,
|
|
Enabled: false,
|
|
InstallationID: installation.ID,
|
|
WorkspaceID: workspaceID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePluginBindingRevision second: %v", err)
|
|
}
|
|
if firstBinding.BindingRevision != 1 || secondBinding.BindingRevision != 2 {
|
|
t.Fatalf("binding revisions = %d, %d", firstBinding.BindingRevision, secondBinding.BindingRevision)
|
|
}
|
|
|
|
updated, err := queries.SetPluginInstallationDesiredState(ctx, db.SetPluginInstallationDesiredStateParams{
|
|
WorkspaceID: workspaceID,
|
|
DesiredReleaseID: release.ID,
|
|
ID: installation.ID,
|
|
Enabled: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetPluginInstallationDesiredState: %v", err)
|
|
}
|
|
if !updated.Enabled || updated.DesiredGeneration != 2 || updated.ActiveGeneration != 0 || updated.LifecycleStatus != "activating" {
|
|
t.Fatalf("desired state update = %#v", updated)
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `SAVEPOINT immutable_release`); err != nil {
|
|
t.Fatalf("savepoint immutable release: %v", err)
|
|
}
|
|
if _, err := tx.Exec(ctx, `UPDATE plugin_release SET version = '1.0.1' WHERE id = $1`, release.ID); err == nil {
|
|
t.Fatal("immutable release update succeeded")
|
|
}
|
|
if _, err := tx.Exec(ctx, `ROLLBACK TO SAVEPOINT immutable_release`); err != nil {
|
|
t.Fatalf("rollback immutable release savepoint: %v", err)
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `SAVEPOINT append_only_contribution`); err != nil {
|
|
t.Fatalf("savepoint append-only contribution: %v", err)
|
|
}
|
|
if _, err := tx.Exec(ctx, `UPDATE plugin_contribution SET display_name = 'Changed' WHERE id = $1`, contribution.ID); err == nil {
|
|
t.Fatal("append-only contribution update succeeded")
|
|
}
|
|
if _, err := tx.Exec(ctx, `ROLLBACK TO SAVEPOINT append_only_contribution`); err != nil {
|
|
t.Fatalf("rollback append-only contribution savepoint: %v", err)
|
|
}
|
|
|
|
if err := queries.DeleteWorkspacePluginData(ctx, workspaceID); err != nil {
|
|
t.Fatalf("DeleteWorkspacePluginData: %v", err)
|
|
}
|
|
if _, err := queries.GetPluginInstallation(ctx, installation.ID); !errors.Is(err, pgx.ErrNoRows) {
|
|
t.Fatalf("installation after workspace cleanup error = %v", err)
|
|
}
|
|
remainingContributions, err := queries.ListPluginContributionsByRelease(ctx, release.ID)
|
|
if err != nil {
|
|
t.Fatalf("ListPluginContributionsByRelease: %v", err)
|
|
}
|
|
if len(remainingContributions) != 1 {
|
|
t.Fatalf("global contributions after workspace cleanup = %d, want 1", len(remainingContributions))
|
|
}
|
|
|
|
revoked, err := queries.RevokePluginRelease(ctx, db.RevokePluginReleaseParams{
|
|
RevocationStatus: "revoked",
|
|
RevocationReason: pgtype.Text{String: "integration test", Valid: true},
|
|
ID: release.ID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RevokePluginRelease: %v", err)
|
|
}
|
|
if revoked.RevocationStatus != "revoked" || !revoked.RevokedAt.Valid {
|
|
t.Fatalf("revoked release = %#v", revoked)
|
|
}
|
|
if _, err := queries.RevokePluginRelease(ctx, db.RevokePluginReleaseParams{
|
|
RevocationStatus: "quarantined",
|
|
RevocationReason: pgtype.Text{String: "second transition", Valid: true},
|
|
ID: release.ID,
|
|
}); !errors.Is(err, pgx.ErrNoRows) {
|
|
t.Fatalf("second release revocation error = %v", err)
|
|
}
|
|
}
|