blob: 7e7bc91ba19082b9adb2bbcf2fef25a92f259ee5 [file]
/*
* Copyright (c) 2026 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <pw_unit_test/framework.h>
#include <app/clusters/audio-control-server/AudioControlCluster.h>
#include <app/clusters/audio-control-server/AudioControlDelegate.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <app/server-cluster/testing/AttributeTesting.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <clusters/AudioControl/Commands.h>
#include <clusters/ScenesManagement/Structs.h>
#include <lib/support/DefaultStorageKeyAllocator.h>
namespace {
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::AudioControl;
using namespace chip::app::Clusters::AudioControl::Attributes;
using namespace chip::app::Clusters::AudioControl::Commands;
using namespace chip::Testing;
using chip::Protocols::InteractionModel::Status;
// ---------------------------------------------------------------------------
// Mock delegate
// ---------------------------------------------------------------------------
class MockAudioControlDelegate : public AudioControlDelegate
{
public:
// Configurable return value for the next delegate call.
Status nextStatus = Status::Success;
// Fixed hardware limits, queried once at construction.
uint16_t minDeviceVolume = 1;
uint16_t maxDeviceVolume = 100;
uint16_t maxDeviceVolumeDB = 0;
int16_t minCorrection = -10;
int16_t maxCorrection = 10;
uint16_t GetMinDeviceVolume() const override { return minDeviceVolume; }
uint16_t GetMaxDeviceVolume() const override { return maxDeviceVolume; }
uint16_t GetMaxDeviceVolumeDB() const override { return maxDeviceVolumeDB; }
int16_t GetMinCorrection() const override { return minCorrection; }
int16_t GetMaxCorrection() const override { return maxCorrection; }
// Call-tracking for OnStartup.
int startupCalls = 0;
AudioControlDelegate::StartupState startupState{};
// Call-tracking for HandleVolumeAndMuteChange.
int volumeAndMuteCalls = 0;
uint16_t lastNewVolume = 0;
bool lastNewSoftMuted = false;
// Call-tracking for BEQ handlers.
int bassChangedCalls = 0;
int midChangedCalls = 0;
int trebleChangedCalls = 0;
int16_t lastBass = 0;
int16_t lastMid = 0;
int16_t lastTreble = 0;
void Reset()
{
nextStatus = Status::Success;
startupCalls = 0;
startupState = {};
volumeAndMuteCalls = 0;
lastNewVolume = 0;
lastNewSoftMuted = false;
bassChangedCalls = 0;
midChangedCalls = 0;
trebleChangedCalls = 0;
lastBass = lastMid = lastTreble = 0;
}
void OnStartup(const AudioControlDelegate::StartupState & state) override
{
++startupCalls;
startupState = state;
}
Status HandleVolumeAndMuteChange(uint16_t newVolume, bool newSoftMuted) override
{
++volumeAndMuteCalls;
lastNewVolume = newVolume;
lastNewSoftMuted = newSoftMuted;
return nextStatus;
}
Status HandleBassChanged(int16_t bass) override
{
++bassChangedCalls;
lastBass = bass;
return nextStatus;
}
Status HandleMidChanged(int16_t mid) override
{
++midChangedCalls;
lastMid = mid;
return nextStatus;
}
Status HandleTrebleChanged(int16_t treble) override
{
++trebleChangedCalls;
lastTreble = treble;
return nextStatus;
}
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static AudioControlCluster::Config BasicConfig()
{
return AudioControlCluster::Config{}.WithInitialVolume(50).WithInitialDefaultStepSize(10);
}
// Sets a non-default [-5, 5] correction range on delegate and returns the matching Config.
static AudioControlCluster::Config BEQAllBandsConfig(MockAudioControlDelegate & delegate)
{
delegate.minCorrection = -5;
delegate.maxCorrection = 5;
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>().Set<Mid::Id>().Set<Treble::Id>();
return BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet);
}
// ---------------------------------------------------------------------------
// Test fixture
// ---------------------------------------------------------------------------
struct TestAudioControlCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
MockAudioControlDelegate mMockDelegate;
TestServerClusterContext testContext;
};
// ---------- Attribute list tests ----------
TEST_F(TestAudioControlCluster, AttributeListMandatoryOnly)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> attributes;
ASSERT_EQ(cluster.Attributes(ConcreteClusterPath(kRootEndpointId, AudioControl::Id), attributes), CHIP_NO_ERROR);
// No features, no optional attrs: mandatory + globals only.
ReadOnlyBufferBuilder<DataModel::AttributeEntry> expected;
AttributeListBuilder listBuilder(expected);
const AttributeListBuilder::OptionalAttributeEntry optionals[] = {
{ false, PhysicallyMuted::kMetadataEntry },
{ false, MaxDeviceVolumeDB::kMetadataEntry }, // optional bit not set
{ false, MaxUserVolume::kMetadataEntry },
{ false, StartUpMuted::kMetadataEntry },
{ false, StartUpVolume::kMetadataEntry },
{ false, Bass::kMetadataEntry },
{ false, Mid::kMetadataEntry },
{ false, Treble::kMetadataEntry },
{ false, MinCorrection::kMetadataEntry },
{ false, MaxCorrection::kMetadataEntry },
};
ASSERT_EQ(listBuilder.Append(Span(kMandatoryMetadata), Span(optionals)), CHIP_NO_ERROR);
ASSERT_TRUE(chip::Testing::EqualAttributeSets(attributes.TakeBuffer(), expected.TakeBuffer()));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, AttributeListWithOptionals)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>()
.Set<MaxDeviceVolumeDB::Id>()
.Set<MaxUserVolume::Id>()
.Set<StartUpMuted::Id>()
.Set<StartUpVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(80));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> attributes;
ASSERT_EQ(cluster.Attributes(ConcreteClusterPath(kRootEndpointId, AudioControl::Id), attributes), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> expected;
AttributeListBuilder listBuilder(expected);
const AttributeListBuilder::OptionalAttributeEntry expectedOptionals[] = {
{ true, PhysicallyMuted::kMetadataEntry },
{ true, MaxDeviceVolumeDB::kMetadataEntry },
{ true, MaxUserVolume::kMetadataEntry },
{ true, StartUpMuted::kMetadataEntry },
{ true, StartUpVolume::kMetadataEntry },
{ false, Bass::kMetadataEntry },
{ false, Mid::kMetadataEntry },
{ false, Treble::kMetadataEntry },
{ false, MinCorrection::kMetadataEntry },
{ false, MaxCorrection::kMetadataEntry },
};
ASSERT_EQ(listBuilder.Append(Span(kMandatoryMetadata), Span(expectedOptionals)), CHIP_NO_ERROR);
ASSERT_TRUE(chip::Testing::EqualAttributeSets(attributes.TakeBuffer(), expected.TakeBuffer()));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, AttributeListWithBEQFeature)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>().Set<Mid::Id>().Set<Treble::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> attributes;
ASSERT_EQ(cluster.Attributes(ConcreteClusterPath(kRootEndpointId, AudioControl::Id), attributes), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> expected;
AttributeListBuilder listBuilder(expected);
const AttributeListBuilder::OptionalAttributeEntry expectedOptionals[] = {
{ false, PhysicallyMuted::kMetadataEntry },
{ false, MaxDeviceVolumeDB::kMetadataEntry }, // optional bit not set (BEQ is unrelated to DB gating)
{ false, MaxUserVolume::kMetadataEntry },
{ false, StartUpMuted::kMetadataEntry },
{ false, StartUpVolume::kMetadataEntry },
{ true, Bass::kMetadataEntry },
{ true, Mid::kMetadataEntry },
{ true, Treble::kMetadataEntry },
{ true, MinCorrection::kMetadataEntry },
{ true, MaxCorrection::kMetadataEntry },
};
ASSERT_EQ(listBuilder.Append(Span(kMandatoryMetadata), Span(expectedOptionals)), CHIP_NO_ERROR);
ASSERT_TRUE(chip::Testing::EqualAttributeSets(attributes.TakeBuffer(), expected.TakeBuffer()));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, AttributeListDBFeatureHidesMaxDeviceVolumeDB)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kDecibel)));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> attributes;
ASSERT_EQ(cluster.Attributes(ConcreteClusterPath(kRootEndpointId, AudioControl::Id), attributes), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> expected;
AttributeListBuilder listBuilder(expected);
const AttributeListBuilder::OptionalAttributeEntry expectedOptionals[] = {
{ false, PhysicallyMuted::kMetadataEntry },
{ false, MaxDeviceVolumeDB::kMetadataEntry }, // DB feature IS set → hidden
{ false, MaxUserVolume::kMetadataEntry },
{ false, StartUpMuted::kMetadataEntry },
{ false, StartUpVolume::kMetadataEntry },
{ false, Bass::kMetadataEntry },
{ false, Mid::kMetadataEntry },
{ false, Treble::kMetadataEntry },
{ false, MinCorrection::kMetadataEntry },
{ false, MaxCorrection::kMetadataEntry },
};
ASSERT_EQ(listBuilder.Append(Span(kMandatoryMetadata), Span(expectedOptionals)), CHIP_NO_ERROR);
ASSERT_TRUE(chip::Testing::EqualAttributeSets(attributes.TakeBuffer(), expected.TakeBuffer()));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, MaxDeviceVolumeDBHiddenWhenOptionalBitNotSet)
{
// DB feature off, optional bit not set: MaxDeviceVolumeDB must not be exposed.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ReadOnlyBufferBuilder<DataModel::AttributeEntry> attributes;
ASSERT_EQ(cluster.Attributes(ConcreteClusterPath(kRootEndpointId, AudioControl::Id), attributes), CHIP_NO_ERROR);
auto buf = attributes.TakeBuffer();
for (const auto & entry : buf)
{
EXPECT_NE(entry.attributeId, MaxDeviceVolumeDB::Id);
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, BEQFeatureRequiresAtLeastOneBand)
{
// Minimum valid BEQ configuration: exactly one band in OptionalAttributeSet.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Treble::Id>(); // only Treble — satisfies the "at least one" constraint
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialTreble(3));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// The single configured band is readable.
int16_t treble{};
ASSERT_EQ(tester.ReadAttribute(Treble::Id, treble), CHIP_NO_ERROR);
EXPECT_EQ(treble, 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DBFeatureProhibitsMaxDeviceVolumeDBConfig)
{
// A ZAP config enabling both the DB feature and the MaxDeviceVolumeDB optional bit is invalid
// and fatal at construction (constructor VerifyOrDie); BasicConfig() never sets the bit, so this
// exercises the valid configuration instead.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kDecibel)));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
// MaxDeviceVolumeDB is absent from the attribute list when DB feature is active.
ReadOnlyBufferBuilder<DataModel::AttributeEntry> attributes;
ASSERT_EQ(cluster.Attributes(ConcreteClusterPath(kRootEndpointId, AudioControl::Id), attributes), CHIP_NO_ERROR);
auto buf = attributes.TakeBuffer();
for (const auto & entry : buf)
{
EXPECT_NE(entry.attributeId, MaxDeviceVolumeDB::Id);
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Read attribute tests ----------
TEST_F(TestAudioControlCluster, ReadMandatoryAttributes)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t revision{};
ASSERT_EQ(tester.ReadAttribute(ClusterRevision::Id, revision), CHIP_NO_ERROR);
EXPECT_EQ(revision, AudioControl::kRevision);
uint32_t featureMap{};
ASSERT_EQ(tester.ReadAttribute(FeatureMap::Id, featureMap), CHIP_NO_ERROR);
EXPECT_EQ(featureMap, 0u);
bool softMuted{};
ASSERT_EQ(tester.ReadAttribute(SoftMuted::Id, softMuted), CHIP_NO_ERROR);
EXPECT_FALSE(softMuted);
uint16_t volume{};
ASSERT_EQ(tester.ReadAttribute(Volume::Id, volume), CHIP_NO_ERROR);
EXPECT_EQ(volume, 50u);
uint16_t minVol{};
ASSERT_EQ(tester.ReadAttribute(MinDeviceVolume::Id, minVol), CHIP_NO_ERROR);
EXPECT_EQ(minVol, 1u);
uint16_t maxVol{};
ASSERT_EQ(tester.ReadAttribute(MaxDeviceVolume::Id, maxVol), CHIP_NO_ERROR);
EXPECT_EQ(maxVol, 100u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, ReadUnsupportedAttribute)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t dummy{};
EXPECT_NE(tester.ReadAttribute(0xFFFF, dummy), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- default-case coverage: bypass ClusterTester's AttributeList/AcceptedCommands
// pre-checks and call the cluster directly, since those checks otherwise short-circuit before ever
// reaching ReadAttribute()/WriteAttribute()/InvokeCommand()'s `default:` branches. ----------
TEST_F(TestAudioControlCluster, ReadAttributeDefaultCaseReturnsUnsupportedAttribute)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
chip::Testing::ReadOperation readOperation(kRootEndpointId, AudioControl::Id, 0xFFFF);
MockCommandHandler handler;
// OperationRequest stores subjectDescriptor by reference, so it must be a named local that
// outlives readOperation/encoder -- not a temporary passed directly to SetSubjectDescriptor().
const Access::SubjectDescriptor subjectDescriptor = handler.GetSubjectDescriptor();
readOperation.SetSubjectDescriptor(subjectDescriptor);
std::unique_ptr<AttributeValueEncoder> encoder = readOperation.StartEncoding();
auto status = cluster.ReadAttribute(readOperation.GetRequest(), *encoder);
EXPECT_FALSE(status.IsSuccess());
EXPECT_EQ(status.GetStatusCode().GetStatus(), Status::UnsupportedAttribute);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteAttributeDefaultCaseReturnsUnsupportedAttribute)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
chip::Testing::WriteOperation writeOp(kRootEndpointId, AudioControl::Id, 0xFFFF);
MockCommandHandler handler;
// OperationRequest stores subjectDescriptor by reference; must use a named local (see the
// matching comment in ReadAttributeDefaultCaseReturnsUnsupportedAttribute).
const Access::SubjectDescriptor subjectDescriptor = handler.GetSubjectDescriptor();
writeOp.SetSubjectDescriptor(subjectDescriptor);
auto decoder = writeOp.DecoderFor<uint16_t>(0);
auto status = cluster.WriteAttribute(writeOp.GetRequest(), decoder);
EXPECT_FALSE(status.IsSuccess());
EXPECT_EQ(status.GetStatusCode().GetStatus(), Status::UnsupportedAttribute);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, InvokeCommandDefaultCaseReturnsUnsupportedCommand)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
MockCommandHandler handler;
// OperationRequest stores subjectDescriptor by reference; must use a named local (see the
// matching comment in ReadAttributeDefaultCaseReturnsUnsupportedAttribute).
const Access::SubjectDescriptor subjectDescriptor = handler.GetSubjectDescriptor();
const DataModel::InvokeRequest invokeRequest({ kRootEndpointId, AudioControl::Id, 0xFFFF }, subjectDescriptor);
uint8_t buffer[16];
TLV::TLVWriter writer;
writer.Init(buffer);
TLV::TLVType outerContainerType;
ASSERT_EQ(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outerContainerType), CHIP_NO_ERROR);
ASSERT_EQ(writer.EndContainer(outerContainerType), CHIP_NO_ERROR);
ASSERT_EQ(writer.Finalize(), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buffer, writer.GetLengthWritten());
ASSERT_EQ(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()), CHIP_NO_ERROR);
auto result = cluster.InvokeCommand(invokeRequest, reader, &handler);
if (result.has_value())
{
EXPECT_FALSE(result->IsSuccess());
EXPECT_EQ(result->GetStatusCode().GetStatus(), Status::UnsupportedCommand);
}
else
{
FAIL();
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, ReadMaxDeviceVolumeDB)
{
mMockDelegate.maxDeviceVolumeDB = 1200;
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxDeviceVolumeDB::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t maxDeviceVolumeDB{};
ASSERT_EQ(tester.ReadAttribute(MaxDeviceVolumeDB::Id, maxDeviceVolumeDB), CHIP_NO_ERROR);
EXPECT_EQ(maxDeviceVolumeDB, 1200u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, ReadMaxUserVolume)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(70));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t maxUserVolume{};
ASSERT_EQ(tester.ReadAttribute(MaxUserVolume::Id, maxUserVolume), CHIP_NO_ERROR);
EXPECT_EQ(maxUserVolume, 70u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, ReadCorrectionRangeAttributes)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -8;
mMockDelegate.maxCorrection = 6;
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t minCorrection{};
ASSERT_EQ(tester.ReadAttribute(MinCorrection::Id, minCorrection), CHIP_NO_ERROR);
EXPECT_EQ(minCorrection, -8);
int16_t maxCorrection{};
ASSERT_EQ(tester.ReadAttribute(MaxCorrection::Id, maxCorrection), CHIP_NO_ERROR);
EXPECT_EQ(maxCorrection, 6);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Command: Mute / Unmute / ToggleMuted ----------
TEST_F(TestAudioControlCluster, MuteUnmuteToggle)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(Mute::Type()).IsSuccess());
EXPECT_TRUE(cluster.GetSoftMuted());
EXPECT_TRUE(tester.Invoke(Unmute::Type()).IsSuccess());
EXPECT_FALSE(cluster.GetSoftMuted());
EXPECT_TRUE(tester.Invoke(ToggleMuted::Type()).IsSuccess());
EXPECT_TRUE(cluster.GetSoftMuted());
EXPECT_TRUE(tester.Invoke(ToggleMuted::Type()).IsSuccess());
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Command: SetVolume ----------
TEST_F(TestAudioControlCluster, SetVolumeNotMuted)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// Set to valid volume while not muted → fast path
{
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 75u);
}
// Volume=0 → SoftMuted=TRUE
{
SetVolume::Type cmd;
cmd.newVolume = 0;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_TRUE(cluster.GetSoftMuted());
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetVolumeConstraintErrors)
{
// MinDeviceVolume=10, MaxDeviceVolume=100
mMockDelegate.minDeviceVolume = 10;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
AudioControlCluster::Config{}.WithInitialVolume(50).WithInitialDefaultStepSize(5));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// 1..9 are invalid (below MinDeviceVolume but not 0)
{
SetVolume::Type cmd;
cmd.newVolume = 1;
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
}
{
SetVolume::Type cmd;
cmd.newVolume = 9;
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
}
// 0 is valid (mutes)
{
SetVolume::Type cmd;
cmd.newVolume = 0;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
}
// MinDeviceVolume is valid
{
SetVolume::Type cmd;
cmd.newVolume = 10;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 10u);
}
// MaxDeviceVolume is valid
{
SetVolume::Type cmd;
cmd.newVolume = 100;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 100u);
}
// Above MaxDeviceVolume is invalid
{
SetVolume::Type cmd;
cmd.newVolume = 101;
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- UnmutePolicy table tests ----------
TEST_F(TestAudioControlCluster, UnmutePolicyUnmuteOrChangeVolume)
{
// SoftMuted=TRUE, PhysicallyMuted=FALSE, policy=UnmuteOrChangeVolume
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(50).WithSetVolumeUnmutePolicy(
UnmutePolicyEnum::kUnmuteOrChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 75u);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, UnmutePolicyDoNotUnmuteAndDoNotChangeVolume)
{
// SoftMuted=TRUE, policy=DoNotUnmuteAndDoNotChangeVolume → INVALID_IN_STATE
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(50).WithSetVolumeUnmutePolicy(
UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
// State must be unchanged
EXPECT_EQ(cluster.GetVolume(), 50u);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, UnmutePolicyDoNotUnmuteAndChangeVolume)
{
// SoftMuted=TRUE, policy=DoNotUnmuteAndChangeVolume → volume changes, stays muted
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(50).WithSetVolumeUnmutePolicy(
UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 75u);
EXPECT_TRUE(cluster.GetSoftMuted()); // still muted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, UnmutePolicyUnmuteOrDoNotChangeVolumeSoftMutedOnly)
{
// SoftMuted=TRUE, PhysicallyMuted=FALSE, policy=UnmuteOrDoNotChangeVolume
// -> unmutes and applies the requested volume
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(50).WithSetVolumeUnmutePolicy(
UnmutePolicyEnum::kUnmuteOrDoNotChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 75u);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, UnmutePolicyUnmuteOrDoNotChangeVolumePhysicallyMuted)
{
// PhysicallyMuted=TRUE, policy=UnmuteOrDoNotChangeVolume → INVALID_IN_STATE
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithInitialSoftMuted(false)
.WithInitialVolume(50)
.WithOptionalAttributes(optionalSet)
.WithSetVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrDoNotChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetVolumePhysicallyMutedUnmuteOrChangeVolume)
{
// PhysicallyMuted=TRUE, SoftMuted=FALSE, policy=kUnmuteOrChangeVolume
// → volume changes and SoftMuted is cleared (physical mute is hardware-managed)
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithInitialSoftMuted(false)
.WithInitialVolume(50)
.WithOptionalAttributes(optionalSet)
.WithSetVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 75u);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetVolumePhysicallyMutedDoNotUnmuteAndChangeVolume)
{
// PhysicallyMuted=TRUE, SoftMuted=TRUE, policy=kDoNotUnmuteAndChangeVolume
// → volume changes, SoftMuted stays true
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithInitialSoftMuted(true)
.WithInitialVolume(50)
.WithOptionalAttributes(optionalSet)
.WithSetVolumeUnmutePolicy(UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 75u);
EXPECT_TRUE(cluster.GetSoftMuted()); // DoNotUnmute: soft-mute preserved
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Command: IncreaseVolume ----------
TEST_F(TestAudioControlCluster, IncreaseVolumeNotMuted)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// Default step (10): 50 → 60
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 60u);
// Explicit step 20: 60 → 80
IncreaseVolume::Type cmd;
cmd.stepSize.SetValue(20);
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 80u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumeCapsAtEffectiveMax)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(95).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// 95 + 10 = 105 > 100 → capped at 100
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 100u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumeSoftMutedUnmuteOrChangeWithVolumePlusStep)
{
// SoftMuted=TRUE, PhysicallyMuted=FALSE, policy=UnmuteOrChangeVolume, unmuteVolume=kVolumePlusStepSize
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithInitialSoftMuted(true)
.WithInitialVolume(40)
.WithInitialDefaultStepSize(10)
.WithIncreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume)
.WithIncreaseVolumeUnmuteVolume(UnmuteVolumeEnum::kVolumePlusStepSize));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// RequestedVolume = Volume + StepSize = 40 + 10 = 50; policy unmutes
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumeSoftMutedUnmuteOrChangeWithMinDeviceVolume)
{
mMockDelegate.minDeviceVolume = 5;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
AudioControlCluster::Config{}
.WithInitialVolume(40)
.WithInitialDefaultStepSize(10)
.WithInitialSoftMuted(true)
.WithIncreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume)
.WithIncreaseVolumeUnmuteVolume(UnmuteVolumeEnum::kMinDeviceVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 5u); // MinDeviceVolume
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumeSoftMutedUnmuteOrChangeWithVolume)
{
// unmuteVolume=kVolume: requestedVolume is set to the current Volume (unchanged)
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithInitialSoftMuted(true)
.WithInitialVolume(40)
.WithInitialDefaultStepSize(10)
.WithIncreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume)
.WithIncreaseVolumeUnmuteVolume(UnmuteVolumeEnum::kVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 40u); // volume stays the same
EXPECT_FALSE(cluster.GetSoftMuted()); // unmuted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumeStepSizeConstraintError)
{
// StepSize is only constrained to be >= 1; an over-large StepSize is clamped against
// EffectiveMax rather than rejected.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
IncreaseVolume::Type cmd;
cmd.stepSize.SetValue(0);
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // unchanged
cmd.stepSize.SetValue(100); // effectiveMax(100) - minVol(1) == 99, so this overshoots
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 100u); // clamped to EffectiveMax, not rejected
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumePhysicallyMutedOnlyUsesVolumeStepSize)
{
// PhysicallyMuted=TRUE, SoftMuted=FALSE: the guard (mSoftMuted && !mPhysicallyMuted) is false,
// so the unmuteVolume enum is skipped entirely and requestedVolume = Volume + StepSize.
// Here unmuteVolume=kMinDeviceVolume which would give 1 if applied — proves it is not used.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
AudioControlCluster::Config{}
.WithInitialVolume(40)
.WithInitialDefaultStepSize(10)
.WithInitialSoftMuted(false)
.WithOptionalAttributes(optionalSet)
.WithIncreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume)
.WithIncreaseVolumeUnmuteVolume(UnmuteVolumeEnum::kMinDeviceVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // 40 + 10, NOT kMinDeviceVolume (1)
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumeBothMutedIgnoresUnmuteVolumeEnum)
{
// SoftMuted=TRUE, PhysicallyMuted=TRUE: guard (mSoftMuted && !mPhysicallyMuted) is still false,
// so unmuteVolume enum is skipped even though SoftMuted is true.
// requestedVolume = Volume + StepSize, then policy applied.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
AudioControlCluster::Config{}
.WithInitialVolume(40)
.WithInitialDefaultStepSize(10)
.WithInitialSoftMuted(true)
.WithOptionalAttributes(optionalSet)
.WithIncreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume)
.WithIncreaseVolumeUnmuteVolume(UnmuteVolumeEnum::kMinDeviceVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // 40 + 10, NOT kMinDeviceVolume (1)
EXPECT_FALSE(cluster.GetSoftMuted()); // kUnmuteOrChangeVolume clears SoftMuted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Command: DecreaseVolume ----------
TEST_F(TestAudioControlCluster, DecreaseVolumeNotMuted)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(DecreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 40u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DecreaseVolumeBelowMinMutesDevice)
{
// MinDeviceVolume=10, Volume=15, Step=10 → RequestedVolume=5 < 10 → floor at Min and mute
mMockDelegate.minDeviceVolume = 10;
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
AudioControlCluster::Config{}.WithInitialVolume(15).WithInitialSoftMuted(false).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(DecreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 10u); // clamped to MinDeviceVolume
EXPECT_TRUE(cluster.GetSoftMuted()); // auto-muted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DecreaseVolumeSoftMutedPolicyPath)
{
// SoftMuted=true, Volume=50, Step=10 → requestedVolume=40 >= MinDeviceVolume → ApplyUnmutePolicy
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithInitialSoftMuted(true)
.WithInitialVolume(50)
.WithInitialDefaultStepSize(10)
.WithDecreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(DecreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 40u);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DecreaseVolumeStepSizeConstraintError)
{
// StepSize is only constrained to be >= 1; an over-large StepSize floors at MinDeviceVolume
// (muting the device) rather than being rejected.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
DecreaseVolume::Type cmd;
cmd.stepSize.SetValue(0);
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // unchanged
cmd.stepSize.SetValue(100); // overshoots down to below MinDeviceVolume
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 1u); // floored at MinDeviceVolume
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DecreaseVolumePhysicallyMutedPolicyPath)
{
// PhysicallyMuted=TRUE, SoftMuted=FALSE, Volume=50, Step=10 → requestedVolume=40 >= min
// Fast-path guard (!mSoftMuted && !mPhysicallyMuted) is false → goes through policy.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithInitialSoftMuted(false)
.WithInitialVolume(50)
.WithOptionalAttributes(optionalSet)
.WithDecreaseVolumeUnmutePolicy(UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(tester.Invoke(DecreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 40u);
EXPECT_FALSE(cluster.GetSoftMuted()); // kDoNotUnmuteAndChangeVolume leaves SoftMuted unchanged (was false)
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Write attribute tests ----------
TEST_F(TestAudioControlCluster, WriteDefaultStepSize)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialDefaultStepSize(1));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t newStep = 20;
EXPECT_EQ(tester.WriteAttribute(DefaultStepSize::Id, newStep), CHIP_NO_ERROR);
uint16_t readback{};
ASSERT_EQ(tester.ReadAttribute(DefaultStepSize::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 20u);
// 0 is out of range
newStep = 0;
EXPECT_NE(tester.WriteAttribute(DefaultStepSize::Id, newStep), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteMaxUserVolumeClampsVolume)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithOptionalAttributes(optionalSet)
.WithInitialMaxUserVolume(100)
.WithInitialVolume(90)
.WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// Writing MaxUserVolume=70 should clamp Volume from 90 → 70
uint16_t newMax = 70;
EXPECT_EQ(tester.WriteAttribute(MaxUserVolume::Id, newMax), CHIP_NO_ERROR);
uint16_t volume{};
ASSERT_EQ(tester.ReadAttribute(Volume::Id, volume), CHIP_NO_ERROR);
EXPECT_EQ(volume, 70u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteStartUpMuted)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// Write non-null value
bool val = true;
EXPECT_EQ(tester.WriteAttribute(StartUpMuted::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
DataModel::Nullable<bool> readback{};
ASSERT_EQ(tester.ReadAttribute(StartUpMuted::Id, readback), CHIP_NO_ERROR);
ASSERT_FALSE(readback.IsNull());
EXPECT_TRUE(readback.Value());
// Write null
DataModel::Nullable<bool> nullVal;
nullVal.SetNull();
EXPECT_EQ(tester.WriteAttribute(StartUpMuted::Id, nullVal), CHIP_NO_ERROR);
ASSERT_EQ(tester.ReadAttribute(StartUpMuted::Id, readback), CHIP_NO_ERROR);
EXPECT_TRUE(readback.IsNull());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteBassValidatesRange)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 3;
EXPECT_EQ(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR);
// Out of range
val = 6;
EXPECT_NE(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR);
val = -6;
EXPECT_NE(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteMidValidatesRange)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Mid::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 3;
EXPECT_EQ(tester.WriteAttribute(Mid::Id, val), CHIP_NO_ERROR);
val = 6;
EXPECT_NE(tester.WriteAttribute(Mid::Id, val), CHIP_NO_ERROR);
val = -6;
EXPECT_NE(tester.WriteAttribute(Mid::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteTrebleValidatesRange)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Treble::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 3;
EXPECT_EQ(tester.WriteAttribute(Treble::Id, val), CHIP_NO_ERROR);
val = 6;
EXPECT_NE(tester.WriteAttribute(Treble::Id, val), CHIP_NO_ERROR);
val = -6;
EXPECT_NE(tester.WriteAttribute(Treble::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Write no-op behavior ----------
//
// Writing the same value a second time must not re-persist to KVS, and for Bass/Mid/Treble
// must not invoke the corresponding delegate callback either. Nothing here needs to be
// distinguishable via the write's returned status (see PR discussion): every case below
// returns plain CHIP_NO_ERROR whether or not the value actually changed.
TEST_F(TestAudioControlCluster, WriteSameValueDefaultStepSize)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t val = 20;
ASSERT_EQ(tester.WriteAttribute(DefaultStepSize::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(DefaultStepSize::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteSameValueMaxUserVolume)
{
// MaxUserVolume goes through the shared app-facing SetMaxUserVolume() (CHIP_ERROR-typed), which
// has its own internal no-op check (via SetAttributeValue) -- nothing is re-persisted and the
// delegate is not spuriously invoked, even though WriteAttribute doesn't pre-check itself.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(80));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t val = 60;
ASSERT_EQ(tester.WriteAttribute(MaxUserVolume::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(MaxUserVolume::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMaxUserVolume(), val);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteSameValueSetVolumeUnmutePolicy)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
auto val = UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume;
ASSERT_EQ(tester.WriteAttribute(SetVolumeUnmutePolicy::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(SetVolumeUnmutePolicy::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteSameValueIncreaseVolumeUnmutePolicy)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
auto val = UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume;
ASSERT_EQ(tester.WriteAttribute(IncreaseVolumeUnmutePolicy::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(IncreaseVolumeUnmutePolicy::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteSameValueIncreaseVolumeUnmuteVolume)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
auto val = UnmuteVolumeEnum::kMinDeviceVolume;
ASSERT_EQ(tester.WriteAttribute(IncreaseVolumeUnmuteVolume::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(IncreaseVolumeUnmuteVolume::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteSameValueDecreaseVolumeUnmutePolicy)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
auto val = UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume;
ASSERT_EQ(tester.WriteAttribute(DecreaseVolumeUnmutePolicy::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(DecreaseVolumeUnmutePolicy::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteNoOpBassDoesNotCallDelegate)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 3;
ASSERT_EQ(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR); // first write changes 0→3
mMockDelegate.Reset();
EXPECT_EQ(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR); // same value: no-op
EXPECT_EQ(mMockDelegate.bassChangedCalls, 0); // delegate must not be called on no-op
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteNoOpMidDoesNotCallDelegate)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 3;
ASSERT_EQ(tester.WriteAttribute(Mid::Id, val), CHIP_NO_ERROR);
mMockDelegate.Reset();
EXPECT_EQ(tester.WriteAttribute(Mid::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.midChangedCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteNoOpTrebleDoesNotCallDelegate)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 3;
ASSERT_EQ(tester.WriteAttribute(Treble::Id, val), CHIP_NO_ERROR);
mMockDelegate.Reset();
EXPECT_EQ(tester.WriteAttribute(Treble::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.trebleChangedCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteSameValueStartUpMuted)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// null initial value: writing null is immediately a no-op
DataModel::Nullable<bool> nullVal;
nullVal.SetNull();
EXPECT_EQ(tester.WriteAttribute(StartUpMuted::Id, nullVal), CHIP_NO_ERROR);
// write a non-null value, then the same value again
bool val = true;
ASSERT_EQ(tester.WriteAttribute(StartUpMuted::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(StartUpMuted::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, WriteSameValueStartUpVolume)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// null initial value: writing null is immediately a no-op
DataModel::Nullable<uint16_t> nullVal;
nullVal.SetNull();
EXPECT_EQ(tester.WriteAttribute(StartUpVolume::Id, nullVal), CHIP_NO_ERROR);
// write a non-null value, then the same value again
uint16_t val = 50;
ASSERT_EQ(tester.WriteAttribute(StartUpVolume::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
EXPECT_EQ(tester.WriteAttribute(StartUpVolume::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Application-facing API ----------
TEST_F(TestAudioControlCluster, SetVolumeAPI)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.SetVolume(80), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 80u);
// Out of range (0 is not valid for the API since it doesn't implement the "mute" semantic)
EXPECT_NE(cluster.SetVolume(0), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetVolume(200), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 80u); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Application-facing API does NOT call delegate ----------
TEST_F(TestAudioControlCluster, ApplicationSettersDoNotCallDelegate)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetVolume(80), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetSoftMuted(true), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetSoftMuted(false), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetSoftMutedAPI)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
ASSERT_EQ(cluster.SetSoftMuted(true), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
bool readback{};
ASSERT_EQ(tester.ReadAttribute(SoftMuted::Id, readback), CHIP_NO_ERROR);
EXPECT_TRUE(readback);
ASSERT_EQ(cluster.SetSoftMuted(false), CHIP_NO_ERROR);
EXPECT_FALSE(cluster.GetSoftMuted());
ASSERT_EQ(tester.ReadAttribute(SoftMuted::Id, readback), CHIP_NO_ERROR);
EXPECT_FALSE(readback);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Delegate callback verification ----------
TEST_F(TestAudioControlCluster, DelegateMuteCallbackParameters)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(42));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.Reset();
EXPECT_TRUE(tester.Invoke(Mute::Type()).IsSuccess());
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 1);
EXPECT_EQ(mMockDelegate.lastNewVolume, 42u);
EXPECT_TRUE(mMockDelegate.lastNewSoftMuted);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateUnmuteCallbackParameters)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(42));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.Reset();
EXPECT_TRUE(tester.Invoke(Unmute::Type()).IsSuccess());
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 1);
EXPECT_EQ(mMockDelegate.lastNewVolume, 42u);
EXPECT_FALSE(mMockDelegate.lastNewSoftMuted);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateSetVolumeCallbackParameters)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.Reset();
SetVolume::Type cmd;
cmd.newVolume = 70;
EXPECT_TRUE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 1);
EXPECT_EQ(mMockDelegate.lastNewVolume, 70u);
EXPECT_FALSE(mMockDelegate.lastNewSoftMuted);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateBassChangedCallback)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.Reset();
int16_t val = 3;
EXPECT_EQ(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.bassChangedCalls, 1);
EXPECT_EQ(mMockDelegate.lastBass, 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Delegate veto tests: state must be unchanged on non-Success ----------
TEST_F(TestAudioControlCluster, DelegateMuteVetoLeavesStateUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_FALSE(tester.Invoke(Mute::Type()).IsSuccess());
// SoftMuted must remain false
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateUnmuteVetoLeavesStateUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_FALSE(tester.Invoke(Unmute::Type()).IsSuccess());
// SoftMuted must remain true
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateToggleMutedVetoLeavesStateUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_FALSE(tester.Invoke(ToggleMuted::Type()).IsSuccess());
// SoftMuted must remain false
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateSetVolumeVetoLeavesStateUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
// Volume and mute state must be unchanged
EXPECT_EQ(cluster.GetVolume(), 50u);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateSetVolumeMuteZeroVetoLeavesStateUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
SetVolume::Type cmd;
cmd.newVolume = 0; // special mute path
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
EXPECT_FALSE(cluster.GetSoftMuted()); // still unmuted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateIncreaseVolumeVetoLeavesStateUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_FALSE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateDecreaseVolumeVetoLeavesStateUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_FALSE(tester.Invoke(DecreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // unchanged
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateIncreaseVolumePolicyPathVetoLeavesStateUnchanged)
{
// Start muted so IncreaseVolume goes through ApplyUnmutePolicy, not the fast path.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(50).WithInitialDefaultStepSize(10));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_FALSE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // unchanged
EXPECT_TRUE(cluster.GetSoftMuted()); // still muted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateBassVetoLeavesStateUnchanged)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialBass(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
int16_t val = 3;
EXPECT_NE(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR);
// Bass must remain 0
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Bass::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateMidVetoLeavesStateUnchanged)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Mid::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialMid(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
int16_t val = 3;
EXPECT_NE(tester.WriteAttribute(Mid::Id, val), CHIP_NO_ERROR);
// Mid must remain 0
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Mid::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DelegateTrebleVetoLeavesStateUnchanged)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Treble::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialTreble(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
mMockDelegate.nextStatus = Status::Busy;
int16_t val = 3;
EXPECT_NE(tester.WriteAttribute(Treble::Id, val), CHIP_NO_ERROR);
// Treble must remain 0
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Treble::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Persistence ----------
// Each test starts a cluster, modifies state, shuts down, restarts with the same testContext
// (same backing storage), and verifies the state was restored.
TEST_F(TestAudioControlCluster, PersistenceSoftMuted)
{
// StartUpMuted must be in the optional set so SoftMuted is loaded from KVS on restart.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetSoftMuted(true), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceVolume)
{
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetVolume(80), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 80u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceMaxUserVolumeAndVolumeClip)
{
// SetMaxUserVolume(70) clips Volume 90→70; both values must survive a restart.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
auto cfg = BasicConfig()
.WithOptionalAttributes(optionalSet)
.WithInitialMaxUserVolume(100)
.WithInitialVolume(90)
.WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMaxUserVolume(70), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMaxUserVolume(), 70u);
EXPECT_EQ(cluster.GetVolume(), 70u);
// Persisted MaxUserVolume must constrain SetVolume: 75 > 70 must fail.
EXPECT_NE(cluster.SetVolume(75), CHIP_NO_ERROR);
EXPECT_EQ(cluster.SetVolume(65), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceStartUpVolumeClipOnSetMaxUserVolume)
{
// StartUpVolume=80 written, then MaxUserVolume lowered to 60 clips StartUpVolume to 60.
// On next boot, Volume must be 60 (from the clipped StartUpVolume).
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>().Set<StartUpVolume::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100).WithInitialVolume(50);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t val = 80;
ASSERT_EQ(tester.WriteAttribute(StartUpVolume::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMaxUserVolume(60), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 60u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceStartUpMuted)
{
// WriteAttribute StartUpMuted=true is persisted; on next boot SoftMuted must be true.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
bool val = true;
ASSERT_EQ(tester.WriteAttribute(StartUpMuted::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceStartUpVolume)
{
// WriteAttribute StartUpVolume=75 is persisted; on next boot Volume must be 75.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpVolume::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialVolume(50);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t val = 75;
ASSERT_EQ(tester.WriteAttribute(StartUpVolume::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 75u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceBass)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg = BasicConfig()
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialBass(0);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 3;
ASSERT_EQ(tester.WriteAttribute(Bass::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Bass::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceMid)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Mid::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg = BasicConfig()
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialMid(0);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = -2;
ASSERT_EQ(tester.WriteAttribute(Mid::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Mid::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, -2);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceTreble)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Treble::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg = BasicConfig()
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialTreble(0);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t val = 5;
ASSERT_EQ(tester.WriteAttribute(Treble::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Treble::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 5);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// ---------- Command persistence ----------
// Verify every InvokeCommand path that calls Store*() actually survives a restart.
TEST_F(TestAudioControlCluster, PersistenceMuteCommand)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialSoftMuted(false).WithInitialVolume(50);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_TRUE(ClusterTester(cluster).Invoke(Mute::Type{}).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceUnmuteCommand)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialSoftMuted(true).WithInitialVolume(50);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_TRUE(ClusterTester(cluster).Invoke(Unmute::Type{}).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceToggleMutedCommand)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialSoftMuted(false).WithInitialVolume(50);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_TRUE(ClusterTester(cluster).Invoke(ToggleMuted::Type{}).IsSuccess()); // false → true
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceSetVolumeCommandFastPath)
{
// Unmuted fast path → StoreVolume().
auto cfg = BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
SetVolume::Type cmd;
cmd.newVolume = 75;
ASSERT_TRUE(ClusterTester(cluster).Invoke(cmd).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 75u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceSetVolumeCommandMuteZero)
{
// Volume=0 path → StoreSoftMuted(true).
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialSoftMuted(false).WithInitialVolume(50);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
SetVolume::Type cmd;
cmd.newVolume = 0;
ASSERT_TRUE(ClusterTester(cluster).Invoke(cmd).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceSetVolumeCommandPolicyPath)
{
// Muted + policy path → ApplyUnmutePolicy → StoreVolume() + StoreSoftMuted().
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig()
.WithOptionalAttributes(optionalSet)
.WithInitialSoftMuted(true)
.WithInitialVolume(50)
.WithSetVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
SetVolume::Type cmd;
cmd.newVolume = 75;
ASSERT_TRUE(ClusterTester(cluster).Invoke(cmd).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 75u);
EXPECT_FALSE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceIncreaseVolumeCommand)
{
// Fast path → StoreVolume().
auto cfg = BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_TRUE(ClusterTester(cluster).Invoke(IncreaseVolume::Type{}).IsSuccess()); // 50 → 60
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 60u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceDecreaseVolumeCommand)
{
// Fast path → StoreVolume().
auto cfg = BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50).WithInitialDefaultStepSize(10);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_TRUE(ClusterTester(cluster).Invoke(DecreaseVolume::Type{}).IsSuccess()); // 50 → 40
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 40u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceDecreaseVolumeFloorCommand)
{
// Floor path → StoreVolume(MinDeviceVolume) + StoreSoftMuted(true).
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
mMockDelegate.minDeviceVolume = 10;
auto cfg = AudioControlCluster::Config{}
.WithOptionalAttributes(optionalSet)
.WithInitialVolume(15)
.WithInitialSoftMuted(false)
.WithInitialDefaultStepSize(10);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
// 15 - 10 = 5 < MinDeviceVolume(10) → floor
ASSERT_TRUE(ClusterTester(cluster).Invoke(DecreaseVolume::Type{}).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 10u);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// ---------- Startup: null StartUp* retains last persisted state ----------
TEST_F(TestAudioControlCluster, StartupNullStartUpMutedRetainsPersistedSoftMuted)
{
// SoftMuted=true is persisted via Mute command; StartUpMuted=null means
// "use last persisted state", so SoftMuted must remain true after restart.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
ASSERT_TRUE(tester.Invoke(Mute::Type{}).IsSuccess()); // persists SoftMuted=true
DataModel::Nullable<bool> nullVal;
nullVal.SetNull();
ASSERT_TRUE(tester.WriteAttribute(StartUpMuted::Id, nullVal).IsSuccess()); // null→null: no-op, but still succeeds
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted()); // retained, not overridden by null StartUpMuted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, StartupNullStartUpVolumeRetainsPersistedVolume)
{
// Volume=80 is persisted via command; StartUpVolume=null means "use last persisted Volume".
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpVolume::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialVolume(50).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 80;
ASSERT_TRUE(tester.Invoke(cmd).IsSuccess()); // persists Volume=80
DataModel::Nullable<uint16_t> nullVal;
nullVal.SetNull();
ASSERT_TRUE(tester.WriteAttribute(StartUpVolume::Id, nullVal).IsSuccess()); // null→null: no-op, but still succeeds
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 80u); // retained, not overridden by null StartUpVolume
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// ---------- App-facing API: SetMaxUserVolume ----------
TEST_F(TestAudioControlCluster, SetMaxUserVolumeConstraintBelowMin)
{
// MinDeviceVolume=1 (BasicConfig default); 0 is below the minimum.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetMaxUserVolume(0), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMaxUserVolume(), 100u); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMaxUserVolumeConstraintAboveMaxDevice)
{
// MaxDeviceVolume=100 (BasicConfig default); 101 is above the device ceiling.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetMaxUserVolume(101), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMaxUserVolume(), 100u); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMaxUserVolumeClipsStartUpVolumeInMemory)
{
// After SetMaxUserVolume clips StartUpVolume, ReadAttribute in the same session
// must reflect the clipped in-memory value, not the original write.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>().Set<StartUpVolume::Id>();
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t startupVol = 80;
ASSERT_EQ(tester.WriteAttribute(StartUpVolume::Id, DataModel::MakeNullable(startupVol)), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMaxUserVolume(60), CHIP_NO_ERROR);
DataModel::Nullable<uint16_t> readback{};
ASSERT_EQ(tester.ReadAttribute(StartUpVolume::Id, readback), CHIP_NO_ERROR);
ASSERT_FALSE(readback.IsNull());
EXPECT_EQ(readback.Value(), 60u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMaxUserVolumeDelegateArgsWhenSoftMuted)
{
// When Volume is clipped while SoftMuted=true, delegate receives (newMax, true).
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithOptionalAttributes(optionalSet)
.WithInitialMaxUserVolume(100)
.WithInitialVolume(90)
.WithInitialSoftMuted(true));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetMaxUserVolume(70), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 1);
EXPECT_EQ(mMockDelegate.lastNewVolume, 70u);
EXPECT_TRUE(mMockDelegate.lastNewSoftMuted);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetVolumeRespectEffectiveMaxAfterSetMaxUserVolume)
{
// After SetMaxUserVolume(70) lowers EffectiveMax, SetVolume(75) must be rejected
// in the same session — no restart needed.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(
kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMaxUserVolume(70), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetVolume(75), CHIP_NO_ERROR); // 75 > EffectiveMax=70
EXPECT_EQ(cluster.SetVolume(65), CHIP_NO_ERROR); // 65 ≤ EffectiveMax=70
EXPECT_EQ(cluster.GetVolume(), 65u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- App-facing API: optional-attribute-absent guard ----------
TEST_F(TestAudioControlCluster, SetPhysicallyMutedAbsentReturnsError)
{
// PhysicallyMuted not in optional set → setter must fail, no state change, no notification.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
EXPECT_FALSE(cluster.GetPhysicallyMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMaxUserVolumeAbsentReturnsError)
{
// MaxUserVolume not in optional set → setter must fail.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetMaxUserVolume(70), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, MaxUserVolumeIgnoredWhenAttributeAbsent)
{
// MaxUserVolume not in optional set: whether or not WithInitialMaxUserVolume was called,
// EffectiveMaxVolume() must fall back to MaxDeviceVolume — the OptionalAttributeSet, not a
// zero-vs-nonzero config value, is the source of truth for whether MaxUserVolume applies.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50).WithInitialMaxUserVolume(70));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMaxUserVolume(), 100u); // == MaxDeviceVolume from BasicConfig(), not 70
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetBassAbsentReturnsError)
{
// Bass not in optional set (BEQ feature present, but only Mid enabled) → setter must fail.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Mid::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg =
AudioControlCluster::Config{}.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetBass(2), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetBass(), 0); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMidAbsentReturnsError)
{
// Mid not in optional set (BEQ feature present, only Bass enabled) → setter must fail.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg =
AudioControlCluster::Config{}.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetMid(2), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMid(), 0); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetTrebleAbsentReturnsError)
{
// Treble not in optional set (BEQ feature present, only Bass enabled) → setter must fail.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg =
AudioControlCluster::Config{}.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer)).WithOptionalAttributes(optionalSet);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetTreble(2), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetTreble(), 0); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetBassFeatureAbsentReturnsError)
{
// BEQ feature not enabled → SetBass/SetMid/SetTreble must all fail.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetBass(1), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetMid(1), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetTreble(1), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- App-facing API: SetPhysicallyMuted ----------
TEST_F(TestAudioControlCluster, SetPhysicallyMutedBasic)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
ASSERT_FALSE(cluster.GetPhysicallyMuted());
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetPhysicallyMuted());
bool readback{};
ASSERT_EQ(tester.ReadAttribute(PhysicallyMuted::Id, readback), CHIP_NO_ERROR);
EXPECT_TRUE(readback);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, PhysicallyMutedSeededFromConfig)
{
// The hardware knows its physical mute state at boot (e.g. a mute switch position), so
// WithInitialPhysicallyMuted() must take effect immediately — no SetPhysicallyMuted() call
// needed after Startup().
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialPhysicallyMuted(true));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_TRUE(cluster.GetPhysicallyMuted());
bool readback{};
ASSERT_EQ(tester.ReadAttribute(PhysicallyMuted::Id, readback), CHIP_NO_ERROR);
EXPECT_TRUE(readback);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetPhysicallyMutedDoesNotCallDelegate)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithOptionalAttributes(optionalSet));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- App-facing API: SetBass / SetMid / SetTreble ----------
TEST_F(TestAudioControlCluster, SetBassAPI)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate).WithInitialBass(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_EQ(cluster.SetBass(3), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetBass(), 3);
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Bass::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetBassConstraintError)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate).WithInitialBass(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetBass(-6), CHIP_NO_ERROR); // below min
EXPECT_NE(cluster.SetBass(6), CHIP_NO_ERROR); // above max
EXPECT_EQ(cluster.GetBass(), 0); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetBassDoesNotCallDelegate)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetBass(2), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.bassChangedCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetBassPersistence)
{
auto cfg = BEQAllBandsConfig(mMockDelegate).WithInitialBass(0);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetBass(4), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetBass(), 4);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, SetMidAPI)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate).WithInitialMid(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_EQ(cluster.SetMid(-3), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMid(), -3);
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Mid::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, -3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMidConstraintError)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate).WithInitialMid(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetMid(-6), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetMid(6), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMid(), 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMidDoesNotCallDelegate)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetMid(-1), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.midChangedCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMidPersistence)
{
auto cfg = BEQAllBandsConfig(mMockDelegate).WithInitialMid(0);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMid(-2), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMid(), -2);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, SetTrebleAPI)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate).WithInitialTreble(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
EXPECT_EQ(cluster.SetTreble(5), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetTreble(), 5);
int16_t readback{};
ASSERT_EQ(tester.ReadAttribute(Treble::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 5);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetTrebleConstraintError)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate).WithInitialTreble(0));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetTreble(-6), CHIP_NO_ERROR);
EXPECT_NE(cluster.SetTreble(6), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetTreble(), 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetTrebleDoesNotCallDelegate)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetTreble(1), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.trebleChangedCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetTreblePersistence)
{
auto cfg = BEQAllBandsConfig(mMockDelegate).WithInitialTreble(0);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetTreble(5), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetTreble(), 5);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// ---------- Non-persisted attributes ----------
// Explicit tests confirming intentionally non-persisted attributes revert to config defaults.
TEST_F(TestAudioControlCluster, PhysicallyMutedNotPersisted)
{
// PhysicallyMuted is not persisted because hardware re-reports it on each boot.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<PhysicallyMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetPhysicallyMuted(true), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_FALSE(cluster.GetPhysicallyMuted()); // reset; hardware will re-report
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceDefaultStepSize)
{
auto cfg = BasicConfig().WithInitialDefaultStepSize(10);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t newStep = 20;
ASSERT_EQ(tester.WriteAttribute(DefaultStepSize::Id, newStep), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t readback{};
ASSERT_EQ(tester.ReadAttribute(DefaultStepSize::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, 20u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceSetVolumeUnmutePolicy)
{
auto cfg = BasicConfig().WithSetVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmutePolicyEnum newPolicy = UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume;
ASSERT_EQ(tester.WriteAttribute(SetVolumeUnmutePolicy::Id, newPolicy), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmutePolicyEnum readback{};
ASSERT_EQ(tester.ReadAttribute(SetVolumeUnmutePolicy::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceIncreaseVolumeUnmutePolicy)
{
auto cfg = BasicConfig().WithIncreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmutePolicyEnum newPolicy = UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume;
ASSERT_EQ(tester.WriteAttribute(IncreaseVolumeUnmutePolicy::Id, newPolicy), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmutePolicyEnum readback{};
ASSERT_EQ(tester.ReadAttribute(IncreaseVolumeUnmutePolicy::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceIncreaseVolumeUnmuteVolume)
{
auto cfg = BasicConfig().WithIncreaseVolumeUnmuteVolume(UnmuteVolumeEnum::kVolumePlusStepSize);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmuteVolumeEnum newVal = UnmuteVolumeEnum::kMinDeviceVolume;
ASSERT_EQ(tester.WriteAttribute(IncreaseVolumeUnmuteVolume::Id, newVal), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmuteVolumeEnum readback{};
ASSERT_EQ(tester.ReadAttribute(IncreaseVolumeUnmuteVolume::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, UnmuteVolumeEnum::kMinDeviceVolume);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, PersistenceDecreaseVolumeUnmutePolicy)
{
auto cfg = BasicConfig().WithDecreaseVolumeUnmutePolicy(UnmutePolicyEnum::kUnmuteOrChangeVolume);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmutePolicyEnum newPolicy = UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume;
ASSERT_EQ(tester.WriteAttribute(DecreaseVolumeUnmutePolicy::Id, newPolicy), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
UnmutePolicyEnum readback{};
ASSERT_EQ(tester.ReadAttribute(DecreaseVolumeUnmutePolicy::Id, readback), CHIP_NO_ERROR);
EXPECT_EQ(readback, UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// ---------- SetMaxUserVolume delegate behavior ----------
TEST_F(TestAudioControlCluster, SetMaxUserVolumeDelegateCalledOnVolumeClip)
{
// Volume=90 > new MaxUserVolume=70 → hardware must be notified via delegate.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithOptionalAttributes(optionalSet)
.WithInitialMaxUserVolume(100)
.WithInitialVolume(90)
.WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetMaxUserVolume(70), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 1);
EXPECT_EQ(mMockDelegate.lastNewVolume, 70u);
EXPECT_FALSE(mMockDelegate.lastNewSoftMuted);
EXPECT_EQ(cluster.GetVolume(), 70u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMaxUserVolumeNoDelegateCallWhenNoVolumeClip)
{
// Volume=50 <= new MaxUserVolume=70 → no clip → delegate NOT called.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithOptionalAttributes(optionalSet)
.WithInitialMaxUserVolume(100)
.WithInitialVolume(50)
.WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
mMockDelegate.Reset();
ASSERT_EQ(cluster.SetMaxUserVolume(70), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 0);
EXPECT_EQ(cluster.GetVolume(), 50u);
EXPECT_EQ(cluster.GetMaxUserVolume(), 70u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetMaxUserVolumeDelegateVetoLeavesAllStateUnchanged)
{
// Delegate rejects the volume clip → MaxUserVolume, Volume, and StartUpVolume must all be unchanged.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>().Set<StartUpVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig()
.WithOptionalAttributes(optionalSet)
.WithInitialMaxUserVolume(100)
.WithInitialVolume(90)
.WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t startupVol = 80;
ASSERT_EQ(tester.WriteAttribute(StartUpVolume::Id, DataModel::MakeNullable(startupVol)), CHIP_NO_ERROR);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_NE(cluster.SetMaxUserVolume(70), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMaxUserVolume(), 100u);
EXPECT_EQ(cluster.GetVolume(), 90u);
DataModel::Nullable<uint16_t> readback{};
ASSERT_EQ(tester.ReadAttribute(StartUpVolume::Id, readback), CHIP_NO_ERROR);
ASSERT_FALSE(readback.IsNull());
EXPECT_EQ(readback.Value(), 80u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Optional attribute absent: startup fallback behavior ----------
TEST_F(TestAudioControlCluster, StartUpMutedAbsentRetainsPersistedSoftMuted)
{
// When StartUpMuted is absent, SoftMuted is still non-volatile per spec: the cluster retains
// the persisted SoftMuted state on startup, mirroring the behavior of an explicit
// StartUpMuted=null.
auto cfg = BasicConfig().WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
ASSERT_TRUE(tester.Invoke(Mute::Type{}).IsSuccess()); // persists SoftMuted=true
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted()); // retained from persistence, not forced to false
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, StartUpVolumeAbsentBehavesAsNull)
{
// When StartUpVolume is absent, the cluster retains the persisted Volume on startup,
// mirroring the behavior of an explicit StartUpVolume=null.
auto cfg = BasicConfig().WithInitialVolume(50).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 80;
ASSERT_TRUE(tester.Invoke(cmd).IsSuccess()); // persists Volume=80
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 80u); // retained — absent StartUpVolume behaves as null
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// ---------- OnStartup delegate notification ----------
TEST_F(TestAudioControlCluster, OnStartupCalledWithConfigDefaultsOnFirstBoot)
{
// Empty KVS: OnStartup must receive the config defaults unchanged.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50).WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
EXPECT_EQ(mMockDelegate.startupState.volume, 50u);
EXPECT_FALSE(mMockDelegate.startupState.softMuted);
// No command-driven delegate calls on startup.
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, OnStartupCalledOncePerStartup)
{
// Shutdown + re-Startup must trigger OnStartup again (exactly once each time).
auto cfg = BasicConfig().WithInitialVolume(50).WithInitialSoftMuted(false);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
mMockDelegate.Reset();
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, OnStartupCalledWithPersistedVolume)
{
// After SetVolume(80) is persisted, a restart must call OnStartup with 80.
auto cfg = BasicConfig().WithInitialVolume(50).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
SetVolume::Type cmd;
cmd.newVolume = 80;
ASSERT_TRUE(ClusterTester(cluster).Invoke(cmd).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
mMockDelegate.Reset();
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
EXPECT_EQ(mMockDelegate.startupState.volume, 80u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, OnStartupCalledWithPersistedSoftMuted)
{
// After Mute is persisted (StartUpMuted=null → retain), restart must call OnStartup with muted=true.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialVolume(50).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_TRUE(ClusterTester(cluster).Invoke(Mute::Type{}).IsSuccess());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
mMockDelegate.Reset();
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
EXPECT_TRUE(mMockDelegate.startupState.softMuted);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, OnStartupCalledWithStartUpVolumeOverride)
{
// StartUpVolume=75 overrides persisted Volume=50; OnStartup must see 75.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpVolume::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialVolume(50).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
uint16_t startupVol = 75;
ASSERT_EQ(tester.WriteAttribute(StartUpVolume::Id, DataModel::MakeNullable(startupVol)), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
mMockDelegate.Reset();
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
EXPECT_EQ(mMockDelegate.startupState.volume, 75u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, OnStartupCalledWithStartUpMutedOverride)
{
// StartUpMuted=true overrides persisted SoftMuted=false; OnStartup must see muted=true.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialVolume(50).WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
bool val = true;
ASSERT_EQ(tester.WriteAttribute(StartUpMuted::Id, DataModel::MakeNullable(val)), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
mMockDelegate.Reset();
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
EXPECT_TRUE(mMockDelegate.startupState.softMuted);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, OnStartupCalledWithClampedVolume)
{
// Volume 90 persisted, then MaxUserVolume lowered to 70. OnStartup must see clamped value 70.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
{
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100).WithInitialVolume(50);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
SetVolume::Type cmd;
cmd.newVolume = 90;
ASSERT_TRUE(ClusterTester(cluster).Invoke(cmd).IsSuccess()); // persists Volume=90
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
mMockDelegate.Reset();
{
// Restart with MaxUserVolume lowered to 70 in config; persisted MaxUserVolume not written
// so KVS load falls back to new config default.
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(70).WithInitialVolume(50);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
EXPECT_EQ(mMockDelegate.startupState.volume, 70u); // clamped to new MaxUserVolume
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, OnStartupBEQClampedWhenRangeNarrows)
{
// Bass=4 persisted with range [-5,5]; on restart with narrower range [-3,3] it must be clamped to 3.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
{
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg = AudioControlCluster::Config{}
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
int16_t val = 4;
ASSERT_EQ(ClusterTester(cluster).WriteAttribute(Bass::Id, val), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
mMockDelegate.Reset();
{
mMockDelegate.minCorrection = -3;
mMockDelegate.maxCorrection = 3;
auto cfg = AudioControlCluster::Config{}
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupState.bass, 3); // clamped to new max
EXPECT_EQ(cluster.GetBass(), 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, OnStartupBEQZeroWhenFeatureDisabled)
{
// Without BEQ feature, bass/mid/treble in StartupState are the (default-zero) config values,
// since BasicConfig() never calls WithInitialBass/Mid/Treble. See the StartupState comment:
// the delegate is constructed alongside the same Config, so it already knows BEQ is disabled
// and should ignore these fields regardless of what raw value they carry.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50).WithInitialSoftMuted(false));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupState.bass, 0);
EXPECT_EQ(mMockDelegate.startupState.mid, 0);
EXPECT_EQ(mMockDelegate.startupState.treble, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, OnStartupBEQCalledWithConfigDefaults)
{
// BEQ enabled, no KVS: OnStartup must carry the config-default EQ values.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BEQAllBandsConfig(mMockDelegate).WithInitialBass(2).WithInitialMid(-1).WithInitialTreble(3));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupState.bass, 2);
EXPECT_EQ(mMockDelegate.startupState.mid, -1);
EXPECT_EQ(mMockDelegate.startupState.treble, 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, OnStartupBEQCalledWithPersistedValues)
{
// Write Bass=4, Mid=-3, Treble=2, then restart and verify OnStartup carries them.
auto cfg = BEQAllBandsConfig(mMockDelegate);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
int16_t bass = 4, mid = -3, treble = 2;
ASSERT_EQ(tester.WriteAttribute(Bass::Id, bass), CHIP_NO_ERROR);
ASSERT_EQ(tester.WriteAttribute(Mid::Id, mid), CHIP_NO_ERROR);
ASSERT_EQ(tester.WriteAttribute(Treble::Id, treble), CHIP_NO_ERROR);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
mMockDelegate.Reset();
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupCalls, 1);
EXPECT_EQ(mMockDelegate.startupState.bass, 4);
EXPECT_EQ(mMockDelegate.startupState.mid, -3);
EXPECT_EQ(mMockDelegate.startupState.treble, 2);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, OnStartupBEQPartialBands)
{
// Only Bass in optional set: mid and treble in StartupState are the (default-zero) config
// values, since this config never calls WithInitialMid/WithInitialTreble. The delegate is
// constructed alongside the same Config, so it already knows mid/treble are unsupported here.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<Bass::Id>();
mMockDelegate.minCorrection = -5;
mMockDelegate.maxCorrection = 5;
auto cfg = AudioControlCluster::Config{}
.WithFeatures(BitFlags<Feature>(Feature::kBasicEqualizer))
.WithOptionalAttributes(optionalSet)
.WithInitialBass(3);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.startupState.bass, 3);
EXPECT_EQ(mMockDelegate.startupState.mid, 0);
EXPECT_EQ(mMockDelegate.startupState.treble, 0);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Corrupted KVS: enum fallback to config default on startup ----------
TEST_F(TestAudioControlCluster, StartupCorruptedEnumKvsFallsBackToConfigDefault)
{
// Simulate corrupted KVS: inject an invalid byte (0xFF → kUnknownEnumValue) for each policy enum.
// The cluster must fall back to the config default rather than leaving the enum in an invalid
// state that would break command handling (ApplyUnmutePolicy hits the default case → UnsupportedCommand).
const uint8_t invalidByte = 0xFF;
auto writeCorruptByte = [&](AttributeId attrId) {
auto key = DefaultStorageKeyAllocator::AttributeValue(kRootEndpointId, AudioControl::Id, attrId);
ASSERT_EQ(testContext.StorageDelegate().SyncSetKeyValue(key.KeyName(), &invalidByte, 1), CHIP_NO_ERROR);
};
writeCorruptByte(SetVolumeUnmutePolicy::Id);
writeCorruptByte(IncreaseVolumeUnmutePolicy::Id);
writeCorruptByte(IncreaseVolumeUnmuteVolume::Id);
writeCorruptByte(DecreaseVolumeUnmutePolicy::Id);
// Config: DoNotUnmuteAndDoNotChangeVolume for SetVolume, kMinDeviceVolume for IncreaseVolumeUnmuteVolume.
// After startup with corrupted KVS, these config defaults must be used.
auto cfg = BasicConfig()
.WithSetVolumeUnmutePolicy(UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume)
.WithIncreaseVolumeUnmutePolicy(UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume)
.WithIncreaseVolumeUnmuteVolume(UnmuteVolumeEnum::kMinDeviceVolume)
.WithDecreaseVolumeUnmutePolicy(UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume)
.WithInitialSoftMuted(true)
.WithInitialVolume(50);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// Verify via ReadAttribute that persisted-but-corrupt values were replaced by config defaults.
UnmutePolicyEnum svup{};
ASSERT_EQ(tester.ReadAttribute(SetVolumeUnmutePolicy::Id, svup), CHIP_NO_ERROR);
EXPECT_EQ(svup, UnmutePolicyEnum::kDoNotUnmuteAndDoNotChangeVolume);
UnmutePolicyEnum ivup{};
ASSERT_EQ(tester.ReadAttribute(IncreaseVolumeUnmutePolicy::Id, ivup), CHIP_NO_ERROR);
EXPECT_EQ(ivup, UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume);
UnmuteVolumeEnum ivuv{};
ASSERT_EQ(tester.ReadAttribute(IncreaseVolumeUnmuteVolume::Id, ivuv), CHIP_NO_ERROR);
EXPECT_EQ(ivuv, UnmuteVolumeEnum::kMinDeviceVolume);
UnmutePolicyEnum dvup{};
ASSERT_EQ(tester.ReadAttribute(DecreaseVolumeUnmutePolicy::Id, dvup), CHIP_NO_ERROR);
EXPECT_EQ(dvup, UnmutePolicyEnum::kDoNotUnmuteAndChangeVolume);
// Behavioral verify: SetVolume on a muted device with kDoNotUnmuteAndDoNotChangeVolume → rejected and state unchanged.
// If the corrupt value 0xFF had been used, the policy switch would hit default → UnsupportedCommand,
// and volume/mute state would still be unchanged — but the wrong status code would be a bug.
SetVolume::Type cmd;
cmd.newVolume = 75;
EXPECT_FALSE(tester.Invoke(cmd).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u); // unchanged
EXPECT_TRUE(cluster.GetSoftMuted()); // still muted
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// ---------- Regression tests for command/persistence hardening fixes ----------
TEST_F(TestAudioControlCluster, StartupCorruptedMaxUserVolumeKvsClampedToValidRange)
{
// Simulate a corrupted/stale KVS value for MaxUserVolume that is below MinDeviceVolume.
// Without clamping, EffectiveMaxVolume() would return it verbatim and the later
// std::clamp(mVolume, mMinDeviceVolume, EffectiveMaxVolume()) call would violate std::clamp's
// precondition (lower <= upper).
const uint16_t corruptValue = 0; // BasicConfig() has MinDeviceVolume == 1
auto key = DefaultStorageKeyAllocator::AttributeValue(kRootEndpointId, AudioControl::Id, MaxUserVolume::Id);
ASSERT_EQ(testContext.StorageDelegate().SyncSetKeyValue(key.KeyName(), &corruptValue, sizeof(corruptValue)), CHIP_NO_ERROR);
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100).WithInitialVolume(50);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetMaxUserVolume(), 1u); // clamped up to MinDeviceVolume, not left at 0
EXPECT_EQ(cluster.GetVolume(), 1u); // Volume clamp then applies against the corrected ceiling
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, StartupCorruptedDefaultStepSizeKvsClampedToOne)
{
// Simulate a corrupted/stale KVS value of 0 for DefaultStepSize. Without clamping, an
// IncreaseVolume/DecreaseVolume command that omits an explicit stepSize would fail
// VerifyOrReturnError(stepSize >= 1, ...) forever, with no command-level way to repair it.
const uint16_t corruptValue = 0;
auto key = DefaultStorageKeyAllocator::AttributeValue(kRootEndpointId, AudioControl::Id, DefaultStepSize::Id);
ASSERT_EQ(testContext.StorageDelegate().SyncSetKeyValue(key.KeyName(), &corruptValue, sizeof(corruptValue)), CHIP_NO_ERROR);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(false).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
// Default step clamped to 1: 50 -> 51, rather than a permanent ConstraintError.
EXPECT_TRUE(tester.Invoke(IncreaseVolume::Type{}).IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 51u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SetVolumeInvalidUnmutePolicyReturnsConstraintError)
{
// SoftMuted=TRUE so the command goes through ApplyUnmutePolicy (not the fast path).
// An UnmutePolicy value outside the defined enumerators is a malformed command field,
// not a state conflict, so it must be rejected with ConstraintError (not InvalidInState).
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
SetVolume::Type cmd;
cmd.newVolume = 75;
cmd.unmutePolicy.SetValue(static_cast<UnmutePolicyEnum>(99));
auto result = tester.Invoke(cmd);
EXPECT_FALSE(result.IsSuccess());
auto statusCode = result.GetStatusCode();
// validation to make clang-tidy happy.
if (statusCode.has_value())
{
EXPECT_EQ(statusCode->GetStatus(), Status::ConstraintError);
}
else
{
FAIL();
}
// State unchanged.
EXPECT_EQ(cluster.GetVolume(), 50u);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseVolumeDegenerateRangeClampsToNoOpSuccess)
{
// MinDeviceVolume == MaxDeviceVolume -> EffectiveMax == MinDeviceVolume, so there is no room to
// increase. StepSize is only checked for being >= 1; the resulting volume is clamped to
// EffectiveMax, so the command succeeds as a no-op rather than failing with ConstraintError.
mMockDelegate.minDeviceVolume = 50;
mMockDelegate.maxDeviceVolume = 50;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
AudioControlCluster::Config{}.WithInitialVolume(50).WithInitialDefaultStepSize(1));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
auto result = tester.Invoke(IncreaseVolume::Type{});
EXPECT_TRUE(result.IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, DecreaseVolumeDegenerateRangeClampsToNoOpSuccess)
{
// Same degenerate range as IncreaseVolumeDegenerateRangeClampsToNoOpSuccess, but for
// DecreaseVolume: the result floors at MinDeviceVolume (== MaxDeviceVolume here), so the
// command succeeds, muting the device rather than failing with ConstraintError.
mMockDelegate.minDeviceVolume = 50;
mMockDelegate.maxDeviceVolume = 50;
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
AudioControlCluster::Config{}.WithInitialVolume(50).WithInitialDefaultStepSize(1));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
auto result = tester.Invoke(DecreaseVolume::Type{});
EXPECT_TRUE(result.IsSuccess());
EXPECT_EQ(cluster.GetVolume(), 50u);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, IncreaseDecreaseVolumeRejectZeroStepSize)
{
// StepSize is still required to be >= 1 regardless of the volume range.
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialVolume(50));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(cluster);
IncreaseVolume::Type increaseCmd;
increaseCmd.stepSize.SetValue(0);
auto increaseResult = tester.Invoke(increaseCmd);
EXPECT_FALSE(increaseResult.IsSuccess());
auto increaseStatusCode = increaseResult.GetStatusCode();
// validation to make clang-tidy happy.
if (increaseStatusCode.has_value())
{
EXPECT_EQ(increaseStatusCode->GetStatus(), Status::ConstraintError);
}
else
{
FAIL();
}
DecreaseVolume::Type decreaseCmd;
decreaseCmd.stepSize.SetValue(0);
auto decreaseResult = tester.Invoke(decreaseCmd);
EXPECT_FALSE(decreaseResult.IsSuccess());
auto decreaseStatusCode = decreaseResult.GetStatusCode();
// validation to make clang-tidy happy.
if (decreaseStatusCode.has_value())
{
EXPECT_EQ(decreaseStatusCode->GetStatus(), Status::ConstraintError);
}
else
{
FAIL();
}
EXPECT_EQ(cluster.GetVolume(), 50u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SoftMutedPersistedWithoutStartUpMuted)
{
// SoftMuted is non-volatile per spec and must survive a restart even when StartUpMuted is not
// in the optional set.
auto cfg = BasicConfig().WithInitialSoftMuted(false);
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetSoftMuted(true), CHIP_NO_ERROR);
auto key = DefaultStorageKeyAllocator::AttributeValue(kRootEndpointId, AudioControl::Id, SoftMuted::Id);
EXPECT_TRUE(testContext.StorageDelegate().SyncDoesKeyExist(key.KeyName()));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted()); // restored from persistence, not the config default
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, StartUpMutedOverridePersistsAcrossReboot)
{
// Boot 1: StartUpMuted=true overrides SoftMuted -> the effective value must be persisted, not
// just applied in RAM, so a later boot without the override still sees it.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpMuted::Id>();
bool startUpMuted = true;
{
auto cfg = BasicConfig().WithOptionalAttributes(optionalSet).WithInitialStartUpMuted(DataModel::MakeNullable(startUpMuted));
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
auto key = DefaultStorageKeyAllocator::AttributeValue(kRootEndpointId, AudioControl::Id, SoftMuted::Id);
EXPECT_TRUE(testContext.StorageDelegate().SyncDoesKeyExist(key.KeyName()));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Boot 2: StartUpMuted no longer overrides -> SoftMuted must load the persisted value from
// boot 1, not fall back to the config default.
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
TEST_F(TestAudioControlCluster, StartUpVolumeOverridePersistsAcrossReboot)
{
// Boot 1: StartUpVolume=75 overrides Volume -> the effective value must be persisted, not just
// applied in RAM, so a later boot without the override still sees it.
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<StartUpVolume::Id>();
uint16_t startUpVolume = 75;
{
auto cfg =
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialStartUpVolume(DataModel::MakeNullable(startUpVolume));
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 75u);
auto key = DefaultStorageKeyAllocator::AttributeValue(kRootEndpointId, AudioControl::Id, Volume::Id);
EXPECT_TRUE(testContext.StorageDelegate().SyncDoesKeyExist(key.KeyName()));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Boot 2: StartUpVolume no longer overrides -> Volume must load the persisted value from
// boot 1, not fall back to the config default.
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 75u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// ---------- Scene handler (scenes::SceneHandler) ----------
namespace {
using ScenePair = ScenesManagement::Structs::AttributeValuePairStruct::Type;
using SceneDecodable = ScenesManagement::Structs::AttributeValuePairStruct::DecodableType;
// Encodes a list of {attributeID, value} pairs into a scene EFS blob, as the scene table would.
CHIP_ERROR EncodePairs(AudioControlCluster & cluster, const app::DataModel::List<ScenePair> & pairs, MutableByteSpan & out)
{
return cluster.EncodeAttributeValueList(pairs, out);
}
// Builds the decodable extension field set that ScenesServer hands to SerializeAdd (the AddScene
// path), round-tripping the given cluster id and pairs through TLV.
CHIP_ERROR MakeDecodableEfs(ClusterId clusterId, chip::Span<const ScenePair> pairs, MutableByteSpan & backing,
ScenesManagement::Structs::ExtensionFieldSetStruct::DecodableType & out)
{
ScenesManagement::Structs::ExtensionFieldSetStruct::Type in;
in.clusterID = clusterId;
in.attributeValueList = app::DataModel::List<const ScenePair>(pairs.data(), pairs.size());
TLV::TLVWriter writer;
writer.Init(backing);
ReturnErrorOnFailure(in.Encode(writer, TLV::AnonymousTag()));
ReturnErrorOnFailure(writer.Finalize());
backing.reduce_size(writer.GetLengthWritten());
TLV::TLVReader reader;
reader.Init(backing);
ReturnErrorOnFailure(reader.Next());
return out.Decode(reader);
}
} // namespace
TEST_F(TestAudioControlCluster, SceneSupportsClusterOnlyOwnEndpointAndCluster)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.SupportsCluster(kRootEndpointId, AudioControl::Id));
EXPECT_FALSE(cluster.SupportsCluster(kRootEndpointId + 1, AudioControl::Id));
EXPECT_FALSE(cluster.SupportsCluster(kRootEndpointId, OnOff::Id));
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SceneSerializeMandatoryAttributesOnly)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig().WithInitialSoftMuted(true).WithInitialVolume(42));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
uint8_t buffer[128];
MutableByteSpan serialized(buffer);
ASSERT_EQ(cluster.SerializeSave(kRootEndpointId, AudioControl::Id, serialized), CHIP_NO_ERROR);
app::DataModel::DecodableList<SceneDecodable> list;
ASSERT_EQ(cluster.DecodeAttributeValueList(serialized, list), CHIP_NO_ERROR);
bool sawSoftMuted = false, sawVolume = false;
auto it = list.begin();
while (it.Next())
{
const auto & p = it.GetValue();
if (p.attributeID == Attributes::SoftMuted::Id)
{
sawSoftMuted = true;
EXPECT_EQ(p.valueUnsigned8.Value(), 1u);
}
else if (p.attributeID == Attributes::Volume::Id)
{
sawVolume = true;
EXPECT_EQ(p.valueUnsigned16.Value(), 42u);
}
else
{
ADD_FAILURE() << "unexpected attribute in non-BEQ scene: " << p.attributeID;
}
}
EXPECT_EQ(it.GetStatus(), CHIP_NO_ERROR);
EXPECT_TRUE(sawSoftMuted);
EXPECT_TRUE(sawVolume);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SceneSerializeIncludesEnabledEqualizerBands)
{
auto cfg = BEQAllBandsConfig(mMockDelegate).WithInitialBass(-3).WithInitialMid(1).WithInitialTreble(4);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
uint8_t buffer[128];
MutableByteSpan serialized(buffer);
ASSERT_EQ(cluster.SerializeSave(kRootEndpointId, AudioControl::Id, serialized), CHIP_NO_ERROR);
app::DataModel::DecodableList<SceneDecodable> list;
ASSERT_EQ(cluster.DecodeAttributeValueList(serialized, list), CHIP_NO_ERROR);
int16_t bass = 0, mid = 0, treble = 0;
bool sawBass = false, sawMid = false, sawTreble = false;
auto it = list.begin();
while (it.Next())
{
const auto & p = it.GetValue();
if (p.attributeID == Attributes::Bass::Id)
{
sawBass = true;
bass = p.valueSigned16.Value();
}
else if (p.attributeID == Attributes::Mid::Id)
{
sawMid = true;
mid = p.valueSigned16.Value();
}
else if (p.attributeID == Attributes::Treble::Id)
{
sawTreble = true;
treble = p.valueSigned16.Value();
}
}
EXPECT_TRUE(sawBass && sawMid && sawTreble);
EXPECT_EQ(bass, -3);
EXPECT_EQ(mid, 1);
EXPECT_EQ(treble, 4);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SceneApplyRoundTrip)
{
auto cfg = BEQAllBandsConfig(mMockDelegate).WithInitialSoftMuted(false).WithInitialVolume(20).WithInitialBass(0);
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, cfg);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
// State A -> save.
ASSERT_EQ(cluster.SetSoftMuted(true), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetVolume(80), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetBass(5), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMid(-5), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetTreble(2), CHIP_NO_ERROR);
uint8_t buffer[128];
MutableByteSpan saved(buffer);
ASSERT_EQ(cluster.SerializeSave(kRootEndpointId, AudioControl::Id, saved), CHIP_NO_ERROR);
// Move to state B.
ASSERT_EQ(cluster.SetSoftMuted(false), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetVolume(10), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetBass(0), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMid(0), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetTreble(0), CHIP_NO_ERROR);
// Recall -> back to state A, transition time ignored.
ASSERT_EQ(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, saved, 1000), CHIP_NO_ERROR);
EXPECT_TRUE(cluster.GetSoftMuted());
EXPECT_EQ(cluster.GetVolume(), 80u);
EXPECT_EQ(cluster.GetBass(), 5);
EXPECT_EQ(cluster.GetMid(), -5);
EXPECT_EQ(cluster.GetTreble(), 2);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A pair referencing an attribute that is not implemented on the endpoint is ignored rather than
// failing the recall (Scenes Management AttributeValuePairStruct): the recall succeeds and the
// state is untouched.
TEST_F(TestAudioControlCluster, SceneApplyIgnoresUnsupportedAttribute)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ScenePair pairs[1];
pairs[0].attributeID = Attributes::MaxUserVolume::Id; // not scene-able
pairs[0].valueUnsigned16.SetValue(50);
app::DataModel::List<ScenePair> list(pairs);
uint8_t buffer[128];
MutableByteSpan blob(buffer);
ASSERT_EQ(EncodePairs(cluster, list, blob), CHIP_NO_ERROR);
EXPECT_EQ(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, blob, 0), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 50u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SceneApplyIgnoresUnsupportedAttributeAndAppliesVolume)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
// The unsupported pair comes first so the test proves skipping does not derail the later pair.
ScenePair pairs[2];
pairs[0].attributeID = Attributes::MaxUserVolume::Id; // not scene-able
pairs[0].valueUnsigned16.SetValue(50);
pairs[1].attributeID = Attributes::Volume::Id;
pairs[1].valueUnsigned16.SetValue(30);
app::DataModel::List<ScenePair> list(pairs);
uint8_t buffer[128];
MutableByteSpan blob(buffer);
ASSERT_EQ(EncodePairs(cluster, list, blob), CHIP_NO_ERROR);
EXPECT_EQ(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, blob, 0), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 30u);
EXPECT_EQ(mMockDelegate.lastNewVolume, 30u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestAudioControlCluster, SceneApplyWrongClusterFails)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig());
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
uint8_t buffer[128];
MutableByteSpan saved(buffer);
ASSERT_EQ(cluster.SerializeSave(kRootEndpointId, AudioControl::Id, saved), CHIP_NO_ERROR);
EXPECT_EQ(cluster.ApplyScene(kRootEndpointId, OnOff::Id, saved, 0), CHIP_ERROR_INVALID_ARGUMENT);
EXPECT_EQ(cluster.SerializeSave(kRootEndpointId, OnOff::Id, saved), CHIP_ERROR_INVALID_ARGUMENT);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A scene captured while Volume was legal must still recall after MaxUserVolume shrinks: the
// stored value is saturated to the current EffectiveMaxVolume rather than rejected.
TEST_F(TestAudioControlCluster, SceneApplyClampsVolumeToReducedMaxUserVolume)
{
AudioControlCluster::OptionalAttributeSet optionalSet;
optionalSet.Set<MaxUserVolume::Id>();
AudioControlCluster cluster(kRootEndpointId, mMockDelegate,
BasicConfig().WithOptionalAttributes(optionalSet).WithInitialMaxUserVolume(100));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetVolume(80), CHIP_NO_ERROR);
uint8_t buffer[128];
MutableByteSpan saved(buffer);
ASSERT_EQ(cluster.SerializeSave(kRootEndpointId, AudioControl::Id, saved), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMaxUserVolume(60), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetVolume(30), CHIP_NO_ERROR);
EXPECT_EQ(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, saved, 0), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetVolume(), 60u);
EXPECT_EQ(mMockDelegate.lastNewVolume, 60u);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// Likewise for an equalizer band whose stored value falls outside a correction range that
// narrowed since capture.
TEST_F(TestAudioControlCluster, SceneApplyClampsEqualizerBandToCorrectionRange)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate)); // range [-5, 5]
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ScenePair pairs[3];
pairs[0].attributeID = Attributes::SoftMuted::Id;
pairs[0].valueUnsigned8.SetValue(0);
pairs[1].attributeID = Attributes::Volume::Id;
pairs[1].valueUnsigned16.SetValue(50);
pairs[2].attributeID = Attributes::Bass::Id;
pairs[2].valueSigned16.SetValue(100); // far above mMaxCorrection
app::DataModel::List<ScenePair> list(pairs);
uint8_t buffer[128];
MutableByteSpan blob(buffer);
ASSERT_EQ(EncodePairs(cluster, list, blob), CHIP_NO_ERROR);
EXPECT_EQ(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, blob, 0), CHIP_NO_ERROR);
EXPECT_EQ(cluster.GetBass(), 5);
EXPECT_EQ(mMockDelegate.lastBass, 5);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A band that is not enabled on this instance is silently skipped, not treated as an error.
TEST_F(TestAudioControlCluster, SceneApplySkipsDisabledEqualizerBand)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BasicConfig()); // no BasicEqualizer feature
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ScenePair pairs[3];
pairs[0].attributeID = Attributes::SoftMuted::Id;
pairs[0].valueUnsigned8.SetValue(0);
pairs[1].attributeID = Attributes::Volume::Id;
pairs[1].valueUnsigned16.SetValue(50);
pairs[2].attributeID = Attributes::Bass::Id;
pairs[2].valueSigned16.SetValue(3);
app::DataModel::List<ScenePair> list(pairs);
uint8_t buffer[128];
MutableByteSpan blob(buffer);
ASSERT_EQ(EncodePairs(cluster, list, blob), CHIP_NO_ERROR);
EXPECT_EQ(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, blob, 0), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.bassChangedCalls, 0);
EXPECT_EQ(cluster.GetBass(), 0); // unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A scene carrying only an equalizer band must not notify HandleVolumeAndMuteChange.
TEST_F(TestAudioControlCluster, SceneApplyBandOnlySceneDoesNotNotifyVolumeMute)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ScenePair pairs[1];
pairs[0].attributeID = Attributes::Bass::Id;
pairs[0].valueSigned16.SetValue(3);
app::DataModel::List<ScenePair> list(pairs);
uint8_t buffer[128];
MutableByteSpan blob(buffer);
ASSERT_EQ(EncodePairs(cluster, list, blob), CHIP_NO_ERROR);
EXPECT_EQ(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, blob, 0), CHIP_NO_ERROR);
EXPECT_EQ(mMockDelegate.volumeAndMuteCalls, 0);
EXPECT_EQ(mMockDelegate.bassChangedCalls, 1);
EXPECT_EQ(cluster.GetBass(), 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// The delegate is notified before any commit, so a rejection leaves every attribute untouched.
TEST_F(TestAudioControlCluster, SceneApplyDelegateRejectionLeavesAttributesUnchanged)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
// State A -> save.
ASSERT_EQ(cluster.SetSoftMuted(true), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetVolume(80), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetBass(5), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMid(-5), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetTreble(4), CHIP_NO_ERROR);
uint8_t buffer[128];
MutableByteSpan saved(buffer);
ASSERT_EQ(cluster.SerializeSave(kRootEndpointId, AudioControl::Id, saved), CHIP_NO_ERROR);
// Move to state B, then make the delegate reject the recall.
ASSERT_EQ(cluster.SetSoftMuted(false), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetVolume(25), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetBass(1), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetMid(2), CHIP_NO_ERROR);
ASSERT_EQ(cluster.SetTreble(3), CHIP_NO_ERROR);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_NE(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, saved, 0), CHIP_NO_ERROR);
// Everything stays at state B.
EXPECT_FALSE(cluster.GetSoftMuted());
EXPECT_EQ(cluster.GetVolume(), 25u);
EXPECT_EQ(cluster.GetBass(), 1);
EXPECT_EQ(cluster.GetMid(), 2);
EXPECT_EQ(cluster.GetTreble(), 3);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
// A band delegate rejecting a recall propagates the error and leaves that band unchanged (a
// band-only scene skips the Volume/SoftMuted delegate call, so its status alone decides the result).
TEST_F(TestAudioControlCluster, SceneApplyBandDelegateRejectionPropagates)
{
struct BandCase
{
AttributeId id;
int16_t (AudioControlCluster::*get)() const;
};
const BandCase cases[] = {
{ Attributes::Bass::Id, &AudioControlCluster::GetBass },
{ Attributes::Mid::Id, &AudioControlCluster::GetMid },
{ Attributes::Treble::Id, &AudioControlCluster::GetTreble },
};
for (const auto & c : cases)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
ScenePair pairs[1];
pairs[0].attributeID = c.id;
pairs[0].valueSigned16.SetValue(3); // differs from the initial 0
app::DataModel::List<ScenePair> list(pairs);
uint8_t buffer[128];
MutableByteSpan blob(buffer);
ASSERT_EQ(EncodePairs(cluster, list, blob), CHIP_NO_ERROR);
mMockDelegate.nextStatus = Status::Busy;
EXPECT_NE(cluster.ApplyScene(kRootEndpointId, AudioControl::Id, blob, 0), CHIP_NO_ERROR);
EXPECT_EQ((cluster.*c.get)(), 0); // band unchanged
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
}
// The AddScene path (SerializeAdd) runs every pair through AudioControlSceneValidator: the five
// scene-able attributes on the AudioControl cluster are accepted, and anything else - a
// non-scene-able attribute or a foreign cluster id - is rejected.
TEST_F(TestAudioControlCluster, SceneSerializeAddValidatesAttributesAndCluster)
{
AudioControlCluster cluster(kRootEndpointId, mMockDelegate, BEQAllBandsConfig(mMockDelegate));
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
uint8_t backing[192];
uint8_t out[192];
// All five scene-able attributes, correct cluster -> accepted.
{
ScenePair pairs[5];
pairs[0].attributeID = Attributes::SoftMuted::Id;
pairs[0].valueUnsigned8.SetValue(1);
pairs[1].attributeID = Attributes::Volume::Id;
pairs[1].valueUnsigned16.SetValue(40);
pairs[2].attributeID = Attributes::Bass::Id;
pairs[2].valueSigned16.SetValue(1);
pairs[3].attributeID = Attributes::Mid::Id;
pairs[3].valueSigned16.SetValue(-1);
pairs[4].attributeID = Attributes::Treble::Id;
pairs[4].valueSigned16.SetValue(2);
MutableByteSpan backingSpan(backing);
ScenesManagement::Structs::ExtensionFieldSetStruct::DecodableType efs;
ASSERT_EQ(MakeDecodableEfs(AudioControl::Id, chip::Span<const ScenePair>(pairs), backingSpan, efs), CHIP_NO_ERROR);
MutableByteSpan outSpan(out);
EXPECT_EQ(cluster.SerializeAdd(kRootEndpointId, efs, outSpan), CHIP_NO_ERROR);
}
// A non-scene-able AudioControl attribute -> rejected.
{
ScenePair pairs[1];
pairs[0].attributeID = Attributes::MaxUserVolume::Id;
pairs[0].valueUnsigned16.SetValue(50);
MutableByteSpan backingSpan(backing);
ScenesManagement::Structs::ExtensionFieldSetStruct::DecodableType efs;
ASSERT_EQ(MakeDecodableEfs(AudioControl::Id, chip::Span<const ScenePair>(pairs), backingSpan, efs), CHIP_NO_ERROR);
MutableByteSpan outSpan(out);
EXPECT_EQ(cluster.SerializeAdd(kRootEndpointId, efs, outSpan), CHIP_ERROR_INVALID_ARGUMENT);
}
// A scene-able attribute id but a foreign cluster id -> rejected.
{
ScenePair pairs[1];
pairs[0].attributeID = Attributes::Volume::Id;
pairs[0].valueUnsigned16.SetValue(40);
MutableByteSpan backingSpan(backing);
ScenesManagement::Structs::ExtensionFieldSetStruct::DecodableType efs;
ASSERT_EQ(MakeDecodableEfs(OnOff::Id, chip::Span<const ScenePair>(pairs), backingSpan, efs), CHIP_NO_ERROR);
MutableByteSpan outSpan(out);
EXPECT_EQ(cluster.SerializeAdd(kRootEndpointId, efs, outSpan), CHIP_ERROR_INVALID_ARGUMENT);
}
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
}
} // namespace