# Machine Context Custom Fields — Backend Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **Parallel plan:** This is the backend half. The frontend plan is at `2026-04-02-machine-context-fields-frontend.md`. Both can run in parallel on separate worktrees — they share no files. **Goal:** Add `machineContextOnly` flag on `CustomField`, link FKs on `CustomFieldValue`, structure controller normalization, upsert support, clone support, and tests. **Architecture:** `CustomField.machineContextOnly` flags definitions. `CustomFieldValue` gets nullable FKs to `MachineComponentLink`/`MachinePieceLink`. The structure response returns `contextCustomFields` and `contextCustomFieldValues` per link. Upsert and clone are extended. **Tech Stack:** Symfony 8, Doctrine ORM, API Platform 4, PostgreSQL 16, PHPUnit 12 **Spec:** `docs/superpowers/specs/2026-04-02-machine-context-custom-fields-design.md` --- ## File Map ### Create - `migrations/VersionXXXX_MachineContextCustomFields.php` — migration - `tests/Api/Entity/MachineContextCustomFieldTest.php` — test class ### Modify - `src/Entity/CustomField.php` — add `machineContextOnly` property - `src/Entity/CustomFieldValue.php` — add `machineComponentLink` and `machinePieceLink` FKs - `src/Entity/MachineComponentLink.php` — add `contextFieldValues` collection - `src/Entity/MachinePieceLink.php` — add `contextFieldValues` collection - `src/Controller/MachineStructureController.php` — normalize context fields + clone - `src/Controller/CustomFieldValueController.php` — link-based upsert - `tests/AbstractApiTestCase.php` — extend factories --- ## Task 1: Migration + Entity `CustomField` — `machineContextOnly` **Files:** - Modify: `src/Entity/CustomField.php` (add property after line 56) - [ ] **Step 1: Add `machineContextOnly` property to `CustomField` entity** In `src/Entity/CustomField.php`, add after the `$required` property (line 56): ```php #[ORM\Column(type: Types::BOOLEAN, options: ['default' => false], name: 'machinecontextonly')] #[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])] private bool $machineContextOnly = false; ``` Add getter/setter before the closing `}`: ```php public function isMachineContextOnly(): bool { return $this->machineContextOnly; } public function setMachineContextOnly(bool $machineContextOnly): static { $this->machineContextOnly = $machineContextOnly; return $this; } ``` - [ ] **Step 2: Generate and adjust migration** ```bash docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:diff ``` Edit the generated migration to use idempotent SQL: ```sql ALTER TABLE custom_fields ADD COLUMN IF NOT EXISTS machinecontextonly BOOLEAN DEFAULT false NOT NULL; ``` - [ ] **Step 3: Run migration** ```bash docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction ``` - [ ] **Step 4: Run linter** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 5: Commit** ```bash git add src/Entity/CustomField.php migrations/ git commit -m "feat(custom-fields) : add machineContextOnly flag to CustomField entity" ``` --- ## Task 2: Entity `CustomFieldValue` — link FKs + inverse collections **Files:** - Modify: `src/Entity/CustomFieldValue.php` (add after line 67 — `$product` property) - Modify: `src/Entity/MachineComponentLink.php` (add after line 72 — `$productLinks`) - Modify: `src/Entity/MachinePieceLink.php` (add after line 61 — `$productLinks`) - [ ] **Step 1: Add FKs to `CustomFieldValue`** In `src/Entity/CustomFieldValue.php`, add after the `$product` property (line 67): ```php #[ORM\ManyToOne(targetEntity: MachineComponentLink::class, inversedBy: 'contextFieldValues')] #[ORM\JoinColumn(name: 'machinecomponentlinkid', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] private ?MachineComponentLink $machineComponentLink = null; #[ORM\ManyToOne(targetEntity: MachinePieceLink::class, inversedBy: 'contextFieldValues')] #[ORM\JoinColumn(name: 'machinepiecelinkid', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] private ?MachinePieceLink $machinePieceLink = null; ``` Add getters/setters before the closing `}`: ```php public function getMachineComponentLink(): ?MachineComponentLink { return $this->machineComponentLink; } public function setMachineComponentLink(?MachineComponentLink $machineComponentLink): static { $this->machineComponentLink = $machineComponentLink; return $this; } public function getMachinePieceLink(): ?MachinePieceLink { return $this->machinePieceLink; } public function setMachinePieceLink(?MachinePieceLink $machinePieceLink): static { $this->machinePieceLink = $machinePieceLink; return $this; } ``` - [ ] **Step 2: Add `contextFieldValues` collection to `MachineComponentLink`** In `src/Entity/MachineComponentLink.php`, add after the `$productLinks` collection (line 72): ```php /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'machineComponentLink', targetEntity: CustomFieldValue::class, cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection $contextFieldValues; ``` In the constructor (line 95), add: ```php $this->contextFieldValues = new ArrayCollection(); ``` Add getter before the closing `}`: ```php /** * @return Collection */ public function getContextFieldValues(): Collection { return $this->contextFieldValues; } ``` - [ ] **Step 3: Add `contextFieldValues` collection to `MachinePieceLink`** In `src/Entity/MachinePieceLink.php`, add after the `$productLinks` collection (line 61): ```php /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'machinePieceLink', targetEntity: CustomFieldValue::class, cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection $contextFieldValues; ``` In the constructor (line 86), add: ```php $this->contextFieldValues = new ArrayCollection(); ``` Add getter: ```php /** * @return Collection */ public function getContextFieldValues(): Collection { return $this->contextFieldValues; } ``` - [ ] **Step 4: Generate and adjust migration** ```bash docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:diff ``` Edit migration to use idempotent SQL: ```sql ALTER TABLE custom_field_values ADD COLUMN IF NOT EXISTS machinecomponentlinkid VARCHAR(36) DEFAULT NULL; ALTER TABLE custom_field_values ADD COLUMN IF NOT EXISTS machinepiecelinkid VARCHAR(36) DEFAULT NULL; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_cfv_machine_component_link') THEN ALTER TABLE custom_field_values ADD CONSTRAINT fk_cfv_machine_component_link FOREIGN KEY (machinecomponentlinkid) REFERENCES machine_component_links(id) ON DELETE CASCADE; END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_cfv_machine_piece_link') THEN ALTER TABLE custom_field_values ADD CONSTRAINT fk_cfv_machine_piece_link FOREIGN KEY (machinepiecelinkid) REFERENCES machine_piece_links(id) ON DELETE CASCADE; END IF; END $$; CREATE INDEX IF NOT EXISTS idx_cfv_machine_component_link ON custom_field_values(machinecomponentlinkid); CREATE INDEX IF NOT EXISTS idx_cfv_machine_piece_link ON custom_field_values(machinepiecelinkid); ``` - [ ] **Step 5: Run migration + linter** ```bash docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate --no-interaction make php-cs-fixer-allow-risky ``` - [ ] **Step 6: Commit** ```bash git add src/Entity/CustomFieldValue.php src/Entity/MachineComponentLink.php src/Entity/MachinePieceLink.php migrations/ git commit -m "feat(custom-fields) : add link FKs to CustomFieldValue for machine context" ``` --- ## Task 3: Test factories — extend helpers **Files:** - Modify: `tests/AbstractApiTestCase.php:399-461` - [ ] **Step 1: Update `createCustomField()` factory** In `tests/AbstractApiTestCase.php`, add `bool $machineContextOnly = false` parameter at the end of `createCustomField` (line 399), and add `$cf->setMachineContextOnly($machineContextOnly);` after `$cf->setOrderIndex($orderIndex);` (line 411). - [ ] **Step 2: Update `createCustomFieldValue()` factory** Add two new nullable parameters at the end of `createCustomFieldValue` (line 432): ```php ?MachineComponentLink $machineComponentLink = null, ?MachinePieceLink $machinePieceLink = null, ``` Add the corresponding setter calls **after the closing `}` of the `if (null !== $product)` block** (after line 454, NOT inside it): ```php if (null !== $machineComponentLink) { $cfv->setMachineComponentLink($machineComponentLink); } if (null !== $machinePieceLink) { $cfv->setMachinePieceLink($machinePieceLink); } ``` - [ ] **Step 3: Run linter** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 4: Commit** ```bash git add tests/AbstractApiTestCase.php git commit -m "test(custom-fields) : extend factories for machineContextOnly and link params" ``` --- ## Task 4: `MachineStructureController` — normalize context fields **Files:** - Modify: `src/Controller/MachineStructureController.php` - [ ] **Step 1: Add `machineContextOnly` to all normalization methods** In `normalizeCustomFields` (line 601), add to the output array at line 615: ```php 'machineContextOnly' => $customField->isMachineContextOnly(), ``` In `normalizeCustomFieldDefinitions` (line 838), add to the output array at line 852: ```php 'machineContextOnly' => $cf->isMachineContextOnly(), ``` In `normalizeCustomFieldValues` (line 861), add to the nested `customField` array at line 879: ```php 'machineContextOnly' => $cf->isMachineContextOnly(), ``` - [ ] **Step 2: Add `normalizeContextCustomFieldDefinitions` helper** Add a new private method after `normalizeCustomFieldValues`: ```php private function normalizeContextCustomFieldDefinitions(Collection $customFields): array { $items = []; foreach ($customFields as $cf) { if (!$cf instanceof CustomField || !$cf->isMachineContextOnly()) { continue; } $items[] = [ 'id' => $cf->getId(), 'name' => $cf->getName(), 'type' => $cf->getType(), 'required' => $cf->isRequired(), 'options' => $cf->getOptions(), 'defaultValue' => $cf->getDefaultValue(), 'orderIndex' => $cf->getOrderIndex(), 'machineContextOnly' => true, ]; } usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']); return $items; } ``` - [ ] **Step 3: Update `normalizeComponentLinks` to include context fields** In `normalizeComponentLinks` (line 622), add `$type` variable and context field keys to the returned array: ```php private function normalizeComponentLinks(array $links): array { return array_map(function (MachineComponentLink $link): array { $composant = $link->getComposant(); $parentLink = $link->getParentLink(); $type = $composant->getTypeComposant(); return [ 'id' => $link->getId(), 'linkId' => $link->getId(), 'machineId' => $link->getMachine()->getId(), 'composantId' => $composant->getId(), 'composant' => $this->normalizeComposant($composant), 'parentLinkId' => $parentLink?->getId(), 'parentComponentLinkId' => $parentLink?->getId(), 'parentComponentId' => $parentLink?->getComposant()->getId(), 'overrides' => $this->normalizeOverrides($link), 'childLinks' => [], 'pieceLinks' => [], 'contextCustomFields' => $type ? $this->normalizeContextCustomFieldDefinitions($type->getComponentCustomFields()) : [], 'contextCustomFieldValues' => $this->normalizeCustomFieldValues($link->getContextFieldValues()), ]; }, $links); } ``` - [ ] **Step 4: Update `normalizePieceLinks` to include context fields** In `normalizePieceLinks` (line 644): ```php private function normalizePieceLinks(array $links): array { return array_map(function (MachinePieceLink $link): array { $piece = $link->getPiece(); $parentLink = $link->getParentLink(); $type = $piece->getTypePiece(); return [ 'id' => $link->getId(), 'linkId' => $link->getId(), 'machineId' => $link->getMachine()->getId(), 'pieceId' => $piece->getId(), 'piece' => $this->normalizePiece($piece), 'parentLinkId' => $parentLink?->getId(), 'parentComponentLinkId' => $parentLink?->getId(), 'parentComponentId' => $parentLink?->getComposant()->getId(), 'overrides' => $this->normalizeOverrides($link), 'quantity' => $this->resolvePieceQuantity($link), 'contextCustomFields' => $type ? $this->normalizeContextCustomFieldDefinitions($type->getPieceCustomFields()) : [], 'contextCustomFieldValues' => $this->normalizeCustomFieldValues($link->getContextFieldValues()), ]; }, $links); } ``` - [ ] **Step 5: Run linter** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 6: Commit** ```bash git add src/Controller/MachineStructureController.php git commit -m "feat(custom-fields) : expose context custom fields in machine structure response" ``` --- ## Task 5: `CustomFieldValueController` — support link-based upsert **Files:** - Modify: `src/Controller/CustomFieldValueController.php` - [ ] **Step 1: Inject link repositories in constructor** In the constructor (line 24), add: ```php private readonly MachineComponentLinkRepository $machineComponentLinkRepository, private readonly MachinePieceLinkRepository $machinePieceLinkRepository, ``` Add use statements at the top: ```php use App\Repository\MachineComponentLinkRepository; use App\Repository\MachinePieceLinkRepository; ``` - [ ] **Step 2: Extend `resolveTarget` to support link entities** In `resolveTarget` (line 211), the method applies `strtolower()` on entityType at line 213. The candidate loop and match block must handle this. Update the candidate list in the foreach (line 217): ```php foreach (['machine', 'composant', 'piece', 'product', 'machineComponentLink', 'machinePieceLink'] as $candidate) { ``` **IMPORTANT:** The candidate loop assigns `$entityType` in camelCase, but `strtolower()` (line 213) only applies when `entityType` comes from the payload directly. Add a normalization after the loop closes (after the existing `break;` at line 226), before the empty-check at line 229: ```php $entityType = strtolower($entityType); ``` This ensures both code paths (direct payload `entityType` and candidate loop) deliver lowercase to the match block. Update the match block (line 233) — all keys lowercase, but `resolveEntity` returns camelCase type for `applyTarget`: ```php return match ($entityType) { 'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository), 'composant' => $this->resolveEntity('composant', $entityId, $this->composantRepository), 'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository), 'product' => $this->resolveEntity('product', $entityId, $this->productRepository), 'machinecomponentlink' => $this->resolveEntity('machineComponentLink', $entityId, $this->machineComponentLinkRepository), 'machinepiecelink' => $this->resolveEntity('machinePieceLink', $entityId, $this->machinePieceLinkRepository), default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400), }; ``` - [ ] **Step 3: Extend `applyTarget` for link entities** Add two new cases in `applyTarget` (line 252): ```php case 'machineComponentLink': $value->setMachineComponentLink($entity); $value->setComposant($entity->getComposant()); break; case 'machinePieceLink': $value->setMachinePieceLink($entity); $value->setPiece($entity->getPiece()); break; ``` - [ ] **Step 4: Run linter** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 5: Commit** ```bash git add src/Controller/CustomFieldValueController.php git commit -m "feat(custom-fields) : support link-based upsert in CustomFieldValueController" ``` --- ## Task 6: Clone support — copy context field values **Files:** - Modify: `src/Controller/MachineStructureController.php` (after `cloneProductLinks`, before `flush` in `cloneMachine`) - [ ] **Step 1: Add `cloneContextFieldValues` helper method** Add after `cloneProductLinks`: ```php /** * @param array $componentLinkMap * @param array $pieceLinkMap */ private function cloneContextFieldValues( array $componentLinkMap, array $pieceLinkMap, ): void { foreach ($componentLinkMap as $oldLinkId => $newLink) { $oldLink = $this->machineComponentLinkRepository->find($oldLinkId); if (!$oldLink) { continue; } foreach ($oldLink->getContextFieldValues() as $cfv) { $newValue = new CustomFieldValue(); $newValue->setCustomField($cfv->getCustomField()); $newValue->setValue($cfv->getValue()); $newValue->setMachineComponentLink($newLink); $newValue->setComposant($newLink->getComposant()); $this->entityManager->persist($newValue); } } foreach ($pieceLinkMap as $oldLinkId => $newLink) { $oldLink = $this->machinePieceLinkRepository->find($oldLinkId); if (!$oldLink) { continue; } foreach ($oldLink->getContextFieldValues() as $cfv) { $newValue = new CustomFieldValue(); $newValue->setCustomField($cfv->getCustomField()); $newValue->setValue($cfv->getValue()); $newValue->setMachinePieceLink($newLink); $newValue->setPiece($newLink->getPiece()); $this->entityManager->persist($newValue); } } } ``` - [ ] **Step 2: Call from `cloneMachine` method** In `cloneMachine` (line 113), after `$this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap);` (line 163) and before `$this->entityManager->flush();` (line 165), add: ```php $this->cloneContextFieldValues($componentLinkMap, $pieceLinkMap); ``` - [ ] **Step 3: Run linter** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 4: Commit** ```bash git add src/Controller/MachineStructureController.php git commit -m "feat(custom-fields) : clone context field values on machine clone" ``` --- ## Task 7: Backend tests **Files:** - Create: `tests/Api/Entity/MachineContextCustomFieldTest.php` - [ ] **Step 1: Write test class** ```php createGestionnaireClient(); $site = $this->createSite('Site A'); $modelType = $this->createModelType('Motor', 'MOT', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'Voltage', type: 'number', typeComposant: $modelType, machineContextOnly: true, ); $normalField = $this->createCustomField( name: 'Serial', type: 'text', typeComposant: $modelType, ); $machine = $this->createMachine('Machine A', $site); $composant = $this->createComposant('Motor 1', 'MOT-001', $modelType); $link = $this->createMachineComponentLink($machine, $composant); $this->createCustomFieldValue( customField: $contextField, value: '220', composant: $composant, machineComponentLink: $link, ); $response = $client->request('GET', '/api/machines/'.$machine->getId().'/structure'); $this->assertResponseIsSuccessful(); $data = $response->toArray(); $componentLink = $data['componentLinks'][0]; $this->assertArrayHasKey('contextCustomFields', $componentLink); $this->assertCount(1, $componentLink['contextCustomFields']); $this->assertSame('Voltage', $componentLink['contextCustomFields'][0]['name']); $this->assertTrue($componentLink['contextCustomFields'][0]['machineContextOnly']); $this->assertArrayHasKey('contextCustomFieldValues', $componentLink); $this->assertCount(1, $componentLink['contextCustomFieldValues']); $this->assertSame('220', $componentLink['contextCustomFieldValues'][0]['value']); $normalFields = array_filter( $componentLink['composant']['customFields'], fn (array $f) => $f['name'] === 'Serial', ); $this->assertCount(1, $normalFields); } public function testStructureReturnsContextFieldsOnPieceLink(): void { $client = $this->createGestionnaireClient(); $site = $this->createSite('Site B'); $modelType = $this->createModelType('Bearing', 'BRG', ModelCategory::PIECE); $contextField = $this->createCustomField( name: 'Wear Level', type: 'select', typePiece: $modelType, machineContextOnly: true, ); $contextField->setOptions(['Good', 'Fair', 'Replace']); $this->getEntityManager()->flush(); $machine = $this->createMachine('Machine B', $site); $piece = $this->createPiece('Bearing 1', 'BRG-001', $modelType); $link = $this->createMachinePieceLink($machine, $piece); $this->createCustomFieldValue( customField: $contextField, value: 'Fair', piece: $piece, machinePieceLink: $link, ); $response = $client->request('GET', '/api/machines/'.$machine->getId().'/structure'); $data = $response->toArray(); $pieceLink = $data['pieceLinks'][0]; $this->assertCount(1, $pieceLink['contextCustomFields']); $this->assertSame('Wear Level', $pieceLink['contextCustomFields'][0]['name']); $this->assertCount(1, $pieceLink['contextCustomFieldValues']); $this->assertSame('Fair', $pieceLink['contextCustomFieldValues'][0]['value']); } public function testUpsertContextFieldValueViaComponentLink(): void { $client = $this->createGestionnaireClient(); $site = $this->createSite('Site C'); $modelType = $this->createModelType('Pump', 'PMP', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'Flow Rate', type: 'number', typeComposant: $modelType, machineContextOnly: true, ); $machine = $this->createMachine('Machine C', $site); $composant = $this->createComposant('Pump 1', 'PMP-001', $modelType); $link = $this->createMachineComponentLink($machine, $composant); $response = $client->request('POST', '/api/custom-fields/values/upsert', [ 'json' => [ 'customFieldId' => $contextField->getId(), 'machineComponentLinkId' => $link->getId(), 'value' => '380', ], ]); $this->assertResponseIsSuccessful(); $data = $response->toArray(); $this->assertSame('380', $data['value']); } public function testSameComposantDifferentValuesPerMachine(): void { $client = $this->createGestionnaireClient(); $site = $this->createSite('Site D'); $modelType = $this->createModelType('Valve', 'VLV', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'Pressure', type: 'number', typeComposant: $modelType, machineContextOnly: true, ); $machineA = $this->createMachine('Machine A', $site); $machineB = $this->createMachine('Machine B', $site); $composant = $this->createComposant('Valve 1', 'VLV-001', $modelType); $linkA = $this->createMachineComponentLink($machineA, $composant); $linkB = $this->createMachineComponentLink($machineB, $composant); $this->createCustomFieldValue( customField: $contextField, value: '100', composant: $composant, machineComponentLink: $linkA, ); $this->createCustomFieldValue( customField: $contextField, value: '200', composant: $composant, machineComponentLink: $linkB, ); $dataA = $client->request('GET', '/api/machines/'.$machineA->getId().'/structure')->toArray(); $this->assertSame('100', $dataA['componentLinks'][0]['contextCustomFieldValues'][0]['value']); $dataB = $client->request('GET', '/api/machines/'.$machineB->getId().'/structure')->toArray(); $this->assertSame('200', $dataB['componentLinks'][0]['contextCustomFieldValues'][0]['value']); } public function testMachineContextOnlyFieldSerialization(): void { $client = $this->createGestionnaireClient(); $site = $this->createSite('Site E'); $modelType = $this->createModelType('Sensor', 'SNS', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'Calibration Date', type: 'date', typeComposant: $modelType, machineContextOnly: true, ); $response = $client->request('GET', '/api/custom_fields/'.$contextField->getId()); $this->assertResponseIsSuccessful(); $data = $response->toArray(); $this->assertTrue($data['machineContextOnly']); } public function testCloneMachineCopiesContextFieldValues(): void { $client = $this->createGestionnaireClient(); $site = $this->createSite('Site F'); $modelType = $this->createModelType('Motor Clone', 'MOTC', ModelCategory::COMPONENT); $contextField = $this->createCustomField( name: 'RPM Setting', type: 'number', typeComposant: $modelType, machineContextOnly: true, ); $source = $this->createMachine('Source Machine', $site); $composant = $this->createComposant('Motor C', 'MOTC-001', $modelType); $link = $this->createMachineComponentLink($source, $composant); $this->createCustomFieldValue( customField: $contextField, value: '3000', composant: $composant, machineComponentLink: $link, ); $response = $client->request('POST', '/api/machines/'.$source->getId().'/clone', [ 'json' => [ 'name' => 'Cloned Machine', 'siteId' => $site->getId(), ], ]); $this->assertResponseStatusCodeSame(201); $data = $response->toArray(); $clonedLink = $data['componentLinks'][0] ?? null; $this->assertNotNull($clonedLink); $this->assertCount(1, $clonedLink['contextCustomFieldValues']); $this->assertSame('3000', $clonedLink['contextCustomFieldValues'][0]['value']); } } ``` - [ ] **Step 2: Run tests** ```bash make test FILES=tests/Api/Entity/MachineContextCustomFieldTest.php ``` Expected: All 6 tests pass. - [ ] **Step 3: Run linter** ```bash make php-cs-fixer-allow-risky ``` - [ ] **Step 4: Run full test suite** ```bash make test ``` Expected: All existing tests still pass. - [ ] **Step 5: Commit** ```bash git add tests/Api/Entity/MachineContextCustomFieldTest.php git commit -m "test(custom-fields) : add tests for machine context custom fields" ```