feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
118
internal/services/channel_policy_concurrency_test.go
Normal file
118
internal/services/channel_policy_concurrency_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestChannelConfigCompareAndSwapRejectsStalePolicyAndSecretWrites(t *testing.T) {
|
||||
for _, action := range []string{"editor", "secret-reset"} {
|
||||
t.Run(action, func(t *testing.T) {
|
||||
previousDB := sqls.DB()
|
||||
db := setupChannelServiceTestDB(t)
|
||||
t.Cleanup(func() { sqls.SetDB(previousDB) })
|
||||
agent := createChannelServiceTestAgent(t, db, 1001)
|
||||
channel, err := ChannelService.CreateChannel(request.CreateChannelRequest{ChannelType: enums.ChannelTypeWeb, AIAgentID: agent.ID, Name: "Policy channel", Status: int(enums.StatusOk), ConfigJSON: `{"requireSignedIdentity":false,"userTokenSecret":"test-old-secret"}`}, channelServiceTestOperator())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var injected atomic.Bool
|
||||
callbackName := "test:concurrent_channel_policy"
|
||||
// Commit another writer after the stale service read, just before its SQL.
|
||||
// Preserve updated_at deliberately: bytewise JSON CAS must also catch this.
|
||||
if err := db.Callback().Update().Before("gorm:update").Register(callbackName, func(tx *gorm.DB) {
|
||||
if tx.Statement.Schema == nil || tx.Statement.Schema.Table != "t_channel" || !injected.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
cfg, parseErr := ChannelService.ParseWebChannelConfig(channel.ConfigJSON)
|
||||
if parseErr != nil {
|
||||
tx.AddError(parseErr)
|
||||
return
|
||||
}
|
||||
cfg.RequireSignedIdentity = true
|
||||
cfg.UserTokenSecret = "test-new-secret"
|
||||
encoded, encodeErr := json.Marshal(cfg)
|
||||
if encodeErr != nil {
|
||||
tx.AddError(encodeErr)
|
||||
return
|
||||
}
|
||||
if writeErr := db.Session(&gorm.Session{NewDB: true}).Model(&models.Channel{}).Where("id = ?", channel.ID).Updates(map[string]any{"config_json": string(encoded), "updated_at": channel.UpdatedAt}).Error; writeErr != nil {
|
||||
tx.AddError(writeErr)
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Callback().Update().Remove(callbackName) })
|
||||
switch action {
|
||||
case "editor":
|
||||
err = ChannelService.UpdateChannel(request.UpdateChannelRequest{ID: channel.ID, CreateChannelRequest: request.CreateChannelRequest{ChannelType: enums.ChannelTypeWeb, AIAgentID: agent.ID, Name: "Old editor title", Status: int(enums.StatusOk), ConfigJSON: `{"title":"old editor"}`}}, channelServiceTestOperator())
|
||||
case "secret-reset":
|
||||
var secret string
|
||||
secret, err = ChannelService.ResetUserTokenSecret(channel.ID, channelServiceTestOperator())
|
||||
if secret != "" {
|
||||
t.Fatal("failed reset returned an uncommitted secret")
|
||||
}
|
||||
}
|
||||
if !injected.Load() || err == nil {
|
||||
t.Fatal("stale writer was not rejected")
|
||||
}
|
||||
current := ChannelService.Get(channel.ID)
|
||||
cfg, parseErr := ChannelService.ParseWebChannelConfig(current.ConfigJSON)
|
||||
if parseErr != nil || !cfg.RequireSignedIdentity || cfg.UserTokenSecret != "test-new-secret" {
|
||||
t.Fatal("stale writer replaced the concurrent policy/secret")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelConfigCompareAndSwapUsesExactBytes(t *testing.T) {
|
||||
db, channel := setupIdentitySecurityDB(t)
|
||||
channel.ConfigJSON = `{"userTokenSecret":"CaseSensitive"}`
|
||||
if err := db.Model(channel).Updates(map[string]any{"config_json": channel.ConfigJSON, "updated_at": time.Now()}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := *channel
|
||||
if err := db.Model(channel).UpdateColumn("config_json", `{"userTokenSecret":"casesensitive"}`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updated, err := repositories.ChannelRepository.UpdatesConfigIfUnchanged(db, &expected, map[string]any{"config_json": `{"requireSignedIdentity":false}`})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if updated {
|
||||
t.Fatal("case-sensitive secret change was not treated as a conflict")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelConfigCompareAndSwapAllowsCurrentEditorAndReset(t *testing.T) {
|
||||
previousDB := sqls.DB()
|
||||
db := setupChannelServiceTestDB(t)
|
||||
t.Cleanup(func() { sqls.SetDB(previousDB) })
|
||||
agent := createChannelServiceTestAgent(t, db, 1001)
|
||||
channel, err := ChannelService.CreateChannel(request.CreateChannelRequest{ChannelType: enums.ChannelTypeWeb, AIAgentID: agent.ID, Name: "Current policy", Status: int(enums.StatusOk), ConfigJSON: `{"requireSignedIdentity":true}`}, channelServiceTestOperator())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ChannelService.UpdateChannel(request.UpdateChannelRequest{ID: channel.ID, CreateChannelRequest: request.CreateChannelRequest{ChannelType: enums.ChannelTypeWeb, AIAgentID: agent.ID, Name: "Current policy edited", Status: int(enums.StatusOk), ConfigJSON: `{"title":"Updated title"}`}}, channelServiceTestOperator())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secret, err := ChannelService.ResetUserTokenSecret(channel.ID, channelServiceTestOperator())
|
||||
if err != nil || secret == "" {
|
||||
t.Fatal("current secret reset failed")
|
||||
}
|
||||
cfg, err := ChannelService.ParseWebChannelConfig(ChannelService.Get(channel.ID).ConfigJSON)
|
||||
if err != nil || !cfg.RequireSignedIdentity || cfg.UserTokenSecret != secret {
|
||||
t.Fatal("current update/reset lost policy or returned unsaved secret")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user