Files
nezha_domains/integration/agentcompat/internal/contract/scenario_registry.go
T
2026-07-20 04:39:48 +00:00

176 lines
6.8 KiB
Go

package contract
import (
"errors"
"slices"
)
const (
ScenarioMetadata = "metadata"
ScenarioRegistrationConfigExec = "registration-config-exec"
ScenarioNAT = "nat"
ScenarioLegacyFM = "legacy-fm"
ScenarioTerminal = "terminal"
ScenarioMCPFilesystem = "mcp-filesystem"
ScenarioTransfer100MiB = "transfer-100mib"
ScenarioReconnect = "reconnect"
FaultAgentBadSecret = "agent-bad-secret"
FaultTransferHash = "transfer-hash"
FaultDashboardExit = "dashboard-exit"
)
type DedicatedArtifactKind uint8
const (
DedicatedArtifactNone DedicatedArtifactKind = iota
DedicatedArtifactTransfer
DedicatedArtifactReconnect
)
type ScenarioExecutionKind uint8
const (
ScenarioExecutionMetadata ScenarioExecutionKind = iota
ScenarioExecutionRegistrationConfigExec
ScenarioExecutionNAT
ScenarioExecutionLegacyFM
ScenarioExecutionTerminal
ScenarioExecutionMCPFilesystem
ScenarioExecutionTransfer
ScenarioExecutionReconnect
)
type ScenarioDefinition struct {
Name string
AllowedFaults []string
Execution ScenarioExecutionKind
DedicatedArtifact DedicatedArtifactKind
}
type AssertionDefinition struct {
Name string
Passed bool
}
const (
AssertionInjectedFault = "injected fault produced the expected scenario failure"
AssertionTransferWarmup = "small real upload and download warm-up precedes event and deadline quiescence"
AssertionTransferUpload = "exact 100MiB upload has size mode SHA and create_dirs"
AssertionTransferDownload = "exact 100MiB download has equal nonempty SHA"
AssertionTransferUploadReplay = "upload token replay is typed unauthorized"
AssertionTransferDownloadReplay = "download token replay is typed unauthorized"
AssertionTransferOversize = "100MiB plus one upload is typed too large"
AssertionTransferResidue = "Dashboard spool and Agent temp residue are zero"
AssertionTransferSentinels = "outside-root sentinels remain unchanged"
AssertionTransferHeap = "retained live heap stays within 16MiB"
AssertionTransferCleanup = "process listener and workspace cleanup completed"
AssertionTransferHashRejected = "transfer-hash rejects upload with typed 502"
AssertionTransferHashTargetAbsent = "transfer-hash leaves target absent"
AssertionReconnectDisconnect = "Dashboard disconnect barrier stopped generation one"
AssertionReconnectDashboard = "Dashboard generation two preserves fixture and recreates runtime clients"
AssertionReconnectIdentity = "Agent reconnect preserves exact server ID and UUID"
AssertionReconnectReceipts = "post-reconnect MCP task and result receipts are exactly once"
AssertionReconnectStale = "stale Dashboard generation cannot receive new task receipts"
AssertionReconnectAgent = "Agent restart advances state stream and preserves config identity"
AssertionReconnectSentinel = "outside-root sentinel remains unchanged"
AssertionReconnectCleanup = "multi-generation process listener and workspace cleanup completed"
)
func (definition ScenarioDefinition) DedicatedArtifactName() string {
switch definition.DedicatedArtifact {
case DedicatedArtifactNone:
return ""
case DedicatedArtifactTransfer:
return "transfer.json"
case DedicatedArtifactReconnect:
return "reconnect.json"
default:
return ""
}
}
func (definition ScenarioDefinition) Assertions(fault string) []AssertionDefinition {
switch definition.Name {
case ScenarioTransfer100MiB:
if fault == FaultTransferHash {
return assertions(
AssertionTransferWarmup, AssertionTransferHashRejected, AssertionTransferHashTargetAbsent,
AssertionTransferResidue, AssertionTransferSentinels, AssertionTransferCleanup, AssertionInjectedFault,
)
}
return assertions(
AssertionTransferWarmup, AssertionTransferUpload, AssertionTransferDownload,
AssertionTransferUploadReplay, AssertionTransferDownloadReplay, AssertionTransferOversize,
AssertionTransferResidue, AssertionTransferSentinels, AssertionTransferHeap, AssertionTransferCleanup,
)
case ScenarioReconnect:
if fault == FaultDashboardExit {
return assertions(AssertionReconnectDisconnect, AssertionReconnectSentinel, AssertionReconnectCleanup, AssertionInjectedFault)
}
return assertions(
AssertionReconnectDisconnect, AssertionReconnectDashboard, AssertionReconnectIdentity,
AssertionReconnectReceipts, AssertionReconnectStale, AssertionReconnectAgent,
AssertionReconnectSentinel, AssertionReconnectCleanup,
)
default:
return nil
}
}
func assertions(names ...string) []AssertionDefinition {
definitions := make([]AssertionDefinition, 0, len(names))
for _, name := range names {
definitions = append(definitions, AssertionDefinition{Name: name, Passed: name != AssertionInjectedFault})
}
return definitions
}
var scenarioDefinitions = []ScenarioDefinition{
{Name: ScenarioMetadata, AllowedFaults: []string{""}, Execution: ScenarioExecutionMetadata},
{Name: ScenarioRegistrationConfigExec, AllowedFaults: []string{"", FaultAgentBadSecret}, Execution: ScenarioExecutionRegistrationConfigExec},
{Name: ScenarioNAT, AllowedFaults: []string{""}, Execution: ScenarioExecutionNAT},
{Name: ScenarioLegacyFM, AllowedFaults: []string{"", FaultAgentBadSecret}, Execution: ScenarioExecutionLegacyFM},
{Name: ScenarioTerminal, AllowedFaults: []string{""}, Execution: ScenarioExecutionTerminal},
{Name: ScenarioMCPFilesystem, AllowedFaults: []string{""}, Execution: ScenarioExecutionMCPFilesystem},
{Name: ScenarioTransfer100MiB, AllowedFaults: []string{"", FaultTransferHash}, Execution: ScenarioExecutionTransfer, DedicatedArtifact: DedicatedArtifactTransfer},
{Name: ScenarioReconnect, AllowedFaults: []string{"", FaultDashboardExit}, Execution: ScenarioExecutionReconnect, DedicatedArtifact: DedicatedArtifactReconnect},
}
func ScenarioDefinitions() []ScenarioDefinition {
definitions := make([]ScenarioDefinition, len(scenarioDefinitions))
copy(definitions, scenarioDefinitions)
for index := range definitions {
definitions[index].AllowedFaults = slices.Clone(definitions[index].AllowedFaults)
}
return definitions
}
func ScenarioDefinitionByName(name string) (ScenarioDefinition, error) {
for _, definition := range scenarioDefinitions {
if definition.Name == name {
definition.AllowedFaults = slices.Clone(definition.AllowedFaults)
return definition, nil
}
}
return ScenarioDefinition{}, errors.New("unsupported scenario")
}
func ValidateScenarioFault(scenario Scenario, fault Fault) error {
definition, err := ScenarioDefinitionByName(scenario.String())
if err != nil {
return err
}
if !slices.Contains(definition.AllowedFaults, fault.String()) {
return errors.New("unsupported fault for scenario")
}
return nil
}
func IsSupportedScenario(name string) bool {
_, err := ScenarioDefinitionByName(name)
return err == nil
}